一、?源起
讓我們以一個例子開始。
假設我們要做一個環形進度條,它可以:
1、根據進度數值的不同,計算出百分比,以渲染對應的角度值。
2、根據設置的進度不同,我們用不同的顏色加以區分。
3、在環的中間我們以動畫遞增的方式顯示進度。
最終的效果,大致如下圖:
除了直接用組件庫,聰明的你肯定已經想到了多種解決辦法。如使用現代前端框架React/Vue/Angular/SvelteJS/SolidJS等等,你可能會找到或編寫對應的組件,通過相應數據狀態的變更,完成相對復雜的交互;又如在小快靈的項目下,用jQuery的Widget開發也是一個不錯的選擇;再或者,你可以點開你的HTML+JavaScript+CSS技能樹,純手工打造一個。這都是可以完成的任務。
這里[1]就是一個針對這個需求的實現。
當然,在完成之后,你可能會考慮對組件做一些提煉,下次再遇到同樣的需求,你就可以氣定神閑地“開箱即用”。通常,我們希望撰寫的代碼能夠實現在UI以及行為層面的復用。所以,以組件為單位進行代碼復用的需求也就呼之欲出。
實際上,除了Web前端外,各種相關的界面技術,比如安卓和蘋果App、Windows軟件、Qt以及Flutter等等,都發展出了類似的組件化技術。可以說,組件化是界面技術發展到一定復雜程度的必然產物。
為了放心地復用組件,除了代碼層面的復用方案,也要包含一定的隔離特性:我們希望元素的樣式、行為能夠不被其他的代碼干擾,也不會干擾到頁面其他的元素,即存在一定的隔離性。考慮下面的代碼:
考慮上述第10行到20行,以及21行到29行分別來自不同的組件。這里第二個組件的加入就會影響第一個組件的行為。第一個組件也在完全不知情的情況下被莫名其妙地變更了行為、樣式。
當然,這樣的代碼是精心簡化過的,實際上的工程代碼不會這樣撰寫。但復雜的代碼會造成問題被隱藏的更深,更加難以發現。我們當然可以直接詰問開發者的慮事不周,不過更理智的做法是提煉一些工程化的方法來避免這種問題的發生。比如Vue框架中的scoped樣式。歸根到底的問題在于,早期的HTML、CSS缺乏封裝所必須的特性。
為了彌補這一缺陷,技術人員做了各種探索。
“上古時期”,微軟在IE系列曾實現過一種名曰“HTC”的技術,我們甚至還能從互聯網上找到這個技術的蛛絲馬跡[2]。它實現了組件模版、樣式、行為的復用和隔離方案。然而這項技術是IE Only的技術,后面隨著大廠博弈以及各種標準化方案的誕生,這個方案在IE10以后便逐漸退出歷史舞臺。
Firefox在2007年左右也曾經支持過一個封裝方案,叫XML Binding Language[3]。不過這項技術也由于主要在Firefox支持,也已經逐步淡出。
前端的框架如Angular、React、Vue等等,都提供了組件的復用和行為隔離的方案。不過由于瀏覽器層面對于前端框架的語法不支持,一般會需要在部署在生產環境前作一定的前端編譯,這也是目前前端開發的標準做法。
針對這些需求,W3C標準化項目組,在2011年前后提出了WebComponents標準,現在由WhatWG進行維護。Web Components最終試圖HTML、CSS和DOM API層面配合瀏覽器試圖解決這些問題,是一種新的瀏覽器特性,同時也是一個復合的標準,提供了Web開發中組件的實現模型。
Web Components也是這篇文章的主角。
二、?標準細節
目前認為,Web Components是一組標準的集合。針對集合涵蓋的內容,各個版本有不同的定義。由于篇幅所限,本文我們主要針對三個核心展開講解,分別是Custom Elements、HTML template以及Shadow Dom。最新版本的瀏覽器還支持ES Modules,恰當的使用ES Modules,有利于模塊的復用,在這種情況下,我們也可以把這個標準囊括進標準集合中。
2-1 Custom Elements
Custom Elements的出現,主要是為了解決HTML標簽的有限性。它允許開發者自定義標簽,并為其添加默認的行為、樣式。
那么,如何來定義呢?下面是標準語法:
這里是否傳遞第三個參數,決定了定義元素的類型。
如果增加了第三參數,則直接繼承該HTML元素的行為,稱為Customized built-in elements(自定義內置元素)。比如繼承了HTMLImageElement,則繼承了img標簽元素的所有默認行為和樣式。
如果缺省,該元素直接繼承HTMLElement,稱為Autonomous custom elements(自主自定義元素)。比如我們定義了一個user-card的標簽,之后,我們就能直接使用<user-card></user-card>來引入這種標簽的默認樣式和行為。
我們首先來看一個自定義內置元素的例子:
上述代碼擴展了html默認的button元素,定義了一個hello-button組件。
這段代碼有兩個關鍵點:
1.CustomElements.define 需要指定第三個擴展參數。
2.使用時,使用is屬性,來指定隸屬的自定義元素名。
默認的,每個hello-button都會有button的所有屬性,但增加了一點:點擊會默認彈出“Hello!”。組件的示例依然可以定義事件,并且不會覆蓋定義類上定義的事件。
需要說明的是,目前(2024年6月)safari瀏覽器最新版依然沒有對自定義內置元素進行支持,Chrome、Edge、Firefox、Opera等都提供了支持。
接下來,我們再來看一下自主自定義元素。這個特性目前瀏覽器支持較好,主流的瀏覽器都已提供支持。
之后我們在html里面加入:
就可以顯示了。這似乎有點像我們熟悉的React/Vue組件的用法了。
CustomElements支持生命周期的回調。
生命周期 | 說明 |
connectedCallback() | 每當元素添加到文檔中時調用。 |
disconnectedCallback() | 每當元素從文檔中移除時調用。 |
adoptedCallback() | 每當元素被移動到新文檔中時調用。 |
attributeChangedCallback() | 在屬性更改、添加、移除或替換時調用。 需配合靜態的observedAttributes屬性。 |
這里是一個在線的例子[4],讀者可以嘗試一下。
2-2 HTML template
上一節我們寫的user-card組件,里面都是用dom方法動態創建的。這樣不但麻煩,而且運行效率也偏低。為此,WebComponents標準提供了HTML templates的方式。
<template>標簽也可以多次復用。
盡管到這一步已經挺好了,但是元素仍舊不是很靈活。我們只能在里面顯示一點文本,這里,可以使用 <slot> 插槽元素通過聲明式的語法在每個元素實例中顯示不同的文本。插槽由其 name 屬性標識,并且允許開發者在模板中定義占位符,當在標記中使用該元素時,該占位符可以填充所需的任何 HTML 標記片段。
我們在上述代碼的第18行增加下列代碼:
同時我們在調用時使用:
則完成了文字動態傳入。
再進一步,我們索性支持外界傳值支持一下。
這樣在調用時候,我們就可以用外界傳遞的圖片了:
我們還可以在template標簽里用<style>標簽增加必要的樣式。至此,我們基本上完成了這個自定義組件。這里是完整的代碼[5]。
之所以寫在組件里,也就間接實現了封裝。這樣定義出的樣式,理論上不會影響其他的元素。
這里:
偽類,選擇包含使用這段 CSS 的 Shadow DOM 的影子宿主。具體到這個例子,我們的指的是這里:
為了最終實現封裝,我們再來引出第三項技術:Shadow Dom。
2-3 Shadow Dom
自定義元素從定義上來說是一種可重用功能:它可以被放置在任何網頁中,并且期望它能夠正常工作。因此,很重要的一點是,運行在頁面中的代碼不應該能夠通過修改自定義元素的內部實現而意外地破壞它。Shadow DOM允許你將一個 DOM 樹附加到一個元素上,并且使該樹的內部對于在頁面中運行的 JavaScript 和 CSS 是隱藏的。
為了搞清Shadow DOM的機制,我們需要先厘清幾個概念:
1.Shadow DOM: 是一種依附于文檔原有節點的子 DOM,具有封裝性。
2.Light DOM: 指原生的DOM節點,可以通過常規的API訪問。Light DOM和Shadom DOM常常一起出現。這也是很有意思的一個比喻。一明一暗,燈下有影子。
3.Shadow Trees:Shadow DOM的樹形結構。一般地,在Shadow Trees的節點不能直接被外部JavaScript的API和選擇器訪問到,但是瀏覽器會對這些節點做渲染。
4.Shadow Host:Shadow DOM所依附的DOM節點。
5.Shadow Root:Shadow Trees的根節點。外部JavaScript如果希望對Shadow Dom進行訪問,通常會借助Shadow Root。
6.Shadow Boundary:Shadow Tree的邊界,是JavaScript訪問、CSS選擇器訪問的分界點。
7.content:指原本存在于Light DOM 結構中,被標簽添加到影子 DOM 中的節點。自Chrome 53以后,content標簽被棄用,轉而使用template和slot標簽。
8.distributed nodes:指原本位于Light DOM,但被content或template+slot添加到Shadow DOM 中的節點。
9.template:一致標簽。類似我們經常用的<script type='tpl'>,它不會被解析為dom樹的一部分,template的內容可以被塞入到Shadow DOM中并且反復利用,在template中可以設置style,但只對這個template中的元素有效。
10.slot:與template合用的標簽,用于在template中預留顯示坑位。
下面這幅圖,顯示了這些概念之間的關系:
了解了Shadow DOM的概念,我們就可以利用Shadow Dom做一些事情了。
這里注意下{mode: 'open'},此后通過div.shadowRoot即可拿到sr的實例。sr可以使用一般的JavaScript API來做相關的操作。
如果這里采用{mode: 'closed'},則此時div.shadowRoot為null。外部不可能再拿到sr的實例。此時外部很難操作到sr下的Shadow DOM,僅可以依靠Shadow內部的元素來進行操作。
下面是操作Shadow DOM樣式的幾種方法:
1.在Shadow DOM內部來操作Shadow Host的樣式。
:host 允許你選擇并樣式化 Shadow Tree所寄宿的元素
2.跨越Shadow Boundary的樣式::part()
對于::part,在允許樣式定義的Shadow DOM,給屬性part賦值,樣式選擇器可以使用::part(屬性值)即可實現指定樣式,這與之前不同的是,Shadow Dom元素知道外界可能會對其某些元素進行變化,是可以控制變化范圍的。需要注意的是,在::part()選擇器后,子代選擇器無效。如你不能使用::part(foo) span。
?
::part()選擇器自Chrome73開始支持。之前的版本,可以考慮^和^^選擇器,^和^^選擇Shadow DOM在最新版本已經無效。
三、?與現代前端框架的關系
從之前的內容我們可以看到,Web Components標準中有若干內容與現代前端框架有異曲同工的感覺。同時,現代前端框架在設計時或多或少會參考Web Components的標準。VueJS的創始人尤雨溪曾明確表示過Vue的模版設計部分遵照了Web Components的標準。這也為以后瀏覽器能力逐漸增強,前端框架減負,提供了可能。
同時,現在大多數的前端框架都提供了和Web Components組件共存的機制。以Vue為例,官方更是提供了將Vue組件編譯為Web Components自主自定元素方案和自主自定元素轉化為Vue組件的工具鏈。
直接用Web Components標準撰寫的組件,原則上不需要經過預編譯環節,可以直接運行于瀏覽器,理論上會比其他前端框架性能有一定的優勢。但是,由于該標準提供的API較原始,需要做進一步封裝才能更好的使用。同時,標準缺乏對現今流行的MVVM的支持,使得前端框架在數據驅動開發模式上仍有用武之地。
Shadow Dom等標準的進展,客觀上也使得前端微服務模式慢慢成型,使得qiankun等前端解決方案有了落地的基石。
四、?結語
本文我們介紹了Web Components標準的主要技術點。應該注意到的是,標準還在不斷演化過程中,各家瀏覽器的支持也在不斷完善。我們期待,不久的將來,藉由瀏覽器原生支持的組件化方案,能夠大放光彩。
文內鏈接
[1].https://github.com/taisuke-j/progress-ring-component
[2].https://docs.microsoft.com/en-us/previous-versions/aa918246(v=msdn.10)
[3].https://www.slideshare.net/slideshow/xml-binding-language-20/155196#5
[4].https://jsbin.com/qorohov/8/edit?html,js,console,output
[5]. https://jsbin.com/qorohov/23/edit?html,output