你了解原生web組件嗎

讀完此文你將系統(tǒng)了解原生web組件組成以及它們之間的相互作用丰滑,并且對日常開發(fā)中遇到的問題會有更深層次的理解。

Web Components主要解決代碼復(fù)用及組件封裝問題,其包括三個主要部分:

  • Custom elements(自定義元素):允許定義custom elements及其行為,用戶可在界面中按需使用它們嫡锌。
  • Shadow DOM(影子DOM):用于將封裝的“影子”DOM樹附加到元素上(與主文檔DOM分開渲染),并控制其關(guān)聯(lián)的功能琳钉。通過這種方式势木,可以保持元素的功能私有,這樣它們就可以被腳本化和樣式化歌懒,而不用擔(dān)心與文檔的其他部分發(fā)生沖突啦桌。
  • HTML templates(HTML模板)<template><slot> 元素可以用來編寫不在渲染頁面中顯示的標(biāo)記模板,它們可作為自定義元素結(jié)構(gòu)的基礎(chǔ)被多次重用及皂。

下面將詳細(xì)敘述這三部分的使用及其相互作用甫男。

Custom Elements(自定義元素)

顧名思義且改,可以自己定義的元素標(biāo)簽,類似于在vue板驳,react中我們?yōu)榱朔奖憬M件的復(fù)用又跛,而把某個常用功能單獨(dú)寫成一個組件形式,后續(xù)可以直接調(diào)用該組件若治。不同的是慨蓝,custom elements是一個封裝好的html標(biāo)簽,他可以基于現(xiàn)有標(biāo)簽進(jìn)行功能擴(kuò)展直砂,也可以單獨(dú)定義標(biāo)簽直接調(diào)用菌仁,并且結(jié)合shadow DOM來使用可以做到減少組件中深層嵌套的標(biāo)簽浩习,使DOM更加簡潔静暂。

自定義元素的定義api:Window.customElements.define()

語法:

customElements.define('custom-element-name', customElementsClass, [extends]);
  • custom-element-name:必填,所創(chuàng)建的自定義元素名稱谱秽,需符合 DOMString 標(biāo)準(zhǔn)的字符串洽蛀。注意!custom elements 的名稱不能是單個單詞疟赊,且其中必須要有短橫線(-)郊供。
  • customElementsClass:必填,constructor近哟,通常是用于定義元素行為的 驮审。
  • extends:可選參數(shù),一個包含 extends 屬性的配置對象吉执。它指定了所創(chuàng)建的元素繼承自哪個內(nèi)置元素疯淫,可以繼承任何內(nèi)置元素。

customElements 是Window對象上的一個只讀屬性戳玫,接口返回一個 CustomElementRegistry 對象的引用熙掺,可用于注冊新的 custom elements。

舉個例子:下面注冊了一個通用<my-footer>標(biāo)簽

  • 注冊custom elements
// 為元素創(chuàng)建一個class
class myFooter extends HTMLElement {
    constructor() {
        // 必須先調(diào)用super方法咕宿,類的構(gòu)造函數(shù)constructor總是先調(diào)用super()來建立正確的原型鏈繼承關(guān)系币绩。
        self = super();

        const imgAttr = this.getAttribute('img')
        const textAttr = this.getAttribute('text')

        const style = document.createElement('style');
        style.textContent = ` div {text-align:center;margin: 0 auto; } `;

        const imgNode = document.createElement('img')
        imgNode.setAttribute('src', imgAttr)

        const textNode = document.createElement('div')
        textNode.innerHTML = textAttr

        const rootNode = document.createElement('div')
        rootNode.appendChild(imgNode)
        rootNode.appendChild(textNode)
        rootNode.appendChild(style);

        this.appendChild(rootNode)
    }
}
// 定義自定義元素標(biāo)簽
customElements.define('my-footer', myFooter);
  • 調(diào)用自定義標(biāo)簽
<body>
    <my-footer img="./img/default.png" text="default footer"></my-footer>
</body>
  • 效果


    image.png

Custom Elements的分類

自定義元素分為兩類:

  • Autonomous custom elements 自主定義元素
  • Customized built-in elements 自定義內(nèi)置元素

Autonomous custom elements 自主定義元素

如同上文定義的一個<my-footer>標(biāo)簽一樣,custom elements不繼承其他內(nèi)置的HTML元素府阀,你可以直接把它們寫成HTML標(biāo)簽的形式在頁面上使用缆镣,如<my-footer></my-footer>,或者通過document.createElement("my-footer")創(chuàng)建元素试浙。Autonomous custom elements 的類定義總是繼承自HTMLElement.

Customized built-in elements 自定義內(nèi)置元素

Customized built-in elements 繼承自基本的HTML元素董瞻。在創(chuàng)建時,必須指定所需擴(kuò)展的元素川队,使用時力细,需要先寫出基本的元素標(biāo)簽睬澡,并通過 is 屬性指定custom elements的名稱。例如<p is="word-count">, 或者 document.createElement("p", { is: "word-count" })眠蚂。

// 自定義內(nèi)置元素
class moneyFormat extends HTMLParagraphElement {
        constructor() {
            self = super();
            const money = this.textContent
            this.textContent = `${money / 10000}萬`
        }
    }
customElements.define('money-format', moneyFormat, { extends: 'p'});

// 你可以正常使用<p>標(biāo)簽煞聪,也可以通過is屬性來指定一個custom elements的名稱
<p>100000</p>                    // 在頁面上顯示出來的就是100000
<p is="money-format">100000</p>  // 在頁面上顯示出來的就是10萬

// 通過動態(tài)創(chuàng)建自定義內(nèi)置元素
cosnt moneyFormat = document.createElement('p', { is: 'money-format' })

這里的真正不同點(diǎn)在于元素定義類繼承的是HTMLParagraphElement接口(不同的元素繼承的接口都不一樣,<p>繼承的是HTMLParagraphElement逝慧,<ul>繼承的的是HTMLUListElement)昔脯,而不是HTMLElement。所以它擁有<p>元素所有的特性笛臣,以及在此基礎(chǔ)上我們定義的功能云稚,這是與獨(dú)立元素(standalone element)不同之處。這也是為什么我們稱它為 customized built-in元素沈堡,而不是一個autonomous元素静陈。

生命周期回調(diào)函數(shù)

生命周期 調(diào)用時機(jī)
constructor 創(chuàng)建元素的實(shí)例時調(diào)用,用于初始化狀態(tài)诞丽、設(shè)置事件偵聽器或創(chuàng)建影子dom
connectedCallback 當(dāng) custom elements首次被插入文檔DOM時
disconnectedCallback 當(dāng) custom elements從文檔DOM中刪除時
adoptedCallback 當(dāng) custom elements被移動到新的文檔時(document.adoptNode方法修改元素ownerDocument時觸發(fā))
attributeChangedCallback(name, oldValue, newValue) 當(dāng) custom element增加鲸拥、刪除、修改自身屬性時僧免,attributeChangedCallback()回調(diào)函數(shù)會執(zhí)行
static get observedAttributes() {return ['attribute']; } 如果需要在元素屬性變化后刑赶,觸發(fā) attributeChangedCallback()回調(diào)函數(shù),則需要監(jiān)聽這個屬性

懂衩!注意:想要attributeChangedCallback生效撞叨,必須設(shè)置observedAttributes來返回該標(biāo)簽需要監(jiān)聽哪些屬性的改變,兩者需要結(jié)合使用浊洞。

下面示例展示了:定義自定義元素加入文檔中牵敷、修改自定義元素的屬性以及從文檔中移除自定義屬性的生命周期觸發(fā)時機(jī)

<my-footer id="my-footer" img="./img/default.png" text="default footer"></my-footer>
<button id="change-attr-btn">change attribute</button>
<button id="remove-attr-btn">remove attribute</button>

定義自定義元素my-footer以及添加綁定事件觸發(fā)自定義元素的生命周期函數(shù)

// 自定義元素生命周期展示
class myFooter extends HTMLElement {
     constructor() {
         self = super();
         const style = document.createElement('style');
         style.textContent = `
             div {text-align:center;margin: 0 auto; }
         `;
         document.body.appendChild(style);
         this.updateText()
     }
     // 監(jiān)聽自定義元素的屬性:text,發(fā)生改變時會觸發(fā) attributeChangedCallback 函數(shù)
     static get observedAttributes() {
         return ['text'];
     }

     connectedCallback() { console.log('【connectedCallback】Custom element added to page.'); }

     disconnectedCallback() { console.log('【disconnectedCallback】Custom element removed from page.'); }

     adoptedCallback() { console.log('【adoptedCallback】Custom element moved to new page.'); }

     attributeChangedCallback(name, oldValue, newValue) {
         console.log('【attributeChangedCallback】', name, oldValue, newValue)
         console.log('【attributeChangedCallback】Custom element attributes changed.');
         this.updateText()
     }

     // 更新自定義元素內(nèi)容
     updateText() {
         const img = this.getAttribute('img')
         const text = this.getAttribute('text')
         this.innerHTML = `<div><img src="${img}"/></div><div>${text}</div>`
     }
 }
 // 定義自定義元素:my-footer
 customElements.define('my-footer', myFooter);

 const doc = document
 const myFooterEle = doc.getElementById('my-footer')
 const changeAttrBtn = doc.getElementById('change-attr-btn')
 const removeAttrBtn = doc.getElementById('remove-attr-btn')

 changeAttrBtn.onclick = function () {
     myFooterEle.setAttribute('text', 'change footer')
 }
 removeAttrBtn.onclick = function () {
     doc.body.removeChild(myFooterEle)
 }

刷新頁面:控制臺輸出

【attributeChangedCallback】 text null default footer
【attributeChangedCallback】Custom element attributes changed.
【connectedCallback】Custom element added to page.

點(diǎn)擊changeAttrBtn按鈕:

【attributeChangedCallback】 text default footer change footer
【attributeChangedCallback】Custom element attributes changed.

點(diǎn)擊removeAttrBtn按鈕:

【disconnectedCallback】Custom element removed from page.

css偽類

:defined

:defined表示任何已定義的元素沛申,包括任何瀏覽器內(nèi)置的標(biāo)準(zhǔn)元素以及已成功定義的自定義元素 (例如通過 CustomElementRegistry.define()方法定義的元素)劣领。

/* 選擇所有已定義的元素 */
:defined {
  font-style: italic;
}

/* 選擇指定自定義元素的任何實(shí)例 */
my-footer:defined {
  display: block;
}

/* 在你有一個復(fù)雜的自定義元素需要一段時間才能加載到頁面中時非常有用 —— 你可能想要隱藏元素的實(shí)例直到定義完成為止,這樣你就不會在頁面上出現(xiàn)一些難看的元素 */
my-footer:not(:defined) {
  display: none;
}

改進(jìn)

  1. 自定義元素都要動態(tài)通過JS生成DOM铁材,很繁瑣尖淘,并且不直觀,針對這個問題著觉,可以使用HTML templates中的<template> 和 <slot> 元素村生。
  2. 每個自定義標(biāo)簽下面都嵌套一堆DOM元素,如下饼丘,十分冗余趁桃,并沒有真正減負(fù);其次,沒有達(dá)到真正HTML和CSS封裝的目的卫病,容易受主文檔的影響油啤,針對這個問題,可以使用Shadow DOM(影子DOM)蟀苛。
 <!-- 瀏覽器devtools中展示的elements情況 -->
<my-footer id="my-footer" img="./img/default.png" text="default footer">
    <div><img src="./img/default.png"></div>
    <div>default footer</div>
</my-footer>

兼容性

image.png
image.png
image.png
image.png

可以看到主流瀏覽器對customElements接口Custom Elements標(biāo)簽都兼容益咬,IE不兼容,但可以通過polyfills去兼容(詳情文末)帜平,并且Customized built-in elements自定義內(nèi)置元素兼容性不佳幽告,部分瀏覽器不兼容。

Shadow DOM(影子DOM)

Shadow DOM主要解決了 DOM 樹的封裝問題裆甩。Shadow DOM允許在文檔(document)渲染時插入一棵DOM元素子樹冗锁,但是這棵子樹不在主DOM樹中,它與文檔的主 DOM 樹分開渲染嗤栓。

什么是Shadow DOM

Shadow DOM重要的特性就是封裝性冻河,它可以將DOM結(jié)構(gòu)、css樣式和行為隱藏起來抛腕,并與頁面上的其他代碼相隔開來芋绸,保證不同的部分不會混在一起媒殉,使代碼更加干凈整潔担敌。Shadow DOM允許將隱藏的 DOM 樹附加到常規(guī)的 DOM 樹中(被附加的這個常規(guī)的樹的節(jié)點(diǎn)叫shadow host),它以 shadow root 節(jié)點(diǎn)為起始根節(jié)點(diǎn)廷蓉,在這個根節(jié)點(diǎn)的下方全封,可以是任意元素,和普通的 DOM 元素一樣桃犬。你可以像操作常規(guī)DOM一樣操作Shadow DOM刹悴,不同的是Shadow DOM內(nèi)部的元素始終不會影響到它外部的元素,這為封裝提供了便利攒暇。

Shadow DOM在哪里

Shadow DOM離我們其實(shí)并不遙遠(yuǎn)土匀,平時我們在瀏覽器devtool工具里面看不到是因?yàn)槲覀儧]有開啟顯示shadow dom,打開方式:瀏覽器打開開發(fā)者調(diào)試工具-右上角“設(shè)置”圖標(biāo)-Preference-Elements-勾選“show user agent shadow DOM”


image.png

文檔結(jié)構(gòu):

<input type="text" placeholder="please add your plan" />
<video controls="" style="width: 300px;height:200px;">
    <source src="./video.mov">
</video>

devtools中的elements表現(xiàn):


image.png

可以看到形用,<input><video>標(biāo)簽下掛載著一個shadow-root就轧,但在常規(guī)調(diào)試工具中,是看不到里面的DOM結(jié)構(gòu)的田度,看到的是代碼中所寫的樣子妒御。這里shadow-root下的DOM就是Shadow DOM。Shadow DOM里面元素以及樣式不會影響外部镇饺,這也是封裝的意義所在乎莉。(也可以看到video里面的Shadow DOM都帶了pseudo屬性,這樣我們就可以通過偽類::-webkit-media-controls去改變video的樣式)

Shadow DOM與常規(guī)DOM的關(guān)系:

Document Tree
      |
  Shadow host
------------------ 邊界 
      |
  Shadow root
      |
  Shadow Tree

image.png
  • Shadow host:一個常規(guī) DOM節(jié)點(diǎn),Shadow DOM 會被附加到這個節(jié)點(diǎn)上惋啃。
  • Shadow root:Shadow tree 的根節(jié)點(diǎn)哼鬓。
  • Shadow boundary:Shadow DOM結(jié)束的地方,也是常規(guī) DOM 開始的地方边灭。
  • Shadow tree:Shadow DOM 內(nèi)部的DOM樹魄宏。

創(chuàng)建Shadow DOM

Element.attachShadow() 方法給指定的元素掛載一個Shadow DOM,并且返回對 ShadowRoot 的引用存筏。
不是每一種類型的元素都可以附加到shadow root(影子根)下面宠互。出于安全考慮,一些元素不能使用 shadow DOM(例如<a>)椭坚,以及許多其他的元素予跌。

基本語法:var shadowroot = element.attachShadow(shadowRootInit<Object>);

參數(shù):
shadowRootInit(Object):

  • mode:指定 Shadow DOM 樹封裝模式的字符串
    • open: 可從外部訪問shadow root元素的根節(jié)點(diǎn),如Element.shadowRoot
    • closed: 不可以從外部獲取 Shadow DOM,Element.shadowRoot返回null
  • delegatesFocus: 焦點(diǎn)委托
    • 一個布爾值, 當(dāng)設(shè)置為 true 時, 指定減輕自定義元素的聚焦性能問題行為.
    • 當(dāng)shadow DOM中不可聚焦的部分被點(diǎn)擊時, 讓第一個可聚焦的部分成為焦點(diǎn), 并且shadow host(影子主機(jī))將提供所有可用的 :focus 樣式.

返回值:返回一個 ShadowRoot 對象或者 null善茎。

提示:也可以選擇host.createShadowRoot()的方法創(chuàng)建Shadow Root掛載Shadow Tree

應(yīng)用

結(jié)合custom elements券册,創(chuàng)建一個掛載Shadow DOM的自定義元素

class myFooter extends HTMLElement {
    constructor() {
        // 必須先調(diào)用super方法,類的構(gòu)造函數(shù)constructor總是先調(diào)用super()來建立正確的原型鏈繼承關(guān)系垂涯。
        self = super();
        // 自定義元素掛載一個Shadow DOM
        this._shadowRoot = this.attachShadow({ mode: 'open' })

        const style = document.createElement('style');
        style.textContent = `img,div {text-align:center;margin: 0 auto;display: block};`;

        const img = this.getAttribute('img')
        const text = this.getAttribute('text')
        const imgNode = document.createElement('img')
        imgNode.setAttribute('src', img)
        const textNode = document.createElement('div')
        textNode.textContent = text

        this._shadowRoot.appendChild(imgNode);
        this._shadowRoot.appendChild(textNode);
        this._shadowRoot.appendChild(style);
    }
}
customElements.define('my-footer', myFooter);

調(diào)用:

<my-footer id="my-footer" img="./img/default.png" text="default footer"></my-footer>

DOM結(jié)構(gòu):


image.png

可以看到<my-footer>下多了一個#shadow-root烁焙,展開#shadow-root才看到具體的shadow tree里面的結(jié)構(gòu),并且shadow dom中定義的樣式不會影響主DOM中樣式耕赘,當(dāng)把調(diào)試工具中的勾選去掉“show user agent shadow DOM”后骄蝇,在文檔上就看不到<my-footer>內(nèi)具體的DOM樹了,這就跟<video>標(biāo)簽一樣的表現(xiàn)形式了操骡。

因?yàn)檫@里設(shè)置了mode:'open'九火,所以可以在外面獲取shadow root下的根節(jié)點(diǎn)。

console.log(document.getElementById('my-footer').shadowRoot)

// 控制臺打印:
#shadow-root(open)
<img src="./img/default.png">
<div>default footer</div>

當(dāng)mode: 'closed'時册招,則外層無法獲取Shadow DOM岔激,<video>標(biāo)簽就是設(shè)置了closed屬性

console.log(document.getElementById('my-footer').shadowRoot) // null

從組件的可擴(kuò)展性與靈活些來說,建議設(shè)置open屬性是掰,方便使用者進(jìn)行修改擴(kuò)展虑鼎。

  • mode="open": Ele.shadowRoot = #shadow-root(open)....
  • mode="closed": Ele.shadowRoot = null

Event.composed && Event.composedPath()

屬性/接口 返回值 作用
Event.composed Boolean 若返回值為true,表明當(dāng)事件到達(dá) shadow DOM 的根節(jié)點(diǎn)(也就是 shadow DOM 中事件開始傳播的第一個節(jié)點(diǎn))時键痛,事件可以從 shadow DOM 傳遞到一般 DOM炫彩。當(dāng)然,事件要具有可傳播性散休,即該事件的 bubbles 屬性必須為 true媒楼。如果屬性值為 false,那么事件將不會跨越 shadow DOM 的邊界傳播戚丸。
Event.composedPath() 一個 EventTarget對象數(shù)組划址,表示將在其上調(diào)用事件偵聽器的對象扔嵌。 返回事件路徑,如果影子根節(jié)點(diǎn)被創(chuàng)建并且ShadowRoot.mode是關(guān)閉的夺颤,那么該路徑不包括影子樹中的節(jié)點(diǎn)痢缎。

對文檔設(shè)置事件監(jiān)聽

document.querySelector('html').addEventListener('click', function (e) {
    console.log(e.bubbles)        
    console.log(e.composed);      
    console.log(e.composedPath());
}, false);

點(diǎn)擊<my-footer>中的<img>標(biāo)簽,得到如下結(jié)論:

  1. 無論ShadowRoot.mode是open或closed世澜,e.composed都返回true独旷,因?yàn)?code>click事件始終能跨越陰影邊界傳播。
  2. 不同在于e.composedPath():
    • ShadowRoot.mode=open時寥裂,e.composedPath()返回[img, document-fragment, my-footer#my-footer, body, html, document, Window]嵌洼,事件能到達(dá)Shadow DOM里面的元素
    • ShadowRoot.mode=closed時,e.composedPath()返回[my-footer#my-footer, body, html, document, Window]封恰,事件不能到達(dá)Shadow DOM中麻养,監(jiān)聽器只會捕獲到<my-footer> 元素本身

當(dāng)一個事件從 Shadow DOM 冒泡時,它的target會被調(diào)整以保持 Shadow DOM 提供的封裝诺舔。也就是說鳖昌,事件被重新定位,使其看起來像是來自組件而不是影子 DOM 中的內(nèi)部元素低飒。有些事件甚至不會從 shadow DOM 傳播出去许昨。

以下為能跨越陰影邊界的事件:

  • Focus Events: blur, focus, focusin, focusout
  • Mouse Events: click, dblclick, mousedown, mouseenter, mousemove, etc.
  • Wheel Events: wheel
  • Input Events: beforeinput, input
  • Keyboard Events: keydown, keyup
  • Composition Events: compositionstart, compositionupdate, compositionend
  • DragEvent: dragstart, drag, dragend, drop, etc.

有些情況下事件綁定不進(jìn)行重定向而直接被干掉,以下事件會被阻塞到根節(jié)點(diǎn)且不會被原有 DOM 結(jié)構(gòu)監(jiān)聽褥赊,被阻塞的事件:

  • abort
  • error
  • select
  • change
  • load
  • reset
  • resize
  • scroll

可以參考以下實(shí)例解釋:

<body>
    <input id="normal-text" type="text" value="I'm normal text">
    <input-item text="姓名:"></input-item>
</body>
class inputItem extends HTMLElement {
    constructor() {
        self = super();
        this._shadowRoot = this.attachShadow({ mode: 'open' })

        const text = this.getAttribute('text')
        const textNode = document.createElement('div')
        textNode.textContent = text
        const inputNode = document.createElement('input')
        inputNode.value = 'I am shadow dom text'

        this._shadowRoot.appendChild(textNode);
        this._shadowRoot.appendChild(inputNode);
    }
}
customElements.define('input-item', inputItem);

document.addEventListener('change', function (e) {
    console.log('[change event target]', e.target);
    console.log('[change event bubbles]', e.bubbles)         
    console.log('[change event composed]', e.composed);       
    console.log('[change event composedPath]', e.composedPath());

});

document.addEventListener('click', function (e) {
    console.log('[click event target]', e.target)
    console.log('[click event bubbles]', e.bubbles)         
    console.log('[click event composed]', e.composed);       
    console.log('[click event composedPath]', e.composedPath()); 
});

當(dāng)點(diǎn)擊正常input標(biāo)簽并修改value的值糕档,事件冒泡到document,click和change事件能夠成功被監(jiān)聽崭倘∫硭辏可以看到change事件中composed返回false,devtool中開啟“show Shadow-root”的話可以看到其實(shí)input標(biāo)簽下也是掛載了一個Shadow DOM司光,所以change事件不能從Shadow DOM中傳遞回一般的DOM,input是一個封裝好的元素悉患,保證了其封裝性残家,change事件的target就是input元素本身。

[click event target] <input id="normal-text" type="text" value="I'm normal text">…</input>
[click event bubbles] true
[click event composed] true
[click event composedPath] (5) [input#normal-text, body, html, document, Window]

[change event target] <input id="normal-text" type="text" value="I'm normal text">…</input>
[change event bubbles] true
[change event composed] false
[change event composedPath] (5) [input#normal-text, body, html, document, Window]

當(dāng)點(diǎn)擊Shadow DOM中的input并修改值售躁,change事件冒泡到Shadow Root就會停止向上坞淮,所以綁定在document上的事件不會被觸發(fā),只有click事件能冒泡到document被觸發(fā)陪捷,并且控制臺上打印target的是宿主對象host回窘,即<input-item>元素。這是因?yàn)橛白庸?jié)點(diǎn)上的事件必須重定向市袖,否則這將破壞封裝性啡直。如果事件繼續(xù)指向 #shadow-root 里面的元素烁涌,那么任何人都可以在 Shadow DOM 里破壞其內(nèi)部結(jié)構(gòu),這就違背了其封裝性的初衷酒觅。

[click event target] <input-item text="姓名:">…</input-item>
[click event bubbles] true
[click event composed] true
[click event composedPath] (7) [input, document-fragment, input-item, body, html, document, Window]

使用自定義事件

在影子樹中的內(nèi)部節(jié)點(diǎn)上觸發(fā)的自定義DOM事件不會冒泡出影子邊界撮执,除非該事件是使用 composed: true 標(biāo)志創(chuàng)建的

使用 new Event()

new Event(eventName<String>, eventInit<Object<{bubbles: false, cancelable: false, composed: fakse}>>)

eventInit(Object),可選

  • "bubbles"舷丹,可選抒钱,Boolean類型,默認(rèn)值為 false颜凯,表示該事件是否冒泡谋币。
  • "cancelable",可選症概,Boolean類型瑞信,默認(rèn)值為 false, 表示該事件能否被取消穴豫。
  • "composed"凡简,可選,Boolean類型精肃,默認(rèn)值為 false秤涩,指示事件是否會在影子DOM根節(jié)點(diǎn)之外觸發(fā)偵聽器。
class myFooter extends HTMLElement {
    constructor() {
        self = super();
        this._shadowRoot = this.attachShadow({ mode: 'open' })

        // 省略中間代碼...
    this._shadowRoot.addEventListener('click', this._changeText.bind(this))
    }

    _changeText() {
        // 向一個指定的事件目標(biāo)派發(fā)一個事件
        this._shadowRoot.dispatchEvent(new Event('text-change', { bubbles: true, composed: true }));
    }
}
customElements.define('my-footer', myFooter);

document.getElementById('my-footer').addEventListener('text-change', function(e) {
    // 當(dāng)事件定義參數(shù)不是`composed: true`時不會觸發(fā)該事件
    // 當(dāng)事件定義參數(shù)`composed: true`時司抱,輸出:【text-change event】 Event {isTrusted: false, type: "text-change", target: my-footer#my-footer, currentTarget: my-footer#my-footer, eventPhase: 2, …}
    console.log('【text-change event】', e)
});

使用 new CustomEvent()

相比于new Event()筐眷,new CustomEvent()可以自定義派發(fā)的數(shù)據(jù)。

new CustomEvent(eventName<String>, eventInit<Object<{bubbles: false, cancelable: false, detail: any}>>)

eventInit(Object)可選

  • "bubbles"习柠,可選匀谣,Boolean類型,默認(rèn)值為 false资溃,表示該事件是否冒泡武翎。
  • "cancelable",可選溶锭,Boolean類型宝恶,默認(rèn)值為 false, 表示該事件能否被取消趴捅。
  • "detail"垫毙,可選,any類型拱绑,默認(rèn)值為 null综芥,當(dāng)事件初始化時傳遞的數(shù)據(jù)
class myFooter extends HTMLElement {
    constructor() {
        self = super();
        this._shadowRoot = this.attachShadow({ mode: 'open' })

        // 省略中間代碼...
        this._shadowRoot.addEventListener('click', this._changeText.bind(this))
    }

    _changeText() {
        // 這里不在this._shadowRoot上派發(fā),而是在<my-footer>上派發(fā)猎拨,因?yàn)镃ustomEvent事件默認(rèn)composed是false膀藐,所以監(jiān)聽事件不會觸發(fā)
        this.dispatchEvent(new CustomEvent('text-change', { detail: { text: 'change text' } }));
    }
}
customElements.define('my-footer', myFooter);

document.getElementById('my-footer').addEventListener('text-change', function(e) {
    console.log('【text-change event】', e)
});

輸出:

> CustomEvent {isTrusted: false, detail: null, type: "text-change", target: my-footer#my-footer, currentTarget: my-footer#my-footer, …}
    bubbles: false
    cancelBubble: false
    cancelable: false
    composed: false
    currentTarget: null
    defaultPrevented: false
    detail: {text: "change text"}   => 派發(fā)過來的數(shù)據(jù)
    eventPhase: 0
    isTrusted: false
    path: (5) [my-footer#my-footer, body, html, document, Window]
    returnValue: true
    srcElement: my-footer#my-footer
    target: my-footer#my-footer
    timeStamp: 1882.7999997138977
    type: "text-change"
    __proto__: CustomEvent

css偽類

只能在Shadow DOM內(nèi)使用屠阻,在之外使用時,沒有任何效果消请。

  • :host:選擇包含其自定義元素內(nèi)部的shadow DOM的根元素栏笆。
  • :host():選擇包含使用這段 CSS 的 Shadow DOM 的影子宿主,只能選擇host元素
  • :host-context():選擇shadow DOM 中shadow host臊泰,這個偽類內(nèi)可以寫關(guān)于該shadow host的CSS規(guī)則蛉加。 在DOM 層級中,括號中的選擇器參數(shù)必須和shadow host 的祖先相匹配缸逃≌爰ⅲ可以用于主體化定制,典型的使用方法是后代選擇器表達(dá)式需频,例如只選擇在<h1>內(nèi)的自定義元素的實(shí)例丁眼。
/* 選擇一個 shadow root host */
:host {
  font-size: 16px;
}

/* 選擇陰影根元素,僅當(dāng)它與選擇器參數(shù)匹配 */
:host(#my-footer) {
    font-weight: bold;
}

/* 選擇陰影根元素昭殉,僅當(dāng)它與選擇器參數(shù)匹配 */
:host(my-footer) {
    color: green;
}

/* host元素下shadow dom */
:host(my-footer) #my-footer-text{
    color: red;
}

/* 選擇了一個 shadow root host, 當(dāng)且僅當(dāng)這個 shadow root host 是括號中選擇器參數(shù)(h1)的后代 */
:host-context(h1) {
    color: blue;
}

如果你想使用者可以從外部修改自定義元素的樣式搭独,那么開發(fā)者可以在自定義元素內(nèi)部預(yù)埋可供使用者覆蓋的css的屬性橡庞,這樣可做到自定義樣式浙芙。

 <!-- 在Shadow DOM內(nèi)通過var預(yù)埋可覆蓋樣式屬性 -->
    :host {
        font-size: 20px;
        background-color: var(--my-footer-bg-color, #fff);
    }

<!-- 在html文檔中 -->
        /* 選擇指定自定義元素的任何實(shí)例 */
        my-footer:defined {
            --my-footer-bg-color: #eee;
        }

兼容性

image.png
image.png

HTML templates(HTML模板)

<template><slot> 元素可定義可重用的HTML結(jié)構(gòu)喜命,減少使用js動態(tài)創(chuàng)建,也更加可視化與靈活性乾蓬。

template

template模板中可以定義DOM結(jié)構(gòu)惠啄,但是瀏覽器不會渲染,可以在custom elements中利用任内。

定義template內(nèi)容

<body> 
    <template id="my-footer-template">
        
        <div><img  id="my-footer-img" src="./img/default.png"></div>
        <div id="my-footer-text">default footer</div>
    </template>

    <my-footer id="my-footer" img="./img/default.png" text="default footer"></my-footer>
</body>

創(chuàng)建custom elements:

class myFooter extends HTMLElement {
    constructor() {
        // 必須先調(diào)用super方法撵渡,類的構(gòu)造函數(shù)constructor總是先調(diào)用super()來建立正確的原型鏈繼承關(guān)系。
        self = super();
        this._shadowRoot = this.attachShadow({ mode: 'open' })
        const template = document.getElementById('my-footer-template')
        // 拷貝template的內(nèi)容添加到shadow dom上
        this._shadowRoot.appendChild(template.content.cloneNode(true))

        this.$img = this._shadowRoot.querySelector('#my-footer-img')
        this.$text = this._shadowRoot.querySelector('#my-footer-text')
        const img = this.getAttribute('img')
        const text = this.getAttribute('text')
        this.$img.setAttribute('src', img)
        this.$text.textContent = text
    }
}
customElements.define('my-footer', myFooter);

在定義好的template中可以清晰地看到將要添加到Shadow DOM中的文檔結(jié)構(gòu)死嗦,就像真正寫在頁面中一樣趋距。將template中的內(nèi)容拷貝到Shadow DOM上,用到了Node.cloneNode()接口越走。

Node.cloneNode()

Node.cloneNode()方法返回調(diào)用該方法的節(jié)點(diǎn)的一個副本.

語法:const dupNode = node.cloneNode(deep);

  • node: 將要被克隆的節(jié)點(diǎn)
  • dupNode: 克隆生成的副本節(jié)點(diǎn)
  • deep:可選棚品,是否采用深度克隆,如果為true廊敌,則該節(jié)點(diǎn)的所有后代節(jié)點(diǎn)也都會被克隆门怪;如果為false骡澈,則只克隆該節(jié)點(diǎn)本身
var dupNode = node.cloneNode(deep);
// 拷貝節(jié)點(diǎn)并不屬于當(dāng)前文檔樹的一部分,也就是說掷空,它沒有父節(jié)點(diǎn)肋殴,需要Node.appendChild()或其他類似的方法將拷貝的節(jié)點(diǎn)添加到文檔中
Node.appendChild(dupNode)

slot插槽

我們在Vue中創(chuàng)建組件時經(jīng)常會使用到<slot>標(biāo)簽囤锉,該標(biāo)簽主要能增加插入元素的靈活性。這里也是如此护锤,slot由其name屬性標(biāo)識官地,并允許在模板中定義占位符,當(dāng)在文檔中通過定義屬性slot=slotName使用時烙懦,可以在占位符中填充任何HTML標(biāo)記片段驱入。

定義template及slot

<body> 
    <template id="my-footer-template">
        
        <div><img  id="my-footer-img" src="./img/default.png"></div>
        <div id="my-footer-text">default footer</div>
        <div><slot name="footer-after"></slot></div>
    </template>

    <my-footer id="my-footer" img="./img/default.png" text="default footer">
        <span slot="footer-after">This is footer-after text.</span>
    </my-footer>
</body>

頁面上渲染出來的結(jié)構(gòu)為:


image.png

可以看出通過插槽的方式,可以定義占位符氯析,后續(xù)復(fù)用自定義元素的時候亏较,既可以有復(fù)用性的一面,又有自定義內(nèi)容的一面掩缓,兩者結(jié)合雪情,完美。

兼容性

image.png
image.png

應(yīng)用

看完上文你辣,應(yīng)該多多少少也了解了web組件的特性及作用巡通,這里總結(jié)下web組件可以用來做什么:

  • 構(gòu)建不依賴任何框架(如Vue,React舍哄,Angular等)的可重用組件庫
  • 創(chuàng)建具有封裝性的自定義組件標(biāo)簽宴凉,隔離HTML與CSS
  • 可用于掛載一個獨(dú)立功能的Shadow DOM,減少dom深層嵌套及與主文檔的相互影響

web組件構(gòu)建框架

目前有一些框架專門用來構(gòu)建web組件蠢熄,可以更加方便我們?nèi)?gòu)建組件庫跪解。

  • Polymer library: (published by Google in 2013) The Polymer library provides a set of features for creating custom elements.
  • Lit: (The Polymer library is in maintenance mode. For new development, we recommend Lit) Lit is a simple library for building fast, lightweight web components.
    At Lit's core is a boilerplate-killing component base class that provides reactive state, scoped styles, and a declarative template system that's tiny, fast and expressive.
  • LitElement: LitElement is now part of the Litlibrary
  • Vue3.2+: defineCustomElement api.
  • ReactJS: Web Components
  • AngularJS: createCustomElement api.

兼容

可在下面文檔中去引入polyfills文件去兼容web組件

最后

看完上文,大家應(yīng)該對video標(biāo)簽為什么在不同瀏覽器展示的樣式不同签孔,以及為什么不同瀏覽器兼容性不一致這些問題都有了很好的理解叉讥。未來,可能會有更多瀏覽器內(nèi)置的自定義元素出現(xiàn)饥追,需要各自瀏覽器去兼容图仓,這樣我們就能用到更多通用的標(biāo)簽。

最近vue也發(fā)布了3.2.0版本但绕,該版本全局api中就新增了defineCustomElement接口救崔,并且google也在不斷維護(hù)其web組件創(chuàng)建框架,各種瀏覽器也在不斷兼容web組件特性捏顺,未來我覺得web組件不會被埋沒六孵,而是會讓更多人了解。

但由于我們現(xiàn)在用的大多框架都在virtual DOM層面上操作DOM幅骄,而web組件是脫離框架的劫窒,這使得需要純JS去操作DOM,這可能需要回到最初的虛擬DOM前的時代去創(chuàng)建拆座,可能會帶來開發(fā)上的繁瑣主巍,這也是一個值得思考與改進(jìn)的問題冠息。

更多閱讀

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市孕索,隨后出現(xiàn)的幾起案子逛艰,更是在濱河造成了極大的恐慌,老刑警劉巖搞旭,帶你破解...
    沈念sama閱讀 222,000評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件散怖,死亡現(xiàn)場離奇詭異,居然都是意外死亡选脊,警方通過查閱死者的電腦和手機(jī)杭抠,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,745評論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來恳啥,“玉大人偏灿,你說我怎么就攤上這事《鄣模” “怎么了翁垂?”我有些...
    開封第一講書人閱讀 168,561評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長硝桩。 經(jīng)常有香客問我沿猜,道長,這世上最難降的妖魔是什么碗脊? 我笑而不...
    開封第一講書人閱讀 59,782評論 1 298
  • 正文 為了忘掉前任啼肩,我火速辦了婚禮,結(jié)果婚禮上衙伶,老公的妹妹穿的比我還像新娘祈坠。我一直安慰自己,他們只是感情好矢劲,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,798評論 6 397
  • 文/花漫 我一把揭開白布赦拘。 她就那樣靜靜地躺著,像睡著了一般芬沉。 火紅的嫁衣襯著肌膚如雪躺同。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,394評論 1 310
  • 那天丸逸,我揣著相機(jī)與錄音蹋艺,去河邊找鬼。 笑死黄刚,一個胖子當(dāng)著我的面吹牛车海,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播隘击,決...
    沈念sama閱讀 40,952評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼侍芝,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了埋同?” 一聲冷哼從身側(cè)響起州叠,我...
    開封第一講書人閱讀 39,852評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎凶赁,沒想到半個月后咧栗,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,409評論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡虱肄,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,483評論 3 341
  • 正文 我和宋清朗相戀三年致板,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片咏窿。...
    茶點(diǎn)故事閱讀 40,615評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡斟或,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出集嵌,到底是詐尸還是另有隱情萝挤,我是刑警寧澤,帶...
    沈念sama閱讀 36,303評論 5 350
  • 正文 年R本政府宣布根欧,位于F島的核電站怜珍,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏凤粗。R本人自食惡果不足惜酥泛,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,979評論 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望嫌拣。 院中可真熱鬧柔袁,春花似錦、人聲如沸亭罪。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,470評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽应役。三九已至情组,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間箩祥,已是汗流浹背院崇。 一陣腳步聲響...
    開封第一講書人閱讀 33,571評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留袍祖,地道東北人底瓣。 一個月前我還...
    沈念sama閱讀 49,041評論 3 377
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親捐凭。 傳聞我的和親對象是個殘疾皇子拨扶,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,630評論 2 359

推薦閱讀更多精彩內(nèi)容