[譯] 2018 來談談 Web Component

來自專欄掘金翻譯計劃12 人贊了文章

  • 原文地址:Web Components in 2018
  • 原文作者:James Milner
  • 譯文出自:掘金翻譯計劃
  • 本文永久鏈接:github.com/xitu/gold-mi
  • 譯者:老教授
  • 校對者:xutaogit、zyziyun

對很多人來說,組件已經成為他們開發工作中的核心概念。組件提供了一種健壯的模型,允許我們用一個個更小的更簡單的封裝好的部件來搭建出複雜的應用程序。組件的概念在 Web 上已經存在一段時間了,比如在 JavaScript 生態的早期,Dojo Toolkit 已經在它的 Dijit 插件系統裡面應用了組件這個概念。

現代框架比如說 React、Angular、Vue 和 Dojo 進一步把組件放在開發的前列,並作為核心要素用在它們自己的框架結構上。然而,雖說組件結構變得越來越普遍,但是各種各樣的框架和庫也衍生出一個紛繁複雜、四分五裂的組件生態。這種分裂常常將一些團隊釘死在某個特定的框架上,哪怕時間、技術的更迭也不會輕易地改變。

解決這種割裂的形勢,讓 Web 組件模型統一化,這項工作已經在努力推進中。最早的努力當數 「Web Component」 規範說明 circa 2011 的出現,並在同年的 Fronteers Conference 大會上由 Alex Russell 將之宣之於眾。該 Web Component 規範的產生和發展,旨在提供一種權威的、瀏覽器能理解的方式來創建組件。在做出跨瀏覽器支持的組件方案這件事上我們還有很多事情要做,但已經比以往任何時候更接近目標了。理論上講,這些規範和實踐鋪平了組件間相互作用相互結合的道路,即使這些組件出自不同的供應方(比如 React,比如 Vue)。下面我們開始探索 Web Component 規範的組成。

組成部分

Web Component 並非單一的技術,而是由一系列 W3C 定義的瀏覽器標準組成,使得開發者可以構建出瀏覽器原生支持的組件。這些標準包括:

  • HTML Templates(譯者註:模板)and Slots(譯者註:插槽) — 可復用的 HTML 標籤,提供了和用戶自定義標籤相結合的介面
  • Shadow DOM(譯者註:影子節點) — 對標籤和樣式的一層 DOM 包裝
  • Custom Elements(譯者註:自定義元素) — 帶有特定行為且用戶自命名的 HTML 元素

這裡還有另一個 Web Component 規範,HTML Imports,用於將 HTML 代碼及 Web Component 導入到網頁中。然而,在交叉參考 ES Module 規範後,Firefox 團隊認為這不是一種最佳實踐,該規範也就沒多少人在推動了。

Shadow DOM 和 Custom Element 規範經歷了一些迭代,現在都已經是第二個版本(v1)。在 2016 年 2 月,有人推動將 Shadow DOM 和 Custom Element 併入 DOM 標準規範裡面,而不再作為獨立的規範存在。

template 標籤和 slot 標籤

HTML 模板是支持度最高的特性,可以說是 Web Component 規範最直觀的體現。它允許開發者定義一個直到被複制使用時才會進行渲染的 HTML 標籤塊。你可以參考下面的簡單示例來定義一個模板:

<template id="custom-template> <h1>HTML Templates are rad</h1></template>

一旦 DOM 裡面定義了這樣的一個模板,就可以在 JavaScript 裡面引用了:

const template = document.getElementById("custom-template");const templateContent = template.content;const container = document.getElementById("container");const templateInstance = templateContent.cloneNode(true);container.appendChild(templateInstance);

像上面那樣寫,就可以藉助 cloneNode 函數來複用這個模板。提到 <template> 標籤就不得不提 <slot> 標籤。slot 標籤允許開發者通過特定接入點來動態替換模板中的 HTML 內容。它用 name 屬性來作為唯一識別標誌(譯者注,就類似普通 DOM 節點的 id 屬性):

<template id="custom-template"> <p><slot name="custom-text">We can put whatever we want here!</slot></p></template>

slot 標籤在 Custom Element 的注入中非常有用。它允許開發者在寫好的 Custom Element 裡面設置標記。當 Custom Element 裡面的節點用到了 slot 屬性作為標記,那這個節點就會替換掉模板裡面對應的 slot 標籤。

Shadow DOM

在頁面上定位具體的節點這是 web 開發的一個基本能力。CSS 選擇器不僅可以用來給節點加樣式,還可以用來查詢特定的 DOM 集合。這通常發生在根據一個標識符選擇特定節點,比方說使用 document.querySelectorAll 就可以找到整個 DOM 樹中匹配指定選擇器的節點數組。然而,如果應用程序非常龐大,有很多節點有衝突的 class 屬性,那又該怎麼辦?此時,程序就不知道哪個節點是想被選中的,bug 也就隨之產生。如果可能的話,將部分 DOM 節點抽象出來,隔離開來,讓它們不會被 DOM 選擇器選擇到,那豈不是很好?Shadow DOM 就能做到,它允許開發者將一些節點放到獨立的子樹上來實現隔離。根本上說 Shadow DOM 提供了一種健壯的封裝方式來做到頁面節點的隔離,這也是 Web Component 的核心優勢。

與此相似,CSS 的類和 ID 應用於全局樣式時也會出現類似的問題。衝突的命名標示會導致樣式的相互覆蓋。那參考上面 DOM 樹選擇節點的思路,如果能將 CSS 樣式限制在某個 DOM 的子樹上,不就可以避免全局樣式衝突,解決問題?比較有名的樣式設置技術比如 CSS Modules 或者 Styled Components,它們的核心出發點之一就是為瞭解決這個問題。舉個例子,CSS 模塊技術通過對類名和模塊名進行哈希處理,賦予每個 CSS 樣式唯一的標識符從而避免衝突。Shadow DOM 跟它們不同之處在於它並不對類名做處理,而是直接就把這個作為原生特性來支持。它將部分 DOM 節點隔離開來使得我們的網站和程序少了不可預知的變化,更加穩定。

那在代碼層面上該怎麼操作?可以這樣將 Shadow DOM 附加到一個節點上:

element.attachShadow({mode: open});

這裡 attachShadow 函數接受一個含 mode 屬性的對象作為參數。Shadow DOM 可以打開關閉打開時使用 element.shadowRoot 就可以拿到 DOM 子樹,反之如果關閉了則會拿到 null。接著創建一個 Shadow DOM 就會創建一個陰影的邊界,在封裝節點的同時封裝樣式。默認情況下該節點內部的所有樣式會被限制僅在這個影子樹裏生效,於是樣式選擇器寫起來就短得多了。Shadow DOM 通常可以和 HTML 模板結合使用:

const shadowRoot = element.attachShadow({mode: open});shadowRoot.appendChild(templateContent.cloneNode(true));

現在這個 element 就有一個影子樹,影子樹的內容是模板的一個複製。Shadow DOM、 <template> 標籤、<slot> 標籤在這裡和諧地應用在一起,構造出了可復用、封裝良好的組件。

通過 Custom Element 進一步封裝

HTML 的 template 和 slot 標籤提供了復用性和靈活性,Shadow DOM 提供了封裝方法。而 Custom Element 再進一步,將所有這些特性打包在一起成為有自己名字的可反覆使用的節點,讓它可以像常規 HTML 節點一樣用起來。

定義一個 Custom Element

定義 Custom Element 要用到 JavaScript。Custom Element 依賴 ES2015+ 的 Class 特性,用 Class 作為其聲明模式,通常是從 HTMLElement或它的子類繼承而來。這裡有一個 Custom Element 的例子,使用 ES2015+ 語法創建,用於計數:

// 我們定義一個 ES6 的類,拓展於 HTMLElementclass CounterElement extends HTMLElement { constructor() { super(); // 初始化計數器的值 this.counter = 0; // 我們在當前 custom element 上附加上一個打開的影子根節點 const shadowRoot= this.attachShadow({mode: open}); // 我們使用模板字元串來定義一些內嵌樣式 const styles=` :host { position: relative; font-family: sans-serif; } #counter-increment, #counter-decrement { width: 60px; height: 30px; margin: 20px; background: none; border: 1px solid black; } #counter-value { font-weight: bold; } `; // 我們給影子根節點提供一些 HTML shadowRoot.innerHTML = ` <style>${styles}</style> <h3>Counter</h3> <slot name=counter-content>Button</slot> <button id=counter-increment> - </button> <span id=counter-value>; 0 </span>; <button id=counter-decrement> + </button> `; // 我們可以通過影子根節點查詢內部節點 // 就比如這裡的按鈕 this.incrementButton = this.shadowRoot.querySelector(#counter-increment); this.decrementButton = this.shadowRoot.querySelector(#counter-decrement); this.counterValue = this.shadowRoot.querySelector(#counter-value); // 我們可以綁定事件,用類方法來響應 this.incrementButton.addEventListener("click", this.decrement.bind(this)); this.decrementButton.addEventListener("click", this.increment.bind(this)); } increment() { this.counter++ this.invalidate(); } decrement() { this.counter-- this.invalidate(); } // 當計數器的值發生變化時調用 invalidate() { this.counterValue.innerHTML = this.counter; }} // 這裡定義了可以在 DOM 樹上直接使用的真實節點customElements.define(counter-element, CounterElement);

特別注意最後一行,那裡註冊了可以用在 DOM 裡面的 Custom Element。

Custom Element 的種類

上面代碼展示瞭如何從 HTMLElement 介面做拓展,然而我們還可以從更具體的節點上拓展,比如 HTMLButtonElement。Web Component 規範提供了一個完整的可供繼承的介面列表。

Custom Element 可分為兩種主要類型:獨立自定義元素(Autonomous custom elements)內置自定義元素(Customized built-in elements)。獨立自定義元素和那些早已定義且不繼承自特定介面的節點類似(譯者註:就是我們平常使用的 DOM 節點)。一個獨立自定義元素只要在頁面一定義上,就可以像常規 HTML 節點那樣使用。舉個例子,上面定義的計數節點,既可以在 HTML 中通過 <counter-element></counter-element> 定義,也可以在 JavaScript 中用 document.createElement(counter-element) 來創建。

內置自定義元素在使用上略有不同,當 HTML 定義節點時可以傳一個 is 屬性到標準節點上(比如 <button is=special-button>),又或者使用 document.createElement 時傳一個 is 屬性作為參數(比如 document.createElement("button", { is: "special-button" })。

Custom Element 的生命週期

Custom Element 也有一系列的生命週期事件,用於管理組件連接和脫離 DOM :

  • connectedCallback:連接到 DOM
  • disconnectedCallback: 從 DOM 上脫離
  • adoptedCallback: 跨文檔移動

一種常見錯誤是將 connectedCallback 用做一次性的初始化事件,然而實際上你每次將節點連接到 DOM 時都會被調用。取而代之的,在 constructor 這個 API 介面調用時做一次性初始化工作會更加合適。

此處還有一個 attributeChangedCallback 事件可以用來監聽節點(譯者註:使用 Custom Element 定義的節點)屬性的變化,然後通過這個變化來更新內部狀態。不過,要想用上這個能力,必須先在節點類裡面定義一個名為 observedAttributes 的 getter:

constructor() { super(); // ... this.observedAttributes();} get observedAttributes() {return [someAttribute]; } // 其他方法

從這裡起就可以通過 attributeChangedCallback 來處理節點屬性的變化:

attributeChangedCallback(attributeName, oldValue, newValue) { if (attributeName==="someAttribute") { console.log(oldValue, newValue) // 根據屬性變化做一些事情 }}

支持度如何?

截至 2018 年 6 月,Shadow DOM 第二版和 Custom Element 第二版在 Chrome、Safari、三星瀏覽器上已經支持,還被 Firefox 列為要支持的特性,希望很大。而 Edge 依然在考慮是否支持。在這個時間點,Github 倉庫 webcomponents 上已經有了一系列的 polyfill。這些 polyfill 使得包括 IE11 在內的所有當下活躍的瀏覽器上都能運轉 Web Component。該 webcomponents 庫包含多種形態,既提供了一個包含所有必要 polyfill 的腳本(webcomponents-bundle.js),也提供了一個通過特性檢測來只載入必要 polyfill 的版本(webcomponents-loader.js)。如果使用第二種,你還是必須將各個 polyfill 文件都放到伺服器上來保證載入器可以載入到。

對於那些代碼中只能用 ES5 的情況,還必須載入一個 custom-elements-es5-adapter.js 文件,而且它必須首先載入,不能跟組件代碼打包在一起。之所以需要這個適配文件是因為 Custom Element 必須 繼承自 HTMLElement 類,且構造函數中必須以 ES2015 的方式調用 super()(這在 ES5 代碼裏看起來會很困惑!)。在 IE11 中還是會由於不支持 ES2015 的類特性而拋出錯誤,不過可以忽略之。

Web Component 和框架

歷史上,Web Component 最大的支持者之一是 Polymer 庫。Polymer 針對 Web Component API 添加了一些語法糖使得定義和傳遞組件變得更加容易。在最新版本 Polymer3 中,它與時俱進用上了 ES2015 的模塊特性並且使用 npm 作為標準的包管理工具,跟上了其他的現代框架。Web Component 編碼工具的另一種形態則更像是編譯器而非框架。Stencil 和 Svelte 這兩個框架就是這樣。它們使用各自的工具 API 來書寫組件,然後編譯成原生的 Web Component。一些框架比如 Dojo 2, 則選擇允許開發者編寫特定框架的組件,不過也允許編譯成原生 Web Component 就是了。在 Dojo2 中這是用 @dojo/cli tools 來實現的。

努力實現原生的 Web Component 的一個願景,是希望跨越不同團隊不同項目來共用組件,即使它們用的是不同的框架。當下不同的框架和 Web Component 規範有不同的關係,有些更貼近規範有些則不然。已經有一些指引告訴我們怎麼在諸如 React 和 Angular 這樣的框架中用上原生的 Web Component ,但它們的實現上還是帶著濃濃的框架特色。有一個很好的資源可以幫你理解這些關係,那就是 Rod Dodson 的 Custom Elements Everywhere,它通過測試用例測出不同框架想和 Custom Element(Web組件規範的核心) 結合的難易程度。

最後的想法

圍繞 Web Component 的使用和炒作不斷持續此起彼伏。這意味著,隨著 Web Component 得到越來越好的支持,polyfill 將逐漸淡出我們的視野,組件書寫將更加簡潔和快速。Shadow DOM 允許開發者寫一些簡單的限定區域有效的 CSS,這無疑更加容易管理,通常性能也會更好。Custom Element 提供了一種統一的方法來定義組件,這些組件可以(理論上)跨代碼庫和團隊來使用。目前有一些額外的規範建議,開發者可以根據基本規範加以利用:

  • Custom Element Registries – 限制節點註冊,避免節點命名衝突;
  • Shadow CSS Parts – 組件的原生主題;
  • Template Instantiation – 使用 JavaScript 變數來快速動態應用模板;

這些補充規範可以為原生 web 平臺增加更多功能,讓開發者不用再去理解那麼多抽象概念,釋放更多的潛力。

該基本規範毫無疑問是一套強大的工具,但最終它是否能發揮最大的效用還是要取決於用到它的框架、開發者和團隊。目前如 React、Vue、Angular 這樣的框架已經大大佔據了開發者的大腦,它們會因為這些原生態的技術和工具而逐漸敗下陣來嗎?只能讓時間來見證了。


下一步

你是否希望在你的下一個項目或框架中用上 Web Component?聯繫我們,探討下我們可以怎麼幫到你!

在 SitePen On-Demand Development 可以獲取幫助,它有我們對 JavaScript 和 TypeScript 大大小小問題的快速有效解決方案。

如果發現譯文存在錯誤或其他需要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可獲得相應獎勵積分。文章開頭的 本文永久鏈接 即為本文在 GitHub 上的 MarkDown 鏈接。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源為 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智慧等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。

推薦閱讀:

相關文章