開發跨瀏覽器策略#

開發能在多種瀏覽器中運作的 JavaScript 程式碼,是一項需要謹慎平衡開發方法論和可用資源的工作。本章探討如何選擇要支援的瀏覽器、跨瀏覽器開發的主要關注點,以及有效的實作策略。

14.1 跨瀏覽器考量#

選擇支援哪些瀏覽器時,意味著以下承諾:

  • 主動對該瀏覽器執行測試套件
  • 修復與該瀏覽器相關的 bug 和 regression
  • 確保程式碼在該瀏覽器上有合理的效能表現

Graded Browser Support#

借鑒 Yahoo! 的做法,建立 browser-support matrix(瀏覽器支援矩陣)——以平台為一軸、瀏覽器為另一軸,為每個組合分配等級(A 到 F),反映支援的重要性和優先順序。

決定支援範圍時需考量的因素:

  • 目標受眾的期望和需求
  • 瀏覽器的市場佔有率
  • 支援該瀏覽器所需的工作量

品質絕不應為了覆蓋率而犧牲。 不要嘗試超出能力範圍的支援——寧可在較少的瀏覽器上做好,也不要在很多瀏覽器上做得馬馬虎虎。

14.2 五大開發關注點#

開發可重用 JavaScript 程式碼時,面臨五個主要挑戰:

  1. Browser bugs(瀏覽器 bug)
  2. Browser bug fixes(瀏覽器 bug 修復)
  3. External code(外部程式碼)
  4. Browser regressions(瀏覽器回歸)
  5. Missing features(缺少的功能)

瀏覽器 Bug 與差異#

  • 程式碼的功能必須在所有支援的瀏覽器中可驗證地正確
  • 需要完善的測試套件,涵蓋常見和邊界使用場景
  • 實作修復時要注意,修復方式不應在瀏覽器修正 bug 後反而造成問題

範例:scrollTop bug

IE 11 和 Firefox 遵循規範在 html 根元素上使用 scrollTop/scrollLeft,而 Safari、Chrome、Opera 需要在 body 元素上操作。開發者若透過 user agent 偵測來處理這個差異,反而可能在瀏覽器修正後造成問題。

判斷某功能是否為 bug 時,務必參照規範驗證。未經規範的 API 可能隨時變更,要特別注意。

瀏覽器 Bug 修復#

  • 不要假設 bug 會永遠存在——大多數 bug 最終會被修復
  • 依賴 bug 存在的修補程式(workaround),在 bug 被修復後可能反而導致破壞
  • 應使用 14.3 節的技巧確保修補方案盡可能不受未來變更影響

外部程式碼與標記#

可重用程式碼必須能與周圍的程式碼共存,需注意:

封裝程式碼:

  • 保持極小的全域足跡(global footprint)
  • 像 jQuery 一樣,只暴露一個全域變數作為命名空間
  • 避免修改既有變數、函式原型或 DOM 元素
var ninja = {}; // 單一命名空間
ninja.hitsuke = function () {
  /* ... */
};

應對品質不佳的外部程式碼:

  • 外部程式碼可能修改函式原型、物件屬性和 DOM 元素方法
  • 要做好防禦性程式設計,假設最壞的情況

DOM Clobbering 問題:

瀏覽器會將 <form> 內的 input 元素以其 idname 值作為 form 元素的屬性。若 id/name 值與既有屬性衝突(如 actionsubmit),原始屬性會被覆蓋:

<form id="form" action="/conceal">
  <input type="text" id="action" />
  <input type="submit" id="submit" />
</form>
document.getElementById("form").action; // 回傳 input 元素而非 "/conceal"
document.getElementById("form").submit(); // TypeError!

避免使用與標準 DOM 屬性名稱相同的 idname 值,特別要避免使用 submit

樣式表與腳本的載入順序:

確保外部樣式表在腳本之前載入,否則腳本可能存取到尚未定義的樣式資訊。

Regressions(回歸)#

Regressions 是瀏覽器引入的非向後相容 API 變更或 bug,導致原本正常的程式碼失效。

預見變更:

function bindEvent(element, type, handle) {
  if (element.addEventListener) {
    element.addEventListener(type, handle, false);
  } else if (element.attachEvent) {
    element.attachEvent("on" + type, handle);
  }
}

透過 feature detection 優先使用標準 API,若不支援再回退到專有 API。

持續監控瀏覽器更新: 定期在新版瀏覽器上執行測試,追蹤各瀏覽器的 nightly builds 和開發部落格。

14.3 實作策略#

安全的跨瀏覽器修復#

最簡單、最安全的修復具備兩個特性:

  • 不會在其他瀏覽器產生負面效果或副作用
  • 不使用任何形式的瀏覽器或功能偵測

範例 1: jQuery 忽略所有瀏覽器中 heightwidth 的負值設定(因為某些版本的 IE 會因此拋出例外)。

範例 2: jQuery 禁止修改已在 DOM 中的 <input> 元素的 type 屬性(因 IE 不允許此操作),統一在所有瀏覽器中拋出資訊性例外。

jQuery 團隊經過權衡,認為提供一致性的 API 比保留某些只在特定瀏覽器才有的功能更重要。開發自己的可重用程式碼時,也可能需要做出類似的取捨。

Feature Detection 與 Polyfills#

Feature detection 透過檢查特定物件或屬性是否存在,來判斷功能是否可用:

if (
  typeof document !== "undefined" &&
  document.addEventListener &&
  document.querySelector &&
  document.querySelectorAll
) {
  // 有足夠的 API 可以建構應用
} else {
  // 提供 fallback
}

Polyfill 是瀏覽器 fallback——若瀏覽器不支援某功能,提供自己的實作:

if (!Array.prototype.find) {
  Array.prototype.find = function (predicate) {
    if (this === null) {
      throw new TypeError("find called on null or undefined");
    }
    if (typeof predicate !== "function") {
      throw new TypeError("predicate must be a function");
    }
    var list = Object(this);
    var length = list.length >>> 0;
    var thisArg = arguments[1];
    var value;
    for (var i = 0; i < length; i++) {
      value = list[i];
      if (predicate.call(thisArg, value, i, list)) {
        return value;
      }
    }
    return undefined;
  };
}

Fallback 的選項包括:

  • 進一步偵測功能,提供部分 JavaScript 功能的精簡體驗
  • 不執行 JavaScript,退回到未腳本化的 HTML
  • 導向網站的簡化版本(如 Google 對 Gmail 的做法)

無法測試的瀏覽器問題#

有些問題在技術上無法或難以測試:

  • Event handler bindings:無法程式化地判斷元素是否已綁定事件 handler
  • Event firing:無法確定瀏覽器是否會觸發特定事件
  • CSS property effects:無法程式化地驗證修改 CSS 屬性(如 coloropacity)是否真的影響了視覺呈現
  • Browser crashes:導致瀏覽器當機的程式碼無法被測試,因為測試本身就會造成當機
  • API performance:某些 API 在不同瀏覽器中速度差異很大,但效能分析成本高,不適合在每次頁面載入時執行
  • Incongruous APIs:有時必須在所有瀏覽器中禁用某功能來繞過特定瀏覽器的 bug

14.4 減少假設#

跨瀏覽器程式碼是一場與假設的對抗。透過巧妙的偵測和設計,可以減少程式碼中的假設數量。

User-agent 偵測的問題:

  • 分析 navigator.userAgent 來推斷瀏覽器行為(browser detection)是常見但危險的做法
  • 假設某個 bug 或功能永遠與特定瀏覽器綁定是災難的根源
  • Feature detection 優於 browser detection

bindEvent 為例的三個隱含假設:

  1. 我們檢查的屬性是可呼叫的函式
  2. 它們是正確的函式,執行我們期望的動作
  3. 這兩種方法是綁定事件的唯一方式

消除所有假設是不可能的。開發者需要在減少假設和程式碼複雜度之間找到平衡。即使是假設最少的程式碼,仍然可能受到瀏覽器引入的 regression 影響。

14.5 本章重點#

  • 瀏覽器雖然持續改善,但仍非無 bug,且通常不會一致地支援 web 標準
  • 選擇支援哪些瀏覽器和平台是重要的決策,品質不應為覆蓋率犧牲
  • 跨瀏覽器開發的最大挑戰:bug 修復、regressions、瀏覽器 bug、缺少的功能、外部程式碼
  • 可重用的跨瀏覽器開發需要平衡多個因素:
    • Code size — 保持檔案大小精簡
    • Performance overhead — 維持可接受的效能水準
    • API quality — 確保 API 跨瀏覽器行為一致
  • 沒有確定正確平衡的萬能公式,開發者必須根據自身情況判斷
  • 透過 feature detection 等技巧,可以在不做過度犧牲的情況下防禦可重用程式碼的各種攻擊面