在前端開發過程中,同樣的 HTML 與 CSS 在不同瀏覽器中卻可能有截然不同的顯示效果。
為什麼一樣是瀏覽器,卻會有這麼大的差異?這就要從瀏覽器大戰開始說起。
差異的來源#
由於瀏覽器是使用者探索網路的入口,從 1990 年代開始,各家廠商都依據當時的 HTML 規範,實作出了瀏覽器;脫穎而出的兩強分別為市場的先驅者網景 Netscape Navigator,以及壟斷個人電腦作業系統的微軟 Internet Explorer。
為了搶佔更高的市佔率,兩家公司都開始實作標準外的功能,甚至忽視當時的標準、設計個別專屬的格式,迫使當時的開發者必須在網頁上標註「用 ___ 瀏覽器方可得到最佳瀏覽效果」,如此的行為又更強化了標準分歧。
最終微軟挾帶豐富的功能及龐大財力,擊敗網景,結束了第一次瀏覽器大戰。
從第一次瀏覽器大戰的故事就能反映出,瀏覽器差異的根源就在於「規範與實作之間的差距」。
規範與實作#
瀏覽器的實作必須遵從幾個單位的規範:
- ECMA:制定標準的 ECMAScript,這也就是 ES6 名稱的由來,ECMAScript 6
- W3C:全球資訊網協會,負責制定網頁內容的標準,包含 HTML、CSS、DOM、XML 等等
ECMAScript 是標準,JavaScript 是標準的一種實現,雖然口語上時常混用,但還是要稍微留心,不要搞混。
這些標準制定組織會在與社群反覆討論的過程中,逐漸更新標準。在新標準草案被提出,並能得到大部分討論者的正面回饋時,瀏覽器開發者便會著手實作這些功能;通常新功能由於還在實驗性質,可能會需要使用者到設定頁面自行開啟,或是在程式參數、樣式屬性名稱中加上特定的前綴字。
現在的瀏覽器不太會像當年大戰時各自搞特規,但由於屬性可能被棄用、預設設定可能會改動,即便瀏覽器符合現今的標準,也不等於它能夠符合未來的標準。
解決方案#
支援所有瀏覽器並不現實。版本持續更迭,無止境向下相容會讓開發完全無法使用新功能,效能與開發效率都會大打折扣。在可以放棄過舊瀏覽器的前提下,盡量擴張支援範圍,便能找到優化空間。
樣式差異#
CSS 的部份,可以加上實驗階段的樣式名稱。例如 flex 排版:
display: -webkit-box;
display: -moz-box;
display: -ms-flexbox;
display: -webkit-flex;
display: flex;如上例,在尚未成為標準時,各家瀏覽器的名稱除了加上前綴外,還有可能屬性名字完全不一樣。一個網站絕對不會只有少少幾條樣式需要加上屬性,這種情況靠人工處理是不太現實的,這時就要仰賴其他套件做 CSS 後處理。
例如實用的 Autoprefixer,只要透過 Browserslist 設定欲支援的瀏覽器版本:
'cover 99.5%':指定要支援最流行的 99.5% 瀏覽器'> .5% or last 2 versions':指定要支援市佔率超過 0.5% 或最新兩個版本的瀏覽器'not ie < 11':指定不支援 ie < 11 的版本
藉由簡單的設定,就能自動幫開發者解決樣式的瀏覽器差異問題。
程式差異#
瀏覽器開發者撰寫 Web API 時,也會有如同 CSS 加前綴字的作法。例如 Web Audio API:
window.AudioContext = window.AudioContext || window.webkitAudioContext;AudioContext 對於一部分舊版的瀏覽器,可以透過這樣的設定,讓環境變數暫時設定成實驗性變數,功能就得以正常運作。
對於完全沒有實驗性質變數的情況,也有別的處理方法,例如 Polyfill。Polyfill 的概念很簡單:原生沒有這個功能,那就從現有的功能重新實作類似的功能。開發者可以透過 Polyfill.io 之類的網站,設定要支援的功能版本,並將補丁檔引入到專案中。
例如 isNaN 函式的補丁:
// Number.isNaN
/* global CreateMethodProperty, Type */
(function () {
var that = this;
// 20.1.2.4. Number.isNaN ( number )
CreateMethodProperty(Number, "isNaN", function isNaN(number) {
// 1. If Type(number) is not Number, return false.
if (Type(number) !== "number") {
return false;
}
// 2. If number is NaN, return true.
if (that.isNaN(number)) {
return true;
}
// 3. Otherwise, return false.
return false;
});
})();CreateMethodProperty 把補丁函式及函式名稱綁到指定的原生物件上,而 Type 則是將 typeof 的結果做一些處理,例如判斷是不是 Symbol 之類的。
相比於 Polyfill 這種補足瀏覽器的解決方案,相反的有另一種作法則是將程式碼經過程式處理,讓它能向下符合目標版本的環境。最有名的套件莫過於 Babel:
// 處理前
[1, 2, 3]
.map((c) => c ** 2)
[
// 處理後
(1, 2, 3)
].map(function (c) {
return Math.pow(c, 2);
});這邊用到了 ES6 的箭頭函式,及 ES7 的指數運算子,經過 Babel 處理後都轉成了 ES5 或更早版本就支援的原生語法。因為有 Babel 這種強大的工具,前端開發者便能肆無忌憚的使用最新的語法,並且不用花太多時間處理使用者瀏覽器相容的問題,大幅度的提升開發效率及開發者體驗。
結語#
許多瀏覽器獨有的特殊眉角,礙於篇幅未一一說明,多半只有踩過雷的人才知道。面對瀏覽器差異,開發者只能靠經驗累積。
從第一次瀏覽器大戰出發,藉由歷史案例可一窺標準與實作嚴重脫節的後果。Autoprefixer、Polyfill、Babel 這些工具雖無法涵蓋所有情況,但應付一般場景綽綽有餘。
原文出處#
- 原書/iThome:https://ithelp.ithome.com.tw/articles/10223981