2012年1月18日 星期三

Shadow DOM 到底是什麼鬼?

原文網址:http://glazkov.com/2011/01/14/what-the-heck-is-shadow-dom/

當你建構網站時,可能會用到一些 JavaScript library 。這樣的話,你也許要感謝一下將這些 library 設計的不是那麼糟糕的無名英雄們。

這些網路上的英勇戰士要面對的一個常見問題就是「封裝」。你知道,這個概念是物件導向編程的重要基礎之一,現代軟體工程大多數建立在此之上。你要如何在「你寫的程式碼」與「使用『你寫的程式碼』的程式碼」之間畫出界線?

先屏除 SVG 不提,現在的網路平台僅提供一種內建機制來將程式碼各自分開,但是這種作法並不漂亮。沒錯,就是在說 iframe。對於大多數封裝的需求來說, frame 實在過於笨重而且有很多限制。
你要我必須把每一個自定的按鈕放在不同 iframe 區塊裡是什麼意思?你是哪來的神經病?
因此,我們需要更好的東西。結果就是,大多數瀏覽器都已經悄悄採用了強大的技術來隱藏其中慘不忍睹的實作細節。這種技術稱為「Shadow DOM」。

我姓 DOM,叫做 Shadow DOM

「Shadow DOM」論及瀏覽器的一種能力,這個能力會讓 DOM element subtree 包含於 document 的呈現內容裡,但是卻不會在 main document DOM tree 裡。

仔細地來想想一個簡單的 slider:
 

把上面這段程式碼貼到 html 檔,隨便用個 WebKit-powered 的瀏覽器開啟,就會看到畫面如下:



還蠻單純的吧。一條滑動軌道(slider track)、一個可以沿軌道滑動的控制軸(thumb)。

等等,什麼?input element 裡面竟然有可以單獨移動的 element?為什麼我用 Javascript 卻找不到它呢?
var slider = document.getElementsById("foo");
console.log(slider.firstChild); // returns null
這是某種魔法嗎?

當然不是,我誠實公平的鄉民們。那是 Shadow DOM 造成的。你看,瀏覽器開發人員意識到,完全用人工編寫 HTML element 的外觀和行為是吃力不討好的。所以他們算是對你使了障眼法。

他們在網頁開發人員的可操作範圍和細節實作面之間劃了一條界線, 讓你無法接觸到細節。然而瀏覽器卻可以跨越那條界線。有了這條界線,他們就能照常使用過去喜歡的 Web 技術來建立所有 HTML 元素,擺脫一堆「div」和「span」的世界。

有些例子單純一點,像剛剛提到的 slider。有些則相當複雜。我們來檢視一下「video」元素。它有播放按鈕,時間軸,滑鼠移入才會出現的音量控制區,如下:



你看到的全都只是 HTML 和 CSS - 藏在 Shadow DOM subtree 裡面。

引用 magnetic meme duo 的其中一句歌詞,“他到底是怎麼辦到的?” 為了建立更理想的推演模型,我們先假設有辦法透過 Javascript 控制它。給定這個簡單的頁面:
<html>
<head>
<style> p { color: Green; } </style>
</head>
<body>
<p>My Future is so bright</p>
<div id="foo"></div>
<script>
    var foo = document.getElementById('foo');
    // 注意!下面這個不是真實的 API
    foo.shadow = document.createElement('p');
    foo.shadow.textContent = 'I gotta wear shades';
</script>
</body>
</html>
我們實際得到的 DOM tree 如下:
<p>My Future is so bright</p>
<div id="foo"></div>
可是它彷彿呈現這個樣子:
<p>My Future is so bright</p>
<div id="foo"> <!-- shadow subtree begins -->
    <p>I gotta wear shades</p>
</div> <!-- shadow subtree ends -->
或者看起來像這樣:



看到了嗎,是如何讓第二個句子不是綠色的?因為在這個網頁裡面,我給定的 p selector 無法指到 Shadow DOM 裡面。你說這有多酷阿?Framework 開發人員有了這種能力可以產出什麼樣的東西呢? 可以讓你寫 widget 而不用擔心隨意一個 selector 會干擾你的 style ,這種能力看起來.....簡直令人為之著迷。

傳遞 Events 的過程

自然而然的,網頁內可以偵聽到 Shadow DOM 裡被觸發的事件。例如,如果您點選 audio 元素裡的靜音按鈕,裝 audio 的 div 內會有 event listener 偵測到這個 click 事件:
<div onclick="alert('who dat?')">
    <audio controls src="test.wav"></audio>
</div>
然而,如果想要找出是誰觸發這個事件,你將會找到 audio element 本身,而不是它裡面的按鈕。
<div onclick="alert('fired by:' + event.target)">
    <audio controls src="test.wav"></audio>
</div>
為什麼?因為當事件被傳送經過 Shadow DOM 邊界時,傳送目標會被 re-targeted 以避免 Shadow subtree 內的細節曝光。這樣一來,你能捕捉到由 Shadow DOM 發出的 Event,但是實作這個功能的人仍然可以對你隱藏這些細節。

用 CSS 操作 Shadow 樣式

另外有件事情是藏有玄機的,那就是能否用 CSS 操控 Shadow DOM subtree 樣式的能力?又該怎麼作。假設我要自己定義 slider 的外觀。代替標準的 OS 特定外觀,我想讓他變得更有型,像這樣:
input[type=range].custom {
    -webkit-appearance: none;
    background-color: Red;
    width: 200px;
}
得到的結果是:



好,目前看起來還不錯,但是我接下來要怎麼裝飾控制軸(thumb)呢?目前已經確定我們一般使用的 CSS selector 是無法指到 Shadow DOM tree 的內部元素。原來,有便利的 偽屬性 能力,使得 Shadow DOM subtree 可以讓任意的偽元素辨識器和 subtree 內的元素產生關聯。例如,在 WebKit 瀏覽器上要指到控制軸(thumb)的樣式可以這麼做:
input[type=range].custom::-webkit-slider-thumb {
    -webkit-appearance: none;
    background-color: Green;
    opacity: 0.5;
    width: 10px;
    height: 40px;
}
就會看到如下圖:



是不是很棒?想想看,您可以裝飾 Shadow DOM 內的元素而不用實際的去存取它們。而且建立這個 Shadow DOM subtree 的人也能決定哪些部份可以被裝飾。當你在打造 UI Widget 工具包的時候,難道不想擁有這樣子的權力嗎?

Shadow 有 API 接口,這對瘋狂科學家們有什麼意義?

談到很棒的能力,我們可以來看看替 shadow DOM subtree 添加一個元素會發生什麼事情?下面做個實驗:
//建立一個帶有 Shadow DOM subtree 的元素。
var input = document.body.appendChild(document.createElement('input'));
//加入一個子元素。
var test = input.appendChild(document.createElement('p'));
//..寫一些文字。
test.textContent = 'Team Edward';
顯示如下:



哇嗚!歡迎來到 twilight DOM - 在畫面上看不見,但是會在 HTML DOM 出現的網頁區塊。它有什麼用處嗎?不是很確定。不過你想要就可以用。年輕人似乎喜歡這種 fu。

但是如果我們確實有辦法像 Shadow DOM subtree 的一部分那樣顯示 element 的 children 又能作什麼?把 Shadow DOM 想像成一個有 API 的樣板,透過 API 你可以看得到 element 的 children:
//注意:以下是虛擬碼,不是真的API。
var element = document.getElementById('element');
///建立一個 Shadow subtree。
element.shadow = document.createElement('div');
element.shadow.innerHTML = '

Think of the Children

' +
{{children-go-here}}
'; // Now add some children. var test = element.appendChild(document.createElement('p')); test.textContent = 'I see the light!';
因此,如果你 traverse DOM 一遍,你會看到:
<div id="element">
    <p>I see the light</p>
</div>
但是畫面上顯示的卻是:
<div id="element">
    <div> <!-- shadow tree begins -->
        <h1>Think of the Children</h1>
        <div class="children"> <!-- shadow tree hole begins -->
            <p>I see the light</p>
        </div> <!-- shadow tree hole ends -->
    </div> <!-- shadow tree ends --> 
</div>
幫 element 這個 div 增加 children 之後,如果你在 DOM 中觀察這些 element,看起來就跟平常的 element 沒兩樣;但是在呈現時,他們會被傳進 Shadow DOM subtree 的 API 接口裡。

2 則留言: