Web Components是個(gè)什么樣的東西
前端組件化這個(gè)主題相關(guān)的內(nèi)容已經(jīng)火了很久很久,angular 剛出來(lái)時(shí)的 Directive 到 angular2 的 components,還有 React 的components 等等,無(wú)一不是前端組件化的一種實(shí)現(xiàn)和探索,但是提上議程的 Web Components 標(biāo)準(zhǔn)是個(gè)怎樣的東西,相關(guān)的一些框架或者類(lèi)庫(kù),如 React,Angular2,甚至是 x-tag,polymer 現(xiàn)在實(shí)現(xiàn)的組件化的東西和 Web Components 標(biāo)準(zhǔn)差別在哪里?我花時(shí)間努力地把現(xiàn)有的 W3C Web Components 文檔看了下,然后堅(jiān)強(qiáng)地寫(xiě)下這些記錄。
首先我們需要知道,Web Components 包括了四個(gè)部分:
- Custom Elements
- HTML Imports
- HTML Templates
- Shadow DOM
這四部分有機(jī)地組合在一起,才是 Web Components。
可以用自定義的標(biāo)簽來(lái)引入組件是前端組件化的基礎(chǔ),在頁(yè)面引用 HTML 文件和 HTML 模板是用于支撐編寫(xiě)組件視圖和組件資源管理,而 Shadow DOM 則是隔離組件間代碼的沖突和影響。
下邊分別是每一部分的筆記內(nèi)容。
Custom Elements
概述
Custom Elements 顧名思義,是提供一種方式讓開(kāi)發(fā)者可以自定義 HTML 元素,包括特定的組成,樣式和行為。支持 Web Components 標(biāo)準(zhǔn)的瀏覽器會(huì)提供一系列 API 給開(kāi)發(fā)者用于創(chuàng)建自定義的元素,或者擴(kuò)展現(xiàn)有元素。
這一項(xiàng)標(biāo)準(zhǔn)的草案還處于不穩(wěn)定的狀態(tài),時(shí)有更新,API 還會(huì)有所變化,下邊的筆記以 Cutsom Elements 2016.02.26 這個(gè)版本為準(zhǔn),因?yàn)樵谧钚碌?chrome 瀏覽器已經(jīng)是可以工作的了,這樣可以使用 demo 來(lái)做嘗試,最后我會(huì)再簡(jiǎn)單寫(xiě)一下最新文檔和這個(gè)的區(qū)別。
registerElement
首先,我們可以嘗試在 chrome 控制臺(tái)輸入 HTMLInputElement,可以看到是有這么一個(gè)東西的,這個(gè)理解為 input DOM 元素實(shí)例化時(shí)的構(gòu)造函數(shù),基礎(chǔ)的是 HTMLElement。
Web Components 標(biāo)準(zhǔn)提出提供這么一個(gè)接口:
- document.registerElement('x-foo', {
- prototype: Object.create(HTMLElement.prototype, {
- createdCallback: {
- value: function() { ... }
- },
- ...
- })
- })
你可以使用 document.registerElement 來(lái)注冊(cè)一個(gè)標(biāo)簽,標(biāo)準(zhǔn)中為了提供 namesapce 的支持,防止沖突,規(guī)定標(biāo)簽類(lèi)型(也可以理解為名字)需要使用 - 連接。同時(shí),不能是以下這一些:
- annotation-xml
- color-profile
- font-face
- font-face-src
- font-face-uri
- font-face-format
- font-face-name
- missing-glyph
第二個(gè)參數(shù)是標(biāo)簽相關(guān)的配置,主要是提供一個(gè) prototype,這個(gè)原型對(duì)象是以 HTMLElement 等的原型為基礎(chǔ)創(chuàng)建的對(duì)象。然后你便可以在 HTML 中去使用自定義的標(biāo)簽。如:
- <div>
- <x-foo></x-foo>
- </div>
是不是嗅到了 React 的味道?好吧,React 說(shuō)它自己主要不是做這個(gè)事情的。
生命周期和回調(diào)
在這個(gè) API 的基礎(chǔ)上,Web Components 標(biāo)準(zhǔn)提供了一系列控制自定義元素的方法。我們來(lái)一一看下:
一個(gè)自定義元素會(huì)經(jīng)歷以下這些生命周期:
- 注冊(cè)前創(chuàng)建
- 注冊(cè)自定義元素定義
- 在注冊(cè)后創(chuàng)建元素實(shí)例
- 元素插入到 document 中
- 元素從 document 中移除
- 元素的屬性變化時(shí)
這個(gè)是很重要的內(nèi)容,開(kāi)發(fā)者可以在注冊(cè)新的自定義元素時(shí)指定對(duì)應(yīng)的生命周期回調(diào)來(lái)為自定義元素添加各種自定義的行為,這些生命周期回調(diào)包括了:
createdCallback
自定義元素注冊(cè)后,在實(shí)例化之后會(huì)調(diào)用,通常多用于做元素的初始化,如插入子元素,綁定事件等。
- attachedCallback
元素插入到 document 時(shí)觸發(fā)。 - detachedCallback 元素從 document 中移除時(shí)觸發(fā),可能會(huì)用于做類(lèi)似 destroy 之類(lèi)的事情。
- attributeChangedCallback
元素屬性變化時(shí)觸發(fā),可以用于從外到內(nèi)的通信。外部通過(guò)修改元素的屬性來(lái)讓內(nèi)部獲取相關(guān)的數(shù)據(jù)并且執(zhí)行對(duì)應(yīng)的操作。
這個(gè)回調(diào)在不同情況下有對(duì)應(yīng)不同的參數(shù):
- 設(shè)置屬性時(shí),參數(shù)列表是:屬性名稱(chēng),null,值,命名空間
- 修改屬性時(shí),參數(shù)列表是:屬性名稱(chēng),舊值,新值,命名空間
- 刪除屬性時(shí),參數(shù)列表是:屬性名稱(chēng),舊值,null,命名空間
好了,就上邊了解到的基礎(chǔ)上,假設(shè)我們要?jiǎng)?chuàng)建一個(gè)自定義的 button-hello 按鈕,點(diǎn)擊時(shí)會(huì) alert('hello world'),代碼如下:
- document.registerElement('button-hello', {
- prototype: Object.create(HTMLButtonElement.prototype, {
- createdCallback: {
- value: function createdCallback() {
- this.innerHTML = '<button>hello world</button>'
- this.addEventListener('click', () => {
- alert('hello world')
- })
- }
- }
- })
- })
要留意上述代碼執(zhí)行之后才能使用 <button-hello></button-hello>
擴(kuò)展原有元素
其實(shí),如果我們需要一個(gè)按鈕,完全不需要重新自定義一個(gè)元素,Web Components 標(biāo)準(zhǔn)提供了一種擴(kuò)展現(xiàn)有標(biāo)簽的方式,把上邊的代碼調(diào)整一下:
- document.registerElement('button-hello', {
- prototype: Object.create(HTMLButtonElement.prototype, {
- createdCallback: {
- value: function createdCallback() {
- this.addEventListener('click', () => {
- alert('hello world')
- })
- }
- }
- }),
- extends: 'button'
- })
然后在 HTML 中要這么使用:
- <button is="button-hello">hello world</button>
使用 is 屬性來(lái)聲明一個(gè)擴(kuò)展的類(lèi)型,看起來(lái)也蠻酷的。生命周期和自定義元素標(biāo)簽的保持一致。
當(dāng)我們需要多個(gè)標(biāo)簽組合成新的元素時(shí),我們可以使用自定義的元素標(biāo)簽,但是如果只是需要在原有的 HTML 標(biāo)簽上進(jìn)行擴(kuò)展的話,使用 is 的這種元素?cái)U(kuò)展的方式就好。
原有的 createElement 和 createElementNS,在 Web Components 標(biāo)準(zhǔn)中也擴(kuò)展成為支持元素?cái)U(kuò)展,例如要?jiǎng)?chuàng)建一個(gè) button-hello:
- const hello = document.createElement('button', 'button-hello')
標(biāo)準(zhǔn)文檔中還有很多細(xì)節(jié)上的內(nèi)容,例如接口的參數(shù)說(shuō)明和要求,回調(diào)隊(duì)列的實(shí)現(xiàn)要求等,這些更多是對(duì)于實(shí)現(xiàn)這個(gè)標(biāo)準(zhǔn)的瀏覽器開(kāi)發(fā)者的要求,這里不做詳細(xì)描述了,內(nèi)容很多,有興趣的自行查閱:Cutsom Elements 2016.02.26。
和最新版的區(qū)別
前邊我提到說(shuō)文檔的更新變化很快,截止至我寫(xiě)這個(gè)文章的時(shí)候,最新的文檔是這個(gè):Custom Elements 2016.07.21。
細(xì)節(jié)不做描述了,講講我看到的最大變化,就是向 ES6 靠攏。大致有下邊三點(diǎn):
- 從原本的擴(kuò)展 prototype 來(lái)定義元素調(diào)整為建議使用 class extends 的方式
- 注冊(cè)自定義元素接口調(diào)整,更加方便使用,傳入 type 和 class 即可
- 生命周期回調(diào)調(diào)整,createdCallback 直接用 class 的 constructor
前兩個(gè)點(diǎn),我們直接看下代碼,原本的代碼按照新的標(biāo)準(zhǔn),應(yīng)該調(diào)整為:
- class ButtonHelloElement extends HTMLButtonElement {
- constructor() {
- super()
- this.addEventListener('click', () => {
- alert('hello world')
- })
- }
- }
- customElements.define('button-hello', ButtonHelloElement, { extends: 'button' })
從代碼上看會(huì)感覺(jué)更加 OO,編寫(xiě)上也比原本要顯得方便一些,原本的生命周期回調(diào)是調(diào)整為新的:
- constructor in class 作用相當(dāng)于原本的 createdCallback
- connectedCallback 作用相當(dāng)于 attachedCallback
- disconnectedCallback 作用相當(dāng)于 detachedCallback
- adoptedCallback 使用 document.adoptNode(node) 時(shí)觸發(fā)
- attributeChangedCallback 和原本保持一致
connect 事件和插入元素到 document 有些許區(qū)別,主要就是插入元素到 document 時(shí),元素狀態(tài)會(huì)變成 connected,這時(shí)會(huì)觸發(fā) connectedCallback,disconnect 亦是如此。
HTML Imports
概述
HTML Imports 是一種在 HTMLs 中引用以及復(fù)用其他的 HTML 文檔的方式。這個(gè) Import 很漂亮,可以簡(jiǎn)單理解為我們常見(jiàn)的模板中的include 之類(lèi)的作用。
我們最常見(jiàn)的引入一個(gè) css 文件的方式是:
- <link rel="stylesheet" href="/css/master.css">
Web Components 現(xiàn)在提供多了一個(gè)這個(gè):
- <link rel="import" href="/components/header.html">
HTMLLinkElement
原本的 link 標(biāo)簽在添加了 HTML Import 之后,多了一個(gè)只讀的 import 屬性,當(dāng)出現(xiàn)下邊兩種情況時(shí),這個(gè)屬性為 null:
- 該 link 不是用來(lái) import 一個(gè) HTML 的。
- 該 link 元素不在 document 中。
否則,這個(gè)屬性會(huì)返回一個(gè)表示引入的 HTML 文件的文檔對(duì)象,類(lèi)似于 document。比如說(shuō),在上邊的代碼基礎(chǔ)上,可以這樣做:
- const link = document.querySelector('link[rel=import]')
- const header = link.import;
- const pulse = header.querySelector('div.logo');
阻塞式
我們要知道的是,默認(rèn)的 link 加載是阻塞式的,除非你給他添加一個(gè) async 標(biāo)識(shí)。
阻塞式從某種程度上講是有必要的,當(dāng)你 improt 的是一個(gè)完整的自定義組件并且需要在主 HTML 中用標(biāo)簽直接使用時(shí),非阻塞的就會(huì)出現(xiàn)錯(cuò)誤了,因?yàn)闃?biāo)簽還沒(méi)有被注冊(cè)。
document
有一點(diǎn)值得留意的是,在 import 的 HTML 中,我們編寫(xiě)的 script 里邊的 document 是指向 import 這個(gè) HTML 的主 HTML 的 document。
如果我們要獲取 import 的 HTML 的 document 的話,得這么來(lái):
- const d = document.currentScript.ownerDocument
這樣設(shè)計(jì)是因?yàn)?import 進(jìn)來(lái)的 HTML 需要用到主 HTML 的 document。例如我們上邊提到的 registerElement。
在一個(gè)被 import 的 HTML 文件中使用下邊三個(gè)方法會(huì)拋出一個(gè) InvalidStateError 異常:
- document.open()
- document.write()
- document.close()
對(duì)于 HTML Import,標(biāo)準(zhǔn)文檔中還有很大一部分內(nèi)容是關(guān)于多個(gè)依賴(lài)加載的處理算法的,在這里就不詳述了,有機(jī)會(huì)的話找時(shí)間再開(kāi)篇談,這些內(nèi)容是需要瀏覽器去實(shí)現(xiàn)的。
HTML Templates
概述
這個(gè)東西很簡(jiǎn)單,用過(guò) handlebars 的人都知道有這么一個(gè)東西:
- <script id="template" type="text/x-handlebars-template">
- ...
- </script>
其他模板引擎也有類(lèi)似的東西,那么 HTML Templates 便是把這個(gè)東西官方標(biāo)準(zhǔn)化,提供了一個(gè) template 標(biāo)簽來(lái)存放以后需要但是暫時(shí)不渲染的 HTML 代碼。
以后可以這么寫(xiě)了:
- <template id="template">
- ...
- </template>
接口和應(yīng)用
template 元素有一個(gè)只讀的屬性 content,用于返回這個(gè) template 里邊的內(nèi)容,返回的結(jié)果是一個(gè) DocumentFragment。
具體是如何使用的,直接參考官方給出的例子:
- <!doctype html>
- <html lang="en">
- <head>
- <title>Homework</title>
- <body>
- <template id="template"><p>Smile!</p></template>
- <script>
- let num = 3;
- const fragment = document.getElementById('template').content.cloneNode(true);
- while (num-- > 1) {
- fragment.firstChild.before(fragment.firstChild.cloneNode(true));
- fragment.firstChild.textContent += fragment.lastChild.textContent;
- }
- document.body.appendChild(fragment);
- </script>
- </html>
使用 DocumentFragment 的 clone 方法以 template 里的代碼為基礎(chǔ)創(chuàng)建一個(gè)元素節(jié)點(diǎn),然后你便可以操作這個(gè)元素節(jié)點(diǎn),最后在需要的時(shí)候插入到 document 中特定位置便可以了。
Template 相關(guān)的東西不多,而且它現(xiàn)在已經(jīng)是納入生效的 標(biāo)準(zhǔn)文檔 中了。
我們接下來(lái)看看重磅的 Shadow DOM。
Shadow DOM
概述
Shadow DOM 好像提出好久了,最本質(zhì)的需求是需要一個(gè)隔離組件代碼作用域的東西,例如我組件代碼的 CSS 不能影響其他組件之類(lèi)的,而 iframe 又太重并且可能有各種奇怪問(wèn)題。
可以這么說(shuō),Shadow DOM 旨在提供一種更好地組織頁(yè)面元素的方式,來(lái)為日趨復(fù)雜的頁(yè)面應(yīng)用提供強(qiáng)大支持,避免代碼間的相互影響。
看下在 chrome 它會(huì)是咋樣的:
我們可以通過(guò) createShadowRoot() 來(lái)給一個(gè)元素節(jié)點(diǎn)創(chuàng)建 Shadow Root,這些元素類(lèi)型必須是下邊列表的其中一個(gè),否則會(huì)拋出 NotSupportedError 異常。
- 自定義的元素
- article
- aside
- blockquote
- body
- div
- header, footer
- h1, h2, h3, h4, h5, h6
- nav
- p
- section
- span
createShadowRoot() 是現(xiàn)在 chrome 實(shí)現(xiàn)的 API,來(lái)自文檔:https://www.w3.org/TR/2014/WD...。最新的文檔 API 已經(jīng)調(diào)整為 attachShadow()。
返回的 Shadow Root 對(duì)象從 DocumentFragment 繼承而來(lái),所以可以使用相關(guān)的一些方法,例如shadowRoot.getElementById('id') 來(lái)獲取 Shadow DOM 里邊的元素。
簡(jiǎn)單的使用如下:
- const div = document.getElementById('id')
- const shadowRoot = div.createShadowRoot()
- const span = document.createElement('span')
- span.textContent = 'hello world'
- shadowRoot.appendChild(span)
在這里,我把這個(gè) div 成為是這個(gè) Shadow DOM 的 宿主元素,下邊的內(nèi)容會(huì)延續(xù)使用這個(gè)稱(chēng)呼。
Shadow DOM 本身就為了代碼隔離而生,所以在 document 上使用 query 時(shí),是沒(méi)法獲取到 Shadow DOM 里邊的元素的,需要在 Shadow Root 上做 query 才行。
在這里附上一個(gè)文檔,里邊有詳細(xì)的關(guān)于新的標(biāo)準(zhǔn)和現(xiàn)在 blink 引擎實(shí)現(xiàn)的 Shadow DOM 的區(qū)別,官方上稱(chēng)之為 v0 和 v1:Shadow DOM v1 in Blink。
API
Shadow Root 除了從 DocumentFragment 繼承而來(lái)的屬性和方法外,還多了另外兩個(gè)屬性:
- host 只讀屬性,用來(lái)獲取這個(gè) Shadow Root 所屬的元素
- innerHTML 用來(lái)獲取或者設(shè)置里邊的 HTML 字符串,和我們常用的 element.innerHTML 是一樣的
另外,在最新的標(biāo)準(zhǔn)文檔中,元素除了上邊提到的 attachShadow 方法之外,還多了三個(gè)屬性:
- assignedSlot 只讀,這個(gè)元素如果被分配到了某個(gè) Shadow DOM 里邊的 slot,那么會(huì)返回這個(gè)對(duì)應(yīng)的 slot 元素
- slot 元素的 slot 屬性,用來(lái)指定 slot 的名稱(chēng)
- shadowRoot 只讀,元素下面對(duì)應(yīng)的 Shadow Root 對(duì)象
slot 是什么?接著看下邊的內(nèi)容,看完下一節(jié)的最后一部分就會(huì)明白上述內(nèi)容和 slot 相關(guān)的兩個(gè) API 有什么作用。
slot
slot 提供了在使用自定義標(biāo)簽的時(shí)候可以傳遞子模板給到內(nèi)部使用的能力,可以簡(jiǎn)單看下 Vue 的一個(gè)例子。
我們先來(lái)看下現(xiàn)在 chrome 可以跑的 v0 版本,這一個(gè)版本是提供了一個(gè) content 標(biāo)簽,代表了一個(gè)占位符,并且有一個(gè) select 屬性用來(lái)指定使用哪些子元素。
- <!-- component input-toggle template -->
- <input type="checkbox"></input>
- <content select=".span"></content>
自定義的元素里邊的子元素代碼是這樣的:
- <input-toggle name="hello">
- <span>hello</span>
- <span class="span">test</span>
- </input-toggle>
那么展現(xiàn)的結(jié)果會(huì)和下邊的代碼是一樣的:
- <input-toggle name="hello">
- <input type="checkbox"></input>
- <span class="span">test</span>
- </input-toggle>
這里只是說(shuō)展現(xiàn)結(jié)果,實(shí)際上,input-toggle 里邊應(yīng)該是創(chuàng)建了一個(gè) Shadow DOM,然后 content 標(biāo)簽引用了目標(biāo)的 span 內(nèi)容,在 chrome 看是這樣的:
然后,是最新標(biāo)準(zhǔn)中的 slot 使用方式,直接上例子代碼:
- <!-- component input-toggle template -->
- <input type="checkbox"></input>
- <slot name="text"></slot>
在自定義的元素標(biāo)簽是這么使用 slot 的:
- <input-toggle name="hello">
- <input type="checkbox"></input>
- <span class="span" slot="text">test</span>
- </input-toggle>
通過(guò) slot="text" 的屬性來(lái)讓元素內(nèi)部的 slot 占位符可以引用到這個(gè)元素,多個(gè)元素使用這個(gè)屬性也是可以的。這樣子我們便擁有了使用標(biāo)簽是從外部傳 template 給到自定義元素的內(nèi)部去使用,而且具備指定放在那里的能力。
CSS 相關(guān)
因?yàn)橛?Shadow DOM 的存在,所以在 CSS 上又添加了很多相關(guān)的東西,其中一部分還是屬于討論中的草案,命名之類(lèi)的可能會(huì)有變更,下邊提及的內(nèi)容主要來(lái)自文檔:Shadow DOM in CSS scoping 1,很多部分在 chrome 是已經(jīng)實(shí)現(xiàn)的了,有興趣可以寫(xiě) demo 試試。
因?yàn)?Shadow DOM 很大程度上是為了隔離樣式作用域而誕生的,主文檔中的樣式規(guī)則不對(duì) Shadow DOM 里的子文檔生效,子文檔中的樣式規(guī)則也不影響外部文檔。
但不可避免的,在某些場(chǎng)景下,我們需要外部可以控制 Shadow DOM 中樣式,如提供一個(gè)組件給你,有時(shí)候你會(huì)希望可以自定義它內(nèi)部的一些樣式,同時(shí),Shadow DOM 中的代碼有時(shí)候可能需要能夠控制其所屬元素的樣式,甚至,組件內(nèi)部可以定義上邊提到的通過(guò) slot 傳遞進(jìn)來(lái)的 HTML 的樣式。所以呢,是的,CSS 選擇器中添加了幾個(gè)偽類(lèi),我們一一來(lái)看下它們有什么作用。
在閱讀下邊描述的時(shí)候,請(qǐng)留意一下選擇器的代碼是在什么位置的,Shadow DOM 內(nèi)部還是外部。
:host 用于在 Shadow DOM 內(nèi)部選擇到其宿主元素,當(dāng)它不是在 Shadow DOM 中使用時(shí),便匹配不到任意元素。
在 Shadow DOM 中的 * 選擇器是無(wú)法選擇到其宿主元素的。
:host( <selector> ) 括號(hào)中是一個(gè)選擇器,這個(gè)可以理解為是一個(gè)用于兼容在主文檔和 Shadow DOM 中使用的方法,當(dāng)這個(gè)選擇器在 Shadow DOM 中時(shí),會(huì)匹配到括號(hào)中選擇器對(duì)應(yīng)的宿主元素,如果不是,則匹配括號(hào)中選擇器能夠匹配到的元素。
文檔中提供了一個(gè)例子:
- <x-foo class="foo">
- <"shadow tree">
- <div class="foo">...</div>
- </>
- </x-foo>
在這個(gè) shadow tree 內(nèi)部的樣式代碼中,會(huì)有這樣的結(jié)果:
- :host 匹配 <x-foo> 元素
- x-foo 匹配不到元素
- .foo 只匹配到 <div> 元素
- .foo:host 匹配不到元素
- :host(.foo) 匹配 <x-foo> 元素
:host-context( <selector> ),用于在 Shadow DOM 中來(lái)檢測(cè)宿主元素的父級(jí)元素,如果宿主元素或者其祖先元素能夠被括號(hào)中的選擇器匹配到的話,那么這個(gè)偽類(lèi)選擇器便匹配到這個(gè) Shadow DOM 的宿主元素。個(gè)人理解是用于在宿主元素外部元素滿(mǎn)足一定的條件時(shí)添加樣式。
::shadow 這個(gè)偽類(lèi)用于在 Shadow DOM 外部匹配其內(nèi)部的元素,而 /deep/ 這個(gè)標(biāo)識(shí)也有同樣的作用,我們來(lái)看一個(gè)例子:
- <x-foo>
- <"shadow tree">
- <div>
- <span id="not-top">...</span>
- </div>
- <span id="top">...</span>
- </>
- </x-foo>
對(duì)于上述這一段代碼的 HTML 結(jié)構(gòu),在 Shadow DOM 外部的樣式代碼中,會(huì)是這樣的:
- x-foo::shadow > span 可以匹配到 #top 元素
- #top 匹配不到元素
- x-foo /deep/ span 可以匹配到 #not-top 和 #top 元素
/deep/ 這個(gè)標(biāo)識(shí)的作用和我們的 > 選擇器有點(diǎn)類(lèi)似,只不過(guò)它是匹配其對(duì)應(yīng)的 Shadow DOM 內(nèi)部的,這個(gè)標(biāo)識(shí)可能還會(huì)變化,例如改成 >> 或者 >>> 之類(lèi)的,個(gè)人感覺(jué), >> 會(huì)更舒服。
最后一個(gè),用于在 Shadow DOM 內(nèi)部調(diào)整 slot 的樣式,在我查閱的這個(gè)文檔中,暫時(shí)是以 chrome 實(shí)現(xiàn)的為準(zhǔn),使用 ::content 偽類(lèi),不排除有更新為 ::slot 的可能性。我們看一個(gè)例子來(lái)了解一下,就算名稱(chēng)調(diào)整了也是差不多的用法:
- <x-foo>
- <div id="one" class="foo">...</div>
- <div id="two">...</div>
- <div id="three" class="foo">
- <div id="four">...</div>
- </div>
- <"shadow tree">
- <div id="five">...</div>
- <div id="six">...</div>
- <content select=".foo"></content>
- </"shadow tree">
- </x-foo>
在 Shadow DOM 內(nèi)部的樣式代碼中,::content div 可以匹配到 #one,#three 和 #four,留意一下 #two 為什么沒(méi)被匹配到,因?yàn)樗鼪](méi)有被 content 元素選中,即不會(huì)進(jìn)行引用。如果更換成 slot 的 name 引用的方式亦是同理。
層疊規(guī)則,按照這個(gè)文檔的說(shuō)法,對(duì)于兩個(gè)優(yōu)先級(jí)別一樣的 CSS 聲明,沒(méi)有帶 !important 的,在 Shadow DOM 外部聲明的優(yōu)先級(jí)高于在 Shadow DOM 內(nèi)部的,而帶有 !important 的,則相反。個(gè)人認(rèn)為,這是提供給外部一定的控制能力,同時(shí)讓內(nèi)部可以限制一定的影響范圍。
繼承方面相對(duì)簡(jiǎn)單,在 Shadow DOM 內(nèi)部的頂級(jí)元素樣式從宿主元素繼承而來(lái)。
至此,Web Components 四個(gè)部分介紹結(jié)束了,其中有一些細(xì)節(jié),瀏覽器實(shí)現(xiàn)細(xì)節(jié),還有使用上的部分細(xì)節(jié),是沒(méi)有提及的,因?yàn)樵敿?xì)記錄的話,還會(huì)有很多東西,內(nèi)容很多。當(dāng)使用過(guò)程中有疑問(wèn)時(shí)可以再次查閱標(biāo)準(zhǔn)文檔,有機(jī)會(huì)的話會(huì)再完善這個(gè)文章。下一部分會(huì)把這四個(gè)內(nèi)容組合起來(lái),整體看下 Web Components 是怎么使用的。
Web Components
Web Components 總的來(lái)說(shuō)是提供一整套完善的封裝機(jī)制來(lái)把 Web 組件化這個(gè)東西標(biāo)準(zhǔn)化,每個(gè)框架實(shí)現(xiàn)的組件都統(tǒng)一標(biāo)準(zhǔn)地進(jìn)行輸入輸出,這樣可以更好推動(dòng)組件的復(fù)用。結(jié)合上邊各個(gè)部分的內(nèi)容,我們整合一起來(lái)看下應(yīng)該怎么使用這個(gè)標(biāo)準(zhǔn)來(lái)實(shí)現(xiàn)我們的組件:
- <!-- components/header.html -->
- <template id="">
- <style>
- ::content li {
- display: inline-block;
- padding: 20px 10px;
- }
- </style>
- <content select="ul"></content>
- </template>
- <script>
- (function() {
- const element = Object.create(HTMLInputElement.prototype)
- const template = document.currentScript.ownerDocument.querySelector('template')
- element.createdCallback = function() {
- const shadowRoot = this.createShadowRoot()
- const clone = document.importNode(template.content, true)
- shadowRoot.appendChild(clone)
- this.addEventListener('click', function(event) {
- console.log(event.target.textContent)
- })
- }
- document.registerElement('test-header', { prototype: element })
- })()
- </script>
這是一個(gè)簡(jiǎn)單的組件的例子,用于定義一個(gè) test-header,并且給傳遞進(jìn)來(lái)的子元素 li 添加了一些組件內(nèi)部的樣式,同時(shí)給組件綁定了一個(gè)點(diǎn)擊事件,來(lái)打印點(diǎn)擊目標(biāo)的文本內(nèi)容。
看下如何在一個(gè) HTML 文件中引入并且使用一個(gè)組件:
- <!-- index.html -->
- <!DOCTYPE html>
- <html>
- <head>
- <meta charset="utf-8">
- <title></title>
- <link rel="import" href="components/header.html">
- </head>
- <body>
- <test-header>
- <ul>
- <li>Home</li>
- <li>About</li>
- </ul>
- </test-header>
- </body>
- </html>
一個(gè) import 的 <link> 把組件的 HTML 文件引用進(jìn)來(lái),這樣會(huì)執(zhí)行組件中的腳本,來(lái)注冊(cè)一個(gè) test-header 元素,這樣子我們便可以在主文檔中使用這個(gè)元素的標(biāo)簽。
上邊的例子是可以在 chrome 正常運(yùn)行的。
所以,根據(jù)上邊簡(jiǎn)單的例子可以看出,各個(gè)部分的內(nèi)容是有機(jī)結(jié)合在一起,Custom Elements 提供了自定義元素和標(biāo)簽的能力,template 提供組件模板,import 提供了在 HTML 中合理引入組件的方式,而 Shadow DOM 則處理組件間代碼隔離的問(wèn)題。
不得不承認(rèn),Web Components 標(biāo)準(zhǔn)的提出解決了一些問(wèn)題,必須交由瀏覽器去處理的是 Shadow DOM,在沒(méi)有 Shadow DOM 的瀏覽器上實(shí)現(xiàn)代碼隔離的方式多多少少有缺陷。個(gè)人我覺(jué)得組件化的各個(gè) API 不夠簡(jiǎn)潔易用,依舊有 getElementById 這些的味道,但是交由各個(gè)類(lèi)庫(kù)去簡(jiǎn)化也可以接受,而 import 功能上沒(méi)問(wèn)題,但是加載多個(gè)組件時(shí)性能問(wèn)題還是值得商榷,標(biāo)準(zhǔn)可能需要在這個(gè)方面提供更多給瀏覽器的指引,例如是否有可能提供一種單一請(qǐng)求加載多個(gè)組件 HTML 的方式等。
在現(xiàn)在的移動(dòng)化趨勢(shì)中,Web Components 不僅僅是 Web 端的問(wèn)題,越來(lái)越多的開(kāi)發(fā)者期望以 Web 的方式去實(shí)現(xiàn)移動(dòng)應(yīng)用,而多端復(fù)用的實(shí)現(xiàn)漸漸是以組件的形式鋪開(kāi),例如 React Native 和 Weex。所以 Web Components 的標(biāo)準(zhǔn)可能會(huì)影響到多端開(kāi)發(fā) Web 化的一個(gè)模式和發(fā)展。
最后,再啰嗦一句,Web Components 個(gè)人覺(jué)得還是未來(lái)發(fā)展趨勢(shì),所以才有了這個(gè)文章。