Modernizr 源碼剖析(上)

讀 Modernizr 的原始碼除了瞭解它做了什麼事之外,更可以學習到不少 JavaScript 與 DOM scripting 的技巧。


Modernizr 是一個 JavaScript 函式庫,它主要會做兩件事:

  • 偵測瀏覽器對於(廣義的) HTML5 各種規格支援的程度,並且在 標籤中加入適當的 class 來表現偵測後的結果。舉例來說,若是瀏覽器不支援在 CSS 中使用 border-radius 的語法,那 標籤中就會有一個 no-borderradius 的 class;而若是支援 CSS 漸層的語法,則會有一個 cssgradients class。
    這樣的好處是,網頁開發者可以使用 CSS 來因應各種支援的狀況,像是這段範例程式碼:

    .box {
    border-bottom: 1px solid #666;
    border-right: 1px solid #777;
    }
    .boxshadow div.box {
    border: none;
    -webkit-box-shadow: #666 1px 1px 1px;
    -moz-box-shadow: #666 1px 1px 1px;
    box-shadow: #666 1px 1px 1px;
    }

    若瀏覽器尚未支援 CSS 中的 box-shadow 語法,則 Modernizr 會在 中加入 no-boxshadow 的 class,則整個頁面並不會套用到下方的 CSS rule;反之則下方的 CSS rule 就會生效(因為 標籤中加入了 boxshadow 的 class。

  • 提供函式或屬性來測試或表示某個 HTML5 的規格是否已經在該瀏覽器下被支援。比方說我要使用 JavaScript 動態插入一個聲音檔案,想要使用 HTML5 的 標籤來做,則可以利用 Modernizr 的屬性來測試支援的程度:

    /* 測試瀏覽器是否已經支援

    由此可知使用 Modernizr 可以省下很多自行偵測瀏覽器能力的程式碼(相信專業的吧)。

雖然 Modernizr 已經幫網頁開發者做了很多事情,不過認真看一下它到底做了哪些事情也是很重要的學習,除了瞭解如何搞定瀏覽器的 feature detect 之外,也可以學習一些 JavaScript 的寫作技巧,以下就以幾個分類來剖析 Modernizr。

定義 API 介面

Modernizr 函式庫提供一個 Modernizr 物件來進行操作(如同上述的範例),而在 Modernizr 的原始碼裡,它是這樣做的:

window.Modernizr = (function( window, document, undefined ) {
var ...,
Modernizr = {},

...

return Modernizr;
})(this, this.document);

用一個 anonymous function 產生一個命名空間(namespace)是很常見的技巧,這個 function 最後傳回在裡面宣告的 Modernizr 物件(因為有使用 var 宣告,所以是 local variable),然後將它 assign 到 window.Modernizr 下,這樣在其它的 JavaScript 程式碼中就可以直接使用 window.Modernizr 或是 Modernizr (定義在 window 下的變數可以省略 window)來存取它。

另外,將 window, document 以及 undefined 當作引數(arguments)傳進去的作法也出現在很多函式庫之中,除了可以減少 scope 檢查的時間,也可以避免這些保留字事前被亂改掉了。而這個立即被執行的 function,呼叫時傳入的參數(parameters)是 this 以及 this.documentthis 就是這段程式碼被執行時的 context,在瀏覽器預設的狀況下,若是沒有被 function 包住的話,則 this 就相當於 window。至於為什麼不直接帶入 window 以及 window.document,只是為了保持彈性,也許這個函式庫會被應用於其它非瀏覽器的執行環境,那時候 global object 就不一定是 window 了。

如何偵測 CSS 樣式支援

在 Modernizr 中,一開始會建立一個 的 DOM 物件,然後就利用這個物件下的 style 屬性來測試各種 CSS 2.1/3 樣式的支援程度,初始化的程式碼如下:

mod = 'modernizr',
modElem = document.createElement(mod),
mStyle = modElem.style,

對瀏覽器而言,只要使用 DOM API 來產生一個 DOM 物件,不管瀏覽器是否認得這個標籤,都可以對它使用 CSS 來設定樣式(所以要讓傳統瀏覽器認識 HTML5 的新標籤,這就是一個解決方法),所以就可以拿它的 style 屬性來試試瀏覽器的能耐。而在做完所有的偵測之後,也要加上一段清空的程式碼避免耗費多餘的記憶體(或是 memory leak):

setCss(''); // 將 mStyle.cssText 整個清空
modElem = inputElem = null;

舉例來說,若要偵測瀏覽器是否支援在 CSS 中使用 hsl/hsla 來表示色彩,那它的檢查流程會是:

  1. 先塞一個使用 hsl/hsla 的色彩表示法到 mStyle.cssText 下,例如:mStyle.cssText = 'background-color:hsla(120,40%,100%,.5)';
  2. 再檢查 mStyle.backgroundColor 的值,是否含有 hsla 的字串,若瀏覽器不支援的話,在上述直接覆寫 cssText 的動作就會沒有作用,所以 backgroundColor 的值也不會有 hsla 的字串囉。(不過,事實上有些瀏覽器會將 hsla 的表示法轉成 rgba,所以同時也要檢查是否有 rgba 的字串)

這一段的原始碼是這樣寫的:

tests['hsla'] = function() {
// Same as rgba(), in fact, browsers re-map hsla() to rgba() internally,
// except IE9 who retains it as hsla

setCss('background-color:hsla(120,40%,100%,.5)');

return contains(mStyle.backgroundColor, 'rgba') || contains(mStyle.backgroundColor, 'hsla');
};

而如果是有瀏覽器前置詞的 CSS 樣式屬性(如:-webkit-box-shadow),則很簡單會去測試 mStyle 下是否有 boxShadowWebkitBoxShadowMozBoxShadowOBoxShadowmsBoxShadow 或是 KhtmlBoxShadow 屬性,值得注意的是,在 style 屬性下的 DOM 前置詞,只有 IE 的前置詞全是小寫字串。

這段程式碼拆成了幾個 functions 來做,它的流程大致是這樣的:

testPropsAll('boxShadow');

// 切到 testPropsAll 的函式定義
// domPrefixes = 'Webkit Moz O ms Khtml'.split(' ')
function testPropsAll( prop, prefixed ) {

var ucProp = prop.charAt(0).toUpperCase() + prop.substr(1),
props = (prop + ' ' + domPrefixes.join(ucProp + ' ') + ucProp).split(' ');

return testProps(props, prefixed);
}

// 再到 testProps 的函式定義
function testProps( props, prefixed ) {
for ( var i in props ) {
if ( mStyle[ props[i] ] !== undefined ) {
return prefixed == 'pfx' ? props[i] : true;
}
}
return false;
}

要檢查一個 CSS 樣式屬性時,直接呼叫 testPropsAll 函式來取得瀏覽器是否支援(最後會得到 true 或 false),而 testPropsAll 函式中會將前置詞拼湊之後,以陣列的形式傳遞至 testProps 函式來檢查,所以在 testProps 函式中就直接以一個 for-each-loop 看看這些屬性(如:WebkitBoxShadow)是否在 mStyle 中有被定義(以 !== undefined 做檢查,以免遇到 falsy value 的問題)。

prefixed 引數則是為了 Modernizr.prefiexed 這個 API 所準備的,這部份稍候再做介紹。

偵測 HTML5 標籤能力

HTML5 新增了不少標籤的定義,但是要如何「確切」地知道瀏覽器是不是支援某個標籤呢?以 為例,如果直接使用 document.createElement('canvas'); 來測試,瀏覽器並不會反應出是否有真正支援 canvas 的繪圖能力,所以必須針對標籤的特性再做一些測試,像是:

var elem = document.createElement('canvas');
return !!(elem.getContext && elem.getContext('2d'));

就可以測試建立出來的 物件是否真的有 getContext 方法可以操作。比較特殊的測試還有:

  • 的支援測試,Modernizr 作了兩件事:
    1. 檢查是不是有 canPlayType 方法
    2. 檢查編碼器的支援能力(ogg, h264, webm)

    完整的程式碼如下:

    tests['video'] = function() {
    var elem = document.createElement('video'),
    bool = false;

    // IE9 Running on Windows Server SKU can cause an exception to be thrown, bug #224
    try {
    if ( bool = !!elem.canPlayType ) {
    bool = new Boolean(bool);
    bool.ogg = elem.canPlayType('video/ogg; codecs="theora"');

    // Workaround required for IE9, which doesn't report video support without audio codec specified.
    // bug 599718 @ msft connect
    var h264 = 'video/mp4; codecs="avc1.42E01E';
    bool.h264 = elem.canPlayType(h264 + '"') || elem.canPlayType(h264 + ', mp4a.40.2"');

    bool.webm = elem.canPlayType('video/webm; codecs="vp8, vorbis"');
    }

    } catch(e) { }

    return bool;
    };

    值得注意的是,這個測試的回傳值是一個 Boolean 物件,為什麼不直接使用 truefalse 這樣的 literal 呢?因為它可以借用這個物件來放入 oggh264 以及 webm 的屬性,這樣單純在使用 Modernizr.video 作為是否支援 標籤的真值判斷,同時又可以再使用 Modernizr.video.h264 來判斷是否支援 h.264 編碼過的影片。

    而類似的 也有類似的作法。

  • SVG 的支援,程式碼很簡單:

    // ns = {'svg': 'http://www.w3.org/2000/svg'}
    return !!document.createElementNS && !!document.createElementNS(ns.svg, 'svg').createSVGRect;

    除了用 createElementNS 來產生 標籤之外,也檢查產生出來的物件是否有 createSVGRect 的方法。

    而瀏覽器是否支援 inline SVG 的部份也有點 tricky,簡單地說就是直接塞一個空的 到文件中,然後看看瀏覽器會不會將它展開成標準格式的 SVG:

    var div = document.createElement('div');
    div.innerHTML = '';
    return (div.firstChild && div.firstChild.namespaceURI) == ns.svg;

(下集將介紹 Modernizr 所提供的 API,以及它程式碼寫作的技巧)