原文地址:hacks.mozilla.org/2018/11/the…
原文作者:Potch
2018年11月15日發表於 Developer Tools, DOM, Featured Article, 以及 Web Components
譯者水平有限,如果有錯誤歡迎指正!
背景
自從第一個動態的 DHTML 游標拖拽的誕生,以及“本週網站”的徽章為網站增色,可複用程式碼對 web 開發者極具誘惑力。但是在自己的網站中引入三方 UI 元件一直是一個比較頭疼的事情。
引入別人造好的輪子會帶來很多 javascript 和 css衝突,想想那些可怕的 !important 吧。使用現代前端框架比如React可能會好一些,但是為了為了重用一些元件而引入一個框架顯然是有些笨重。 HTML5 把一些常用的元件引入 web 標準,像 <video>
和 <input type="date" />
,但是,為每個常用Web UI庫新增新標準標籤並不是一個可持續維護的方式。
這時,一些 web 標準草案就應運而生了。每個標準有其獨立的功能,但是把他們組合在一起,就能解決之前不能用原生方案解決的問題,並且它們非常難偽造,因為自定義 HTML 元件可以像傳統 HTML 標籤一樣使用。這些元件把複雜的實現封裝在內部,就像富文字編輯器和視訊播放器一樣。
標準發展
整體來說,這組標準就是 Web Components。在2018年前端元件化並不是什麼新鮮事物。的確,從2014年開始,chrome一直以這樣或那樣的方式實現這些標準,其他瀏覽器也有相應的polyfills。
在標準委員會工作了一段時間之後,Web Components 標準從早期的形式(現稱為version 0版本)演變成了更成熟的version 1版本,並且被主流瀏覽器實現。Firefox63 對此增加了兩個支援:Custom Elements、shadow DOM。現在一起來看看怎麼扮演 HTML 的發明家吧!
Web Components已經存在了一段時間,相關資源比較多。本文只作為初級讀物,介紹一些列的新效能和資源,如果你想了解更多(你也應該瞭解更多),請移步 MDN Web Docs 和 Google Developers。
自定義 HTML 標籤需要瀏覽器以前沒賦予開發者的新功能。我將在每一單元列出這些從前不能實現的地方,以及他們所使用的其他 web 新技術。
<template>
標籤: 一個小複習
第一個標籤是老朋友了,它滿足的需求早於 Web Components。有時你只想儲存一些 HTML。也許有時你要多次複製標籤,也許有時你還不想馬上建立一個UI。<template>
標籤包含並解析 HTML ,但不把解析出的 DOM 新增到當前文件中。
<template>
<h1>This won`t display!</h1>
<script>alert("this won`t alert!");</script>
</template>
複製程式碼
那麼解析出的 DOM 去哪了呢?它被新增到了“文件碎片”中,可以把它理解成一個包含 html 的薄容器。當被新增到 DOM 中時文件碎片就解體了,當你想保留一組稍後使用的標籤,又不想保留其容器時,文件碎片非常有用。
“那麼,我該怎麼使用一個正在解體的容器中的標籤呢?”
答案是:你只需把模版的文件碎片插入當前文件即可:
let template = document.querySelector(`template`);
document.body.appendChild(template.content);
複製程式碼
上面這段程式碼可以正常執行,但是如果你剛解體了文件碎片就會報錯!如果你重複執行上述程式碼就會報錯。因為第二次執行時 template.content
已經沒有了。我們應該用一個碎片的拷貝代替 template.content
,然後再插入這個拷貝,程式碼如下:
document.body.appendChild(template.content.cloneNode(true));
複製程式碼
cloneNode
方法顧名思義,接收一個引數控制只拷貝標籤本身還是包括它的子標籤。
新知識點:
<template>
標籤包含 HTML,但是不向當前文件新增。
總結:
Custom Elements
Custom Elements是 Web Components標準的代表。它確實讓開發人員實現了自定義 HTML 標籤。這一切的實現得益於 ES6 的 class 語法糖。如果你對 javascript 或者其他面嚮物件語言很熟悉的話,你可以像這樣通過繼承來實現自己的類:
class MyClass extends BaseClass {
// class definition goes here
}
複製程式碼
我們來試一下這樣寫:
class MyElement extends HTMLElement {}
複製程式碼
不久之前這樣寫還會報錯。瀏覽器不允許原生 HTMLElement 類或其子類被繼承。Custom Elements 解除了這一限制。
瀏覽器會把 <p>
標籤對映到 HTMLParagraphElement 原生類,但是它怎麼對映自定義類呢?除了繼承內部類外,還有一個“自定義標籤登錄檔”用於宣告這種對映:
customElements.define(`my-element`, MyElement);
複製程式碼
現在頁面上的每個 <my-element>
標籤都與一個 MyElement 元素對應。 頁面每解析一個 <my-element>
標籤就呼叫一次 MyElement 的建構函式。
為什麼標籤名帶中橫線呢?標準制定者希望未來開發者可以自由的自定義標籤,這意味著開發者都可以建立 <h7>
或者 <vr>
這樣的標籤。為了避免未來的衝突,所有自定義標籤必須加中橫線,同時原生 HTML 標籤保證絕不包含中橫線。問題解決!
除了標籤建立時會呼叫建構函式,還有一系列生命週期函式會在特定時刻被呼叫:
connectedCallback
當元素被新增到文件中時呼叫。這個函式可能多次呼叫,比如標籤移動、移除或重新新增時。disconnectedCallback
與connectedCallback
相對應。attributeChangeCallback
元素屬性更改時呼叫。
下面是一個稍複雜的例子:
class GreetingElement extends HTMLElement {
constructor() {
super();
this._name = `Stranger`;
}
connectedCallback() {
this.addEventListener(`click`, e => alert(`Hello, ${this._name}!`));
}
attributeChangedCallback(attrName, oldValue, newValue) {
if (attrName === `name`) {
if (newValue) {
this._name = newValue;
} else {
this._name = `Stranger`;
}
}
}
}
GreetingElement.observedAttributes = [`name`];
customElements.define(`hey-there`, GreetingElement);
複製程式碼
在頁面上這樣使用:
<hey-there>Greeting</hey-there>
<hey-there name="Potch">Personalized Greeting</hey-there>
複製程式碼
如果要繼承一個 HTML 原生標籤,你可能會想定義一個看起來完全不同新標籤。比如讓 <hey-there>
去繼承 <button>
:
class GreetingElement extends HTMLButtonElement
複製程式碼
同時要在自定義標籤登錄檔中體現出繼承一個已有標籤:
customElements.define(`hey-there`, GreetingElement, { extends: `button` });
複製程式碼
我們應該用被繼承的標籤加 is
屬性來表示這種繼承關係,而不是直接用自定義標籤,我們這樣使用繼承 <button>
的 <hey-there>
標籤:
<button is="hey-there" name="World">Howdy</button>
複製程式碼
這不是多此一舉,這樣程式就會知道 <hey-there>
是繼承的 <button>
。
這些對所有的傳統 web 標籤都適用。我們可以使用 <template>
設定一系列事件處理程式,新增自定義樣式,甚至可以封裝一個內部結構。其他人可以通過 HTML 標籤、 DOM 呼叫、或者新框架(其中一些框架支援在虛擬 DOM 中自定義標籤名)的方式在自己的程式碼中引用你的自定義元件。因為這些都是標準的 DOM 介面,所以 Custom Elements 實現了真正的可移植元件。
新知識點:
- Custom Elements 可以繼承原生 HTMLElement 類和其子類。
- 通過
customElements.define()
維護自定義標籤登錄檔。 - 特定生命週期函式在標籤建立、新增到DOM、屬性被修改等時刻呼叫。
總結:
ES6 Classes 特別是 子類和 extends 關鍵詞
Shadow DOM
我們寫出了友好的 custom element
,也為其新增了漂亮的樣式。現在我們想把它用在我們的站點上,也想把程式碼分享出去,讓更多的人用在他們的網站上。但是我們怎麼避免自定義 <button>
標籤和其他網站的 css 衝突?答案是使用 Shadow DOM。
Shadow DOM 標準提出了 shadow root 的概念。shadow root 有標準的 DOM 方法,也可以像其他 DOM 節點一樣新增到文件中。shadow root 的亮點在於其內容不會出現在包含其父節點的文件中:
// attachShadow creates a shadow root.
let shadow = div.attachShadow({ mode: `open` });
let inner = document.createElement(`b`);
inner.appendChild(document.createTextNode(`Hiding in the shadows`));
// shadow root supports the normal appendChild method.
shadow.appendChild(inner);
div.querySelector(`b`); // empty
複製程式碼
在上面的例子中,<div>
包含 <b>
並且 <b>
標籤也渲染在了頁面上,但是常規的 DOM 方法卻找不到它。不僅如此,頁面的樣式也影響不到它。這意味著 shadow root 既不受外部樣式影響,其內部樣式也不會洩漏。但這邊界不涉及安全性,頁面上的 js 可以檢測到 shadow root 的建立,通過 shadow root 的引用,可以查詢到它裡面的內容。
為 shadow root 裡的內容設定樣式可以通過給根節點新增<style>
(或者 <link>
)標籤:
let style = document.createElement(`style`);
style.innerText = `b { font-weight: bolder; color: red; }`;
shadowRoot.appendChild(style);
let inner = document.createElement(`b`);
inner.innerHTML = "I`m bolder in the shadows";
shadowRoot.appendChild(inner);
複製程式碼
現在我們可以真正使用 <template>
標籤了!不管用哪種方法,shadow root 內部的 <b>
標籤樣式只會被根標籤上的樣式控制,不會受外部影響。
如果 custom element 不使用 shadow DOM 怎麼辦?我們依然可以使用一個新標籤 <slot>
:
<template>
Hello, <slot></slot>!
</template>
複製程式碼
如果這個模板被新增到一個 shadow root 中,那麼下述標籤:
<hey-there>World</hey-there>
複製程式碼
將被渲染為:
Hello, World!
複製程式碼
這種將 shadow DOM 和非 shadow DOM 整合使用的功能,可以把 custom element 複雜的實現封裝在其內部,而把呼叫變的簡單。slot
的威力遠不止這些,還有多重 slot
、命名 slot
、針對特定內容的 css 偽類 slot
等。建議查閱文件瞭解更多。
新知識點
- 一種準遮蔽 DOM 結構 —— shadow root
- 建立和訪問shadow root 的 DOM API
- shadow root 的樣式作用域
- 用於shadow root 和樣式作用域的新 css 偽類
<slot>
標籤
最終效果
最後來一起實現這個漂亮的按鈕吧!我們給這個按鈕取名 <fancy-button>
。它的奇妙之處在於,它有定製的樣式,也允許我們為它新增圖示使它變得美觀。我們把樣式封裝在 shadow root 中,這樣就可以保證在任何引用它的網站上樣式保持不變。
你可以檢視下面這個完整的互動型程式碼示例。請仔細檢視 custom element 的 js 定義以及 <template>
標籤的樣式和結構。
總結
Web Components標準建立在這樣一種理念之上:提供多個底層功能,開發者以標準制定者未曾設想的方式把這些功能組合起來使用。Custom Elements 已經被用於在頁面上建立 VR 內容、富UI工具等,並使這些變得簡單。儘管標準的敲定過程很漫長,Web Components 標準為 Web 開發者提供了更多的可能。現代瀏覽器已經支援了這項技術,Web Components 的未來在你手中,使用它來創造奇蹟吧!