Web Components 簡史
HTML Components(HTCs)
1998年微軟提出了革命性的技術—— HTML Components ,意圖用 HTML 組件來取代 ActiveX 組件,這是最早的 Web Components 雛形,只經歷了 IE5~IE9 5個版本就退出了歷史舞臺,在2011年 IE10 推出的時候被微軟官方宣佈棄用。
XML Binding Language(XBL)
在2001年火狐又提出了一種基於 XML 的語言—— XBL,雖然只能在Firefox瀏覽器上運行。但幸運的是,W3C委員會在2007年通過了 XBL 2 版本。即使如此,直到2012年被廢棄時也只有Firefox瀏覽器支持XBL。
Web Components
2013年 Chrome 和 Opera 又聯合提出了推出的 V0 版本的 Web Components 規範,在2016推進到了 V1 版本。雖然到目前爲止也沒有被 W3C 採納爲正式規範,但瀏覽器的支持情況還不錯。FireFox、Chrome、Opera已全部支持,Safari也大部分支持,Edge都換成webkit內核了,離全面支持應該也不遠了。如果搭配 Polyfills 則不存在這個問題了。
Web Components 構成
Custom elements(自定義元素) 值得注意的是,從 HTML5 規範開始,提倡我們不要使用單個單詞作爲自定義元素,這樣做是爲了避免和默認元素衝突,而要短橫線相連的多個單詞作爲元素名稱。比如 icon 是不符合規範的,asm-icon 是符合規範的。
Shadow DOM(影子DOM)
爲了保證組件的隔離性,Web Components 組件藉助了 Shadow DOM 技術。它和DOM 基本相似,可以簡單地理解爲被隔離的 DOM Shadow DOM 由於這個隔離特性帶來了封裝下同時也帶來一個比較麻煩的問題: 樣式的繼承 。 兩個解決方法:
- 在 Shadow DOM 中使用 link 標籤引入公共樣式文件,這樣會有個小小的問題:瀏覽器會重複請求 css 文件。
- 在 Shadow DOM 中使用style標籤並寫入公共樣式,這樣帶來的小問題就是大量的重複
HTML templates(HTML模板)
模板技術引入了兩個重要的元素 template 和 slot ,增強了組件的靈活性,可以類比vue的template 和slot
wc生命週期
class MyElementLifecycle extends HTMLElement {
// 元素初始化的時候執行類比react的constructor
constructor() {
// HTMLElement.prototype.constructor.call(this)
super()
console.log('constructed!')
}
/**
* connectedCallback
* 當元素插入到 DOM 中時,將調用 connectedCallback。
* 這是運行安裝代碼的好地方,比如獲取數據或設置默認屬性。
* 可以將其與React的componentDidMount方法進行比較
*/
connectedCallback() {
console.log('connected!')
}
/**
* disconnectedCallback
* 只要從 DOM 中移除元素,就會調用 disconnectedCallback。清理時間到了!
* 我們可以使用 disconnectedCallback 刪除事件監聽,或取消記時。
* 但是請記住,當用戶直接關閉瀏覽器或瀏覽器標籤時,這個方法將不會被調用。
*
* 可以用window.unload beforeunload或者widow.close 去觸發在瀏覽器關閉是的回調
*
* 可以與 react 中的 componentWillUnmount 的方法進行比較
*/
disconnectedCallback() {
console.log('disconnected!')
}
static get observedAttributes() {
return ['my-attr']
}
/**
*
* @param {*} name
* @param {*} oldVal
* @param {*} newVal
*
* 每當添加到observedAttributes數組的屬性發生變化時,就會調用這個函數。使用屬性的名稱、舊值和新值調用該方法
* react 中的componentWillReceiveProps,和 static getDerivedStateFromProps(props, state) 有些類似
*/
attributeChangedCallback(name, oldVal, newVal) {
console.log(`Attribute: ${name} changed!`)
}
/**
* 每次將自定義元素移動到新文檔時,都會調用 adoptedCallback。
*/
adoptedCallback() {
console.log('adopted!')
}
/**
* 生命週期的執行順序 掛載的時候 按照react 中的執行順序是相同的
* constructor -> attributeChangedCallback -> connectedCallback
*/
}
// 不是生命週期的API 但是非常重要 註冊
window.customElements.define('my-element-lifecycle', MyElementLifecycle)
react中使用wc組件
另外,react,vue,angular 等都可以編譯成wc,詳細可以看看這個鏈接:https://webcomponents.dev/blog/all-the-ways-to-make-a-web-component/, 裏面列舉了當前55中技術棧生成wc代碼的例子,裏面做了統計和比較包括:打包的大小,30個組件的打包大小,解析JavaScript和創建DOM樹的性能。 wc的兼容性: Internet Explorer 11 不支持,可以使用pollyfill safari不支持內置元素wc,但是有pollyfill Shadow DOM v1 is shipped in Chrome 53 (status), Opera 40, Safari 10, and Firefox 63
知識點:
Node.getRootNode() Node 接口的 getRootNode() 方法返回上下文中的根節點,如果 shadow DOM 可用,則對 shadow DOM 同樣適用。
語法
var root = node.getRootNode(options);
參數
options 可選 獲取根節點時的可選參數對象. 下列值可供選擇:
- composed: Boolean 如果檢索到 shadow Root 需要返回,則設置爲(false,默認值),如果跳過shadow Root 檢索普通Root則設置爲(true)。 返回值 返回一個繼承自 Node 的對象。返回值會因爲 getRootNode() 調用的地方不同而不同; 比如說:
- 在標準的網頁中調用將會返回一個 HTMLDocument 對象表示整個網頁。
- 在Shadow DOM裏調用將會返回一個與之相關聯的 ShadowRoot 。
assignedNodes() 方法
瞭解哪些元素是和插槽有關是很有用處的。調用 slot.assignedNodes() 可以找出哪些元素是由插槽渲染的。flatten: true} 選項會返回插槽的默認內容(若沒有分發任何節點)。
<slot name='slot1'><p>Default content</p></slot>
<my-container>
<span slot="slot1"> container text </span>
</my-container>
//flatten: true
[<span slot="slot1"> container text </span>]
//flatten: false
[<p>Default content</p>]
slotchange 事件
當一個插槽的分發元素節點發生變化的時候觸發 slotchange 事件。
slot.addEventListener('slotchange', function(e) {
console.log('DOM change');
});
經驗總結
css相關
- 變量穿透:
background-color: var(--icon-color, #fff);
組件外可以這樣寫
<style>
body {
--icon-color: red;
}
</style>
- ::part實現穿透
<asm-button>
#shadow-root
<div part="some-div"><span>...</span></div>
<p part="some-p" >...</p>
</asm-button>
<style>
asm-button::part(some-div){
color:red
}
asm-button::part(some-div):hover{
color:purple
}
asm-button::part(some-p){
color:orange
}
</style>
- 給宿主加樣式(:host)
:host {
display: inline-block;
}
:host([disabled]) {
pointer-events: none;
}
:host([block]) {
display: block;
}
- :host-context(selector)
與 :host 相同,但僅當外部文檔中的 shadow 宿主或它的任何祖先節點與 selector 匹配時才應用樣式。 例如,:host-context(.dark-theme) 只有在 < asm-button > 或者 < asm-button > 的任何祖先節點上有 dark-theme 類時才匹配:
<body class="dark-theme">
<asm-button>...</asm-button>
</body>
:host-context(.dark-theme){
color:gray
}
- :defined
:defined CSS 僞類 表示任何已定義的元素。這包括任何瀏覽器內置的標準元素以及已成功定義的自定義元素 (例如通過 CustomElementRegistry.define() 方法)。
asm-button:defined {
display: block;
background-color: red;
}
其實這個是不能選擇裏wc裏面的樣式的,只是可以控制shadowDom的root,也就是asm-button標籤,但是這個有個特性很好。 這在你有一個複雜的自定義元素需要一段時間才能加載到頁面中時非常有用 —— 你可能想要隱藏元素的實例直到定義完成爲止,這樣你就不會在頁面上出現一些難看的元素。
simple-custom:not(:defined) {
display: none;
}
simple-custom:defined {
display: block;
}
- ::slotted()
:slotted() CSS 僞元素 用於選定那些被放在 HTML模板 中的元素 (更多請查看 使用模板和插槽). 這個僞類選擇器僅僅適用於 影子節點樹(Shadow Dom). 並且只會選擇實際的元素節點, 而不包括文本節點. 例子:這個表示成功填充的插槽 會被置成灰色
::slotted(*) { color: gray; font-family: sans-serif; }
::slotted(span)::after {
content:'';
color: gray;
}
官方例子
<template id="person-template">
<div>
<h2>Personal ID Card</h2>
<slot name="person-name">NAME MISSING</slot>
<ul>
<li><slot name="person-age">AGE MISSING</slot></li>
<li><slot name="person-occupation">OCCUPATION MISSING</slot></li>
</ul>
</div>
</template>
<person-details>
<p slot="person-name">Boris</p>
<span slot="age">27</span>
<span slot="i-am-awesome">Time traveller</span>
</person-details>
wc組件形式
規定只能 <my-button></my-button>
不能是< myButton></myButton>
或者 <my_button></my_button>
wc中樣式引入
-
style方式(推薦)
-
link (請注意, 因爲<link> 元素不會打斷 shadow root 的繪製, 因此在加載樣式表時可能會出現未添加樣式內容(FOUC),導致閃爍。)
-
@import 與link類似
三種常用的都支持的,但是引入link的話可能會影響wc的靈活性。 組件傳值
- 屬性數據(attribute)
<counter-box max="10" min="-10" count="0"></counter-box>
- 元素屬性數據(properties)
const counter = document.querySelector('counter-box');
counter.data = {
max: 10,
min: -10,
count: 5
}
class CounterBox extends HTMLElement {
min = 0;
max = 10;
count = 0;
...
set data(value) {
this.min = value.min || this.min;
this.max = value.max || this.max; this.updateCountElement(value.count || this.count);
} ...}
3. Object.defineProperty()
const childTemplate = `<p>child</p>`;
class ChildWc extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
const tpl = document.createElement('template')
tpl.innerHTML = childTemplate
this.shadowRoot.appendChild(tpl.content.cloneNode(true))
this.data = {text: 'child'}
Object.defineProperty(this.data, 'text', {
set: val => {
this.shadowRoot.querySelector('p').textContent = JSON.stringify(val, null, 2)
}
})
}
}
customElements.define('child-wc', ChildWc)
const parentTemplate = `<child-wc/>`;
class ParentWc extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
const tpl = document.createElement('template')
tpl.innerHTML = parentTemplate
this.shadowRoot.appendChild(tpl.content.cloneNode(true))
setTimeout(() => {
this.shadowRoot.querySelector('child-wc').data.text = {name:'son'}
}, 500)
}
}
customElements.define('parent-wc', ParentWc)
調用父組件方法
通過事件監聽/冒泡,利用原生 CustomEvent 函數來創建自定義事件,然後在子組件實例上派發此事件以及數據,同時父組件進行監聽,可以藉助 document 設置事件總線,進行跨組件全局通信。
import React, { useEffect, useRef, useState } from 'react';
const example = () => {
const btnRef = useRef(null)
const [size, setSize] = useState('small')
function handleClick() {
console.log('click')
}
function handleCustomClick(e){
console.log(e.detail.text)
}
useEffect(() => {
if (btnRef && btnRef.current) {
btnRef.current.addEventListener("customClick", handleCustomClick)
}
},[])
return (
<div>
<asm-button ref={btnRef} onClick={handleClick} size={size}></asm-button>
</div>
);
};
export default example;
asm-button內部
function handleClick() {
// dispatch('message',{text:'i am asm-button'})
const event = new CustomEvent("customClick", {
detail: {
text: "i am asm-button"
},
bubbles: true,
cancelable: true,
composed: true //如果設置成false外面就無法監控到customClick
});
// 2. Dispatch the custom event.
buttonRef.dispatchEvent(event);
}
優勢和劣勢
優勢
- 可移植性好,跨技術棧
- 封裝性好不用擔心污染問題
- 學習成本小
劣勢
- 兼容性沒有那麼的好,但是在移動端支持還是很好的,同時也有pollyfill
- 由於shadow dom的封裝所以seo是個問題
- 配合其他組件使用的時候使用起來有點彆扭
react中使用wc
<my-accordion items={JSON.stringify(items)} />
這種使用是我們在react中習慣的方式,但是會導致dom上展示很多的信息,如果這樣寫就不會
document.querySelector('my-accordion').items = items
<my-accordion />
展示的還是一樣的。但是這樣不符合react的習慣,因而如果最終如果考慮使用者的習慣可以使用react封裝一層
實戰例子
:host {
display: block;
font-family: inherit;
}
.tabs {
slot[name='tab'] {
color: red;
display: flex;
align-items: center;
max-width: 100%;
overflow-x: scroll;
overflow-y: hidden;
background-color: var(--tabs-bg-color, var(--asm-color-white, #fff));
&::slotted(*) {
all: initial;
font-family: inherit;
width: min-content;
height: 42px;
line-height: 42px;
padding: 0 12px;
font-size: 16px;
font-weight: 500;
color: var(--tabs-bar-font-default-color, var(--asm-color-font, #000a1f));
}
&::slotted(.active) {
color: var(--tabs-bar-font-active-color, var(--asm-color-primary, #337dff));
position: relative;
&::after {
content: '';
position: absolute;
left: 12px;
right: 12px;
bottom: 0px;
z-index: 1;
height: 2px;
background-color: var(--tabs-bar-font-active-color, var(--asm-color-primary, #337dff));
}
}
}
slot[name='tabpanel'] {
&::slotted(*) {
all: initial;
padding: 12px;
font-size: 14px;
color: var(--tabs-panel-font-default-color, var(--asm-color-font, #000a1f));
display: none;
font-family: inherit;
}
&::slotted(.active) {
display: block;
}
}
}
<asm-tabs bind:this={el}>
<h4 slot="tab">tabs1</h4>
<div slot="tabpanel" class="diy">tabcontent1</div>
<h4 slot="tab">tabs2</h4>
<div slot="tabpanel">tabcontent2</div>
<h4 slot="tab">tabs3</h4>
<div slot="tabpanel">tabcontent3</div>
</asm-tabs>
<div class={clsName} bind:this={el}>
<slot name="tab" />
<slot name="tabpanel" />
</div>