兼顧瀏覽器相容性、一直以來都是做前端開發的一大挑戰。我自己從 IE 4 的年代就開始在寫網頁,那個時代根本等於沒有標準可言,瀏覽器各玩各的規格,要兼顧相容性是一件超級痛苦的事情;到了現代有了 ECMAScript 和 W3C 標準一統天下之後,事情當然是好了很多,但那也不等於相容性問題通通都不見了,畢竟同一款瀏覽器的不同版本對於語法或 API 的支援都不盡相同,更不要說除了主流瀏覽器之外還有一大堆相對小眾(但可能在某些地區特別盛行)的瀏覽器了。在本篇當中,我來分享一些處理相容性方面的心得。
內容目錄
瀏覽器涵蓋率
心理學上很容易有一種錯覺,會在某些議題上以為大家都跟自己差不多。以這邊的情況來說,會看這篇文章的各位基本上都是開發者,我們自己的裝置上當然都會安裝著某種主流瀏覽器的最新版本,這種時候我們很容易產生一種錯覺,以為絕大多數的使用者都是如此、以為只要涵蓋了主流瀏覽器的最新兩三個版本就已經足夠涵蓋例如 99% 以上的使用者。
不過事實遠遠並不是如此。上面第一張圖片中展示的是開發中的新版本 BP Studio 所設定的瀏覽器最低支援版本,特別注意到那些版本數字一點都不新——大概都是五年左右老的版本。關於這些最低版本是怎麼決定出來的我等一下再說明,但總之既然已經涵蓋那麼舊的版本了,應該大部分的使用者都涵蓋到了吧?我本來也會這麼以為。然而,把那些設定輸入到 Browserslist 網站上去一檢查就發現:其實就全球來說這只涵蓋了 93.9% 而已,換句話說,大約每 18 個人就會有一個人沒辦法被我設定的支援範圍涵蓋。更有甚者,在至少兩個國家(奈及利亞與坦尚尼亞)當中,我所設定的範圍連 80% 涵蓋都不到,亦即在那些國家之中每五個人就有一個沒被涵蓋。
當然,這些國家的瀏覽器市占率確實有它們的特別之處。中國有很多中國產的小眾瀏覽器(例如 UC、360、QQ、搜狗等等)在瓜分 Chrome 以外的版圖,在奈及利亞以及若干非洲國家則是有很多人愛用 Opera Mini 這款瀏覽器(因為它主打低基礎建設下的順暢性),又例如在俄國、市占率第二高的是一款大部分的人都沒聽過的俄國產瀏覽器 Yandex。關於這些小眾瀏覽器要怎麼辦、我稍後再說明,不過我想這些事實應該可以突顯出、在處理瀏覽器相容性的時候,直覺想像跟現實的差距有多大。特別是在版本方面,各位永遠會很驚訝有多少人不管過了多久就是沒有升級瀏覽器的版本。1
進行相容性測試
OK,那沒有涵蓋到又如何?真的會怎樣嗎?這我當然沒辦法回答,唯有真的測試下去才知道,但是就如同涵蓋率的事情一般,結果很可能會讓各位很意外的:原本可能會覺得,自己這樣寫應該起碼在舊一點的瀏覽器上好歹能顯示個提示訊息什麼的,卻沒想到實際的結果是根本全白一片、什麼都跑不出來。可以說,意識到兼顧相容性的重要,就要先從進行過相容性測試(然後被當頭棒喝)開始。
問題是,要進行那樣的測試的前提是得要有一個「裝了舊版瀏覽器的裝置」可以用、而且可能還要有非常多種。這樣的資源對於比較小規模的開發團隊來說是很難擁有的;即便有心想去張羅,試問各位事到如今還知道 IE 要去哪裡下載安裝嗎?我自己先承認我不知道。
實務上比較可行的辦法,是透過像例如 BrowserStack 這樣的雲端服務來進行相容性測試,這類服務能讓我們實際連線到一台雲端機器上頭去操作舊版的瀏覽器或裝置、以測試自己的網站的行為表現。BrowserStack 提供了超過三千種的裝置與瀏覽器組合,雖然其中比較沒有涵蓋到小眾的瀏覽器,但是主流瀏覽器的歷史版本是相當齊全的,甚至連 IE 6-11 都有。而且 BrowserStack 也可以安裝本地應用程式、以便在開發階段中直接測試 localhost 上頭的站台而無須經過遠端佈署,相當強大。
不過,BrowserStack 的定價可能也會令一些人感到猶豫:截至本文寫成,它們的主要方案(單一使用者、可測試各種桌機與手機、年繳)的定價是每個月 $39 美金,而由於相容性測試這種事情又不是一天到晚都需要做,我可以想像很多人會對這種價格感到皺眉的。
不過 BrowserStack 有一個超佛心的方案!
如果你是開源專案的維護者,想用他們的服務來測試自己的專案,那麼可以向他們申請加入開源專案贊助,註冊帳號之後輸入自己的開源專案網址即可(有時可能會需要經過審核),如果通過的話,前述定價每個月 $39 的方案就完全免費讓你隨便用!
BrowserStack 在使用上也很簡單直覺:在主控台畫面的左邊選擇裝置版本,右邊點選瀏覽器版本,即可開啟一個遠端操作畫面讓你操作;是實際可以完整操作的遠端機器,而不是只有執行畫面截圖之類的。如果選擇的是桌機裝置,自然也可以打開瀏覽器的開發工具來檢視執行的 log 等等,非常方便。
但是,如同剛才提到的,它有一個弱點就是它對於小眾瀏覽器就沒有那麼齊全的版本收集了。例如它雖然在一些平台上面有 Yandex,但是截至本文寫成為止它只有 14.12 一個版本(而且很舊)可以測試,其它例如 Samsung Internet 或 UC Browser 也有類似的問題。怎麼辦呢?幸好,其實現在市面上存在的小眾瀏覽器幾乎都是 Chromium 的衍生物 2,所以我們只要有辦法知道那些瀏覽器與 Chromium 之間的版本對應關係,大致上來說相容性就是可以類比的。例如在這個維基百科條目上就有列出 Samsung Internet 對應的 Chromium 版本,而我知道我想要支援到 Chrome 66、所以對應過去基本上就是第 9 版,依此類推。
如果找不到這方面的文件呢?那還有一招就是去調查 user agent 字串。例如假設我想知道 UC Browser 跟 Chromium 之間的對應關係,我可以到 user-agents.net 去把所有 UC Browser 已知的 user agent 字串下載下來,其中一個例子是這樣:
Mozilla/5.0 (Linux; U; Android 8.1.0; en-US; Redmi 6 Build/O11019) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/66.0.3359.126 UCBrowser/12.9.7.1158 Mobile Safari/537.36
這樣一來我就知道 Chromium 66 對應的就是 UC Browser 12.9,依此類推。
那如果連這樣做都調查不出來呢?此時我也沒招了,我就只能把那些瀏覽器列為「非官方支援」;也就是說,理論上如果使用者有更新到最新版的話應該是可以用我的 app,但是如果沒有更新,則我沒辦法保證支援到多舊的版本這樣。
決定向下支援程度
有了測試環境之後,我們接著就可以開始來仔細確認自己的網站(或 web 應用程式)到底可以支援到多舊的版本上了。當然,一個版本一個版本地往回頭去確認也太累了,所以我自己是採用如下的策略來加快找出第一個無法運作的版本:
- 先回溯到早到荒謬的程度,找到一個已知無法運作的瀏覽器版本。
- 打開瀏覽器的 console,看看是什麼東西在造成問題。
- 到 CanIUse 網站上去查看看「那個東西」是一直到第幾版的瀏覽器才開始支援。
- 用該版本的瀏覽器去測試看看,如果站台能正常運作,那麼該版本就是能支援的最低版本;否則回到步驟 2. 並繼續。
如此一來我們不僅確定出目前最低的支援度,也對於站台當中使用到了哪些關鍵性的新技術有所掌握。找出最低支援程度之後,我們就可以來決定:究竟是要就此打住,還是要設法修改網站的程式、以繼續往前推進?這個決定的背後會取決於幾個考量點:
- 假如就這樣停住的話,目前的瀏覽器涵蓋率是否已經達到滿意程度了?另一方面,繼續推進的話又能夠增加多少個涵蓋率的百分比?
- 導致不相容的癥結是什麼樣的東西?是 ECMAScript 語法還是網頁 API?發生的位置是在自己的程式裡頭、還是在第三方程式庫之中?有辦法透過建置選項來處理、或是加以 polyfill 嗎?
關於第二點的若干提問,這邊可以再多做進一步的解說。如果引起問題的根源在於自己的程式使用到了舊瀏覽器不支援的新 ECMAScript 語法(像是 ??
等等),視使用的建置工具而定,很多時候可以簡單地透過設定建置選項(通常稱為 target)來把最後的程式轉換成舊版的語法來解決 3。但是如果不相容性發生的位置在於引用的第三方程式庫當中,或者那並不單純只是語法的問題而是一些新的內建物件或方法(像是 BigInt
、Array.prototype.flatMap()
等等)、或甚至是 web API,那就無法這麼簡單解決了。此時第一個可以考慮的解法就是引入 polyfill,亦即在舊的環境當中把新的功能實作出來的一段程式碼(絕大多數的例子中,它會順便去偵測當前的環境是否已經支援了新功能,並只有在不支援的時候才會去進行填補)。最有名的 polyfill 程式庫大抵就是 core-js 了,不過我自己並沒有在 BP Studio 當中使用到它或是其它既有的 polyfill,而都是自己寫就是了(純粹是因為我太挑剃了,我覺得既有的 polyfill 都太冗長了,自己寫比較清爽)。
最麻煩的情況是又是出現在第三方程式庫中、且又是無法 polyfill 的新語法;我在 BP Studio 的開發中遇到的一個例子是第三方程式庫用了正規表達式的 s 旗標,這個語法是沒有辦法 polyfill 的、而只能透過轉譯成 ES2017 以下來達到相容性 4,但這樣一來就變成了第三方程式庫有更新的時候、我都要重新建置或者去加以轉譯。這我自己評估之後覺得算了吧,反正再繼續推進的意義也很有限,所以最終決定的支援版本有一部分就定在這一關卡上了。
CSS 相容性
相容性的考量不只是在於程式碼的部份,CSS 的部份也有。如果站台使用了舊版瀏覽器不支援的新 CSS 語法,雖然多數情況中不至於影響站台的運作,但是可能會造成嚴重的跑版等等的問題。
然而,要找出站台的 CSS 底限版本所在卻相對地不是那麼容易,因為 CSS 的不相容性未必總是很明顯地會馬上呈現在畫面上。所以我自己的策略是以 JavaScript 為準去決定版本支援程度,然後 CSS 的相容性再去用 PostCSS 去解決;如同 ECMAScript 語法很多存在著較舊的等價形式,新的 CSS 語法很多也有舊版的等價寫法,用 PostCSS 搭配 PostCSSPresetEnv 的外掛就可以根據我們設定的 BrowsersList 組態來自動轉換。
至於一些不存在等價寫法的新語法(例如 min()
函數),我們則可以給一個 fallback 的設定,例如:
div {
/* 這是寫給舊版瀏覽器的 fallback */
padding: 10px;
/* 新的語法寫在後面,此時瀏覽器如果不支援就會忽略這一列,
而如果支援的話這一列就會把前一列的設定覆蓋掉。 */
padding: min(10px, 1vw);
}
又例如有一些 CSS 不是在決定外觀而是在決定行為,此時則可能可以用一些程式來模擬。例如 Safari 一直到 13 版才支援 touch-action: none
,這種時候我們可以用類似這樣的方式來 polyfill:
// 底下 el 是我們關切的網頁元件,我們已經用 CSS 指定了 touch-action: none
// 要判斷一個 CSS 語法是否支援,辦法就是去抓實際被採用的值看看是否與設定相符
if(getComputedStyle(el).touchAction != "none") {
// 如果抓出來的值並不是我們所指定的 none,那就表示當前的瀏覽器不支援,
// 此時我們就改用程式的方法達到同樣的效果
el.addEventListener("touchmove", (e: TouchEvent) => {
// 只要是兩指以上的觸控就取消掉預設行為
if(e.touches.length > 1) e.preventDefault();
}, { passive: false }); // 明確指定非被動監聽
}
開發工具
相容性這種事情當然不是決定一次就沒事了,而是在未來的開發當中、也是要持續根據設定的支援程度來撰寫和維護的。隨著瀏覽器持續推陳出新、以及我們持續地學習新的技術,我們有的時候會不小心忘記某些寫法在我們所設定的支援程度當中是不能用的。為了輔助我們抓出這類的情況,一些 linting 的工具就能派上用場了。
針對瀏覽器相容性的問題,我們可以用 ESLint 搭配 eslint-plugin-compat(以及對應於 TypeScript 的 eslint-plugin-typescript-compat)來警示我們關於 ECMAScript 或網頁 API 的不相容性,而 CSS 的部份則有 Stylelint 搭配 stylelint-no-unsupported-browser-features 來作到同樣的事情。這些工具都支援同樣的 BrowsersList 組態設定,只要在一個地方寫好支援清單、這些工具就都通吃。
處理更舊的瀏覽器
另一方面,雖然我們設定好了要支援到多舊的瀏覽器為止,但那是指「能夠讓站台正常運作」的支援程度而言,對於更舊的瀏覽器,雖然站台會無法運作,但是我們還是會希望好歹能夠在這些瀏覽器上面顯示一些提示訊息、讓使用者知道發生了什麼事,而不是打開的時候完全空白或是一團混亂。
針對這部份,BP Studio 處理的方式簡單來說大致是這樣的:
- 一開始在頁面就已經寫死了一個
<div id="error">
、內容包含了預設錯誤提示訊息,並設定它的display: none
。至於網頁真正的主體,則是全部放在一個<div id="app">
裡頭。如果網頁載入的過程中發生了任何的問題,就藉由切換display
樣式來隱藏#app
元件並顯示#error
元件。 - 網頁錯誤的發生則是透過
window.onerror
以及window.onunhandledrejection
這兩個事件來捕捉的。註冊這兩個事件的程式碼是寫死在 HTML 上頭的(以免有外部 script 資源因為任何連線因素而載入失敗)、且會在其它任何的程式碼執行之前執行。為了確保最高的相容性,該段程式碼是採用古老的 ES3 的語法去寫的,所以例如不會有let
等等的語法出現。 - 要注意的是在 IE 8 當中連
window.onerror
都不支援,所以沒有辦法設置全域的錯誤處理機制。因此在程式碼剛開始啟動的時候就會先檢查當前的環境是不是 IE 5,如果是的話就直接在window.onload
事件當中進行前述的畫面切換(必須要等到onload
是因為那一瞬間當中前述的<div>
都還沒有被載入,沒有東西可以切換)。
不過這裡頭有一個實務上的小陷阱需要注意:有一些 in-app 的瀏覽器或瀏覽器外掛會在網頁當中注入一些它們自己的程式碼,而且那些注入的程式碼可能會觸發一些沒有被 catch 的錯誤,這種情況當中也一樣會觸發 window.onerror
事件,即便那些錯誤完全不影響我們站台的運作。我們可以透過一些關鍵字來過濾掉一些已知的錯誤,我自己在 BP Studio 裡面用的包括:
var ignoreError = [
"WeixinJSBridge",
"_WXJS",
"__firefox__",
"$UCBrowser",
"_WebViewJavascriptBridge",
"hackLocationSuccess"
];
我只要看到錯誤的訊息裡面包括這當中任何一個關鍵字,就忽略它。這些關鍵字有些是其它程式庫當中已知的、另外一些是我自己在 Google Analytics 的回報當中看到的。
結語
隨著資料庫的完備和各種開發工具的進步,現在即便是一個人維護的專案也能夠把瀏覽器相容性的難題兼顧得滿不錯的,跟 20 年前比起來真的差太多了。這篇就分享到這邊,希望對各位有幫助!
-
這個現象在 Safari 上面特別明顯,因為 Safari 的版本是跟 Mac 或 iOS 的作業系統版本綁定的,而在一些情況下作業系統甚至會因為裝置老舊而沒有更新的選項存在,這些門檻都使得使用者停留在舊版本的 Safari 上的情況比其它瀏覽器都更多。關於 Safari,特別值得一提的是在 15.4 版之前,Mac 上頭的 Safari 和 iOS 上的 Safari 的版本號未必是完全對得起來的,所以在那之前的支援度是需要分開來確認的。 ↩
-
Opera Mini 是一個值得一提的例外;當它開啟了 Extreme(極限節約流量)模式時,它會變成使用自己的 Presto 引擎,此時它不再相容於 Chromium。或許因為這個緣故,連 CanIUse 網站都沒辦法簡單按照版本來整理其相容性資料,而只會用一個「all」版本來描述之。 ↩
-
具體來說,轉譯的方法是去掉
s
旗標、然後把表達式裡面的「.
」一律替換成「[\s\S]
」。 ↩ -
我用的方法是根據
Promise
物件是否被支援來判斷的,因為 IE 全部的版本都不支援。當然其它瀏覽器非常老舊的版本也一樣,但是那遠超過我的最低支援程度,所以在我的情況當中無妨。 ↩
留言