- date
- entry
- 049
- topic
- infrastructure
- rev
- —
Chrome 擋掉網站資料,首頁大半空白:讀 localStorage 丟出 SecurityError,後面的程式都沒跑
10 月 17 日有人回報 yutuo-tech.com 的首頁是空白的。我在本機用封鎖網站資料的 Chrome 設定檔,重現了一條會造成空白的路徑:讀 localStorage 這一行丟出 SecurityError,整支腳本停在那裡,負責讓區塊顯示、填入清單的程式都排在後面,從來沒有執行。
把使用者偏好存在 localStorage,讀不到就用預設值,常見的寫法像這樣:localStorage.getItem("lang") || "zh-TW"。儲存可以用、只是這個 key 沒存過的時候,getItem 回 null,後面接預設值,看起來已經處理了「讀不到」。
但瀏覽器擋掉這個網站的資料時,讀不到的方式不是回 null,是丟例外。10 月 17 日 yutuo-tech.com 首頁空白的回報,首頁程式裡就有這樣一條路徑。這篇要談的是一個偏好設定怎麼讓大半個首頁消失,依序是:哪些設定會讓它丟例外、為什麼結果是大半空白而不是少一個功能、常見的檢查為什麼擋不住、修法的兩步,以及還有誰會看到同樣的空白。
擋掉網站資料,讀屬性就丟例外
我在本機起一個靜態伺服器,用 headless Chrome 155.0.8059.80 開一個探測頁。我準備了幾個 --user-data-dir 設定檔,在裡面的 Default/Preferences 先寫好內容設定。封鎖的那個把 profile.default_content_setting_values.cookies 設成 2(封鎖),對應設定頁裡「不允許網站儲存資料」的選項;Chrome 的網站資料封鎖用的是 cookies 這個內容設定。探測頁的輸出:
typeof threw: SecurityError: Failed to read the 'localStorage' property from 'Window': Access is denied for this document.
access threw: SecurityError: Failed to read the 'localStorage' property from 'Window': Access is denied for this document.
in: true
session threw: SecurityError: Failed to read the 'sessionStorage' property from 'Window': Access is denied for this document.
cookieEnabled: true
丟例外的是讀 window.localStorage 這個屬性本身,還沒走到 getItem。sessionStorage 一樣。不改全域、只對 http://127.0.0.1:8765 這個網站設封鎖例外(profile.content_settings.exceptions.cookies),結果相同。同一個探測頁用沒有改設定的設定檔開,每一項都成功。
回報的人用的是哪個瀏覽器、開了什麼設定,回報裡沒有。我能確認的是首頁的程式有這條會中斷的路徑,照這條路徑重現出來的也是空白;它是一個可能的原因,不是確認過的原因。
為什麼是大半空白
少一個語言偏好,照理只會讓頁面用預設語言。會變成大半空白,是因為另外兩件事剛好疊在一起。第一件在 CSS:首頁的區塊有 .reveal 這個 class,樣式是 opacity: 0,要等 JavaScript 加上 is-visible 才變成 opacity: 1。這是捲動進場動畫常見的做法,首頁的 HTML 裡有 15 個元素帶這個 class,從最上面的主標題區開始;其中 3 個在案例區,CSS 另外把它們設回 opacity: 1,不受影響。
第二件在 JavaScript:把 .reveal 加上 is-visible 的那段 IntersectionObserver 程式,原本在 main.js 的第 1332 行(全檔 1,539 行);讀 localStorage 的 getLang() 在第 956 行就被呼叫。main.js 以 <script type="module"> 載入,module 最外層丟出沒接住的例外,這個 module 尚未執行的後續程式就不會再跑。同樣排在後面的,還有檔案最後一行的 renderAll():首頁有 4 個容器(問題清單、服務卡片、案例、流程)在 HTML 裡是空的,靠它填入內容。所以中斷之後,大部分區塊是透明的,幾個清單是空的。
在一個只有一個 .reveal 區塊的測試頁上,關鍵的 JavaScript 只要三行:
const getLang = () => localStorage.getItem("lang") || "zh-TW";
let currentLang = getLang();
document.querySelectorAll(".reveal").forEach(n => n.classList.add("is-visible"));
封鎖網站資料的設定檔用 --dump-dom 讀結果,section 還是 class="reveal",沒有 is-visible。用 --screenshot 截 600×200 的畫面,再用 Pillow 數灰階低於 128 的像素:正常設定檔是 6,988 個(那就是標題字),封鎖的設定檔是 0。字在 DOM 裡,只是透明。這是測試頁的數字,不是實際首頁的整頁量測。
常見的檢查擋不住
既然例外發生在讀屬性的時候,很多「先確認有沒有 localStorage」的寫法也會在確認時就丟出來。上面探測頁的第一行是 typeof window.localStorage,同樣丟 SecurityError;if (window.localStorage) 也是讀屬性,結果一樣。'localStorage' in window 不讀值,所以不丟,但它回 true;它只說明這個屬性存在,不代表能用。
navigator.cookieEnabled 在這組測試裡也是 true。這個結果我只在 headless 模式加這個設定檔看過,沒有在一般視窗或其他瀏覽器上比對,所以不把它當成通則;能確定的是,至少有一種環境下它不能拿來判斷儲存能不能用。與其事先判斷,不如每一次實際的讀和寫都包在 try 裡,失敗就退回預設;讀成功一次,也不保證之後寫得進去(例如空間配額滿了)。
修法:try/catch 修這一行,調順序防下一次
10 月 17 日的修正做了兩件事,我在重現頁上把它們拆開試,看各自擋住了什麼。只把顯示程式移到最前面、不加 try/catch:
document.querySelectorAll(".reveal").forEach(n => n.classList.add("is-visible"));
const getLang = () => localStorage.getItem("lang") || "zh-TW";
let currentLang = getLang();
document.body.dataset.lang = currentLang;
封鎖的設定檔下,區塊出現了(class="reveal is-visible"),但 body 上沒有 data-lang:第三行呼叫 getLang() 時丟出例外,第四行沒有執行。在真的首頁上,中斷點之後是語言選單、表單事件,還有填入清單的 renderAll()。只調順序,區塊看得到,但這些初始化都沒有完成。
再把讀取包進 try/catch,讀不到就回預設值:
const getLang = () => {
try { return localStorage.getItem("lang") || "zh-TW"; }
catch { return "zh-TW"; }
};
這次 body 有 data-lang="zh-TW",區塊也可見,截圖回到 6,988 個深色像素。實際的 main.js 改了三處:IntersectionObserver 那段搬到 import 之後的第一段,getLang() 的讀取和語言按鈕裡的 setItem 各包一個 try/catch。setItem 失敗時,選擇只套用到這一頁,沒有存下來。
這次的問題,try/catch 單獨就修好了;調順序是另外加的防護。try/catch 處理的是已知會丟例外的那一行;把顯示程式放最前面,是為了「以後別的地方又丟了一個沒想到的同步例外」時,IntersectionObserver 已經註冊好,HTML 裡原本就有的區塊捲進畫面時還是會被加上 is-visible。測試頁是直接加 class,真的首頁是等 observer 的回呼,所以這是「捲到時會出現」,不是一載入就全部可見。它救不回靠 JavaScript 產生的內容,模組載入失敗或顯示程式本身出錯時也幫不上忙。
關掉 JavaScript 的人也看到透明的區塊
修好 localStorage 之後,還有一個情況會得到同樣的透明區塊:訪客的瀏覽器根本不跑 JavaScript。我用另一個設定檔把 profile.default_content_setting_values.javascript 設成 2,截圖的深色像素也是 0。opacity: 0 寫在 CSS 裡,CSS 不管 JavaScript 有沒有跑都會套用。
yutuo-tech.com 的樣式表裡其實有一個例外:prefers-reduced-motion: reduce 時把 .reveal 設回 opacity: 1。我在重現頁加上同樣的規則,用 --force-prefers-reduced-motion 開封鎖網站資料的設定檔,區塊是可見的(6,988);不加這個參數,還是 0。所以有開「減少動態效果」的訪客,就算腳本中斷,HTML 裡原本就有的區塊也看得到;靠 JavaScript 填入的清單還是空的。
如果是我重寫這段,我會讓「隱藏」這個狀態也由 JavaScript 來開:腳本一開始先在 <html> 加一個 class,CSS 只隱藏帶著這個 class 的 .reveal。腳本沒跑,HTML 裡的內容就照常顯示,只是沒有進場動畫;加上 class 之後顯示程式卻失敗的情況,仍然要另外處理。靠 JavaScript 產生的清單,則要在 HTML 裡先放一份靜態內容才救得回來。10 月 17 日的修正沒有做這一步,修正後的版本裡,多數區塊仍要等 JavaScript 才會顯示。
如果只記得一件事
瀏覽器拒絕這個網頁存取儲存時(例如封鎖網站資料),讀 localStorage 這個屬性就會丟 SecurityError,typeof 和 if (window.localStorage) 都會在檢查時丟出來。存偏好的讀寫一律包 try/catch;決定內容看不看得到的程式,放在所有可能出錯的程式前面,或者乾脆不要讓內容預設隱藏。
修訂紀錄
- 首次發布