1. 你真的了解Web Component嗎?

        共 29720字,需瀏覽 60分鐘

         ·

        2021-09-25 16:51

        大廠技術(shù)  堅(jiān)持周更  精選好文

        為什么使用框架?

        對(duì)框架的理解

        作為現(xiàn)代前端開發(fā)者,擁抱框架是生存的不二法則,有些人一入場(chǎng)便投身框架的海洋,有些人則有幸見證過變革,從原生,到j(luò)q,到各種框架大行其道的今天。而當(dāng)前,國內(nèi)占領(lǐng)市場(chǎng)份額最多的要數(shù)vue、react和angular,他們都有著各自的特點(diǎn),這也是它們一路走來的立足之本。

        那么作為使用者的我們,在使用框架高效處理業(yè)務(wù)的同時(shí),對(duì)框架本身也是需要一定程度的理解,以此來輔助我們更好的學(xué)習(xí)、了解和應(yīng)用框架。下面有一個(gè)表格,內(nèi)容提煉自尤雨溪本人對(duì)三大框架的對(duì)比看法,也許可以一定程度提升我們對(duì)框架的認(rèn)知。


        職責(zé)范圍的意義:

        • 大的職責(zé)范圍讓開發(fā)者習(xí)慣把問題拋給框架,
        • 小的職責(zé)范圍讓開發(fā)者習(xí)慣把問題拋給社區(qū)。

        框架的優(yōu)勢(shì)

        基于上述框架間的差異化,我們可以看出框架各自不同的設(shè)計(jì)、發(fā)展和其衍生出的生態(tài)其實(shí)都是源自于最初各自對(duì)于職責(zé)范圍界定的不同而來。但盡管差異不小,它們依然存在著共性,而共性,正是源于框架本身存在的意義和目標(biāo)。

        回頭審視,你會(huì)發(fā)現(xiàn)所有的框架其實(shí)都有共同的特點(diǎn)和目標(biāo),就是基于原生,然后更高的效率,更棒的性能,更好的差異抹平。

        但我們需要正確理解這句話,這并不意味著框架的指標(biāo)就優(yōu)于原生,而是說,因?yàn)橛辛丝蚣?,我們不用再手寫不依賴業(yè)務(wù)場(chǎng)景的數(shù)據(jù)-視圖的綁定,不用再手動(dòng)抹平平臺(tái)或?yàn)g覽器之間的差異,不用再陷入操作dom的同時(shí)還要兼顧性能苦惱??梢哉f框架提高了開發(fā)者開發(fā)和實(shí)現(xiàn)功能的各項(xiàng)下限,讓快速開發(fā)和基礎(chǔ)性能之間更好的平衡。我們以react和vue為例,這兩大框架所帶來的優(yōu)勢(shì)包括但不限于:

        • 數(shù)據(jù)綁定(單/雙向)
        • 組件化開發(fā)(各種鉤子/生命周期/作用域隔離)
        • 虛擬dom(diff算法)以及路由等。
        • ......

        但這些優(yōu)勢(shì)不是憑空而來,就像vue的雙向綁定,從使用object.defineProperty轉(zhuǎn)為使用proxy,這種類似的實(shí)現(xiàn)或者說轉(zhuǎn)變,核心之處都需要js語法以及瀏覽器的原生支持。因?yàn)閣eb應(yīng)用最終都是要運(yùn)行在宿主--瀏覽器上的,所以制定規(guī)范的各大瀏覽器廠商以及提供原生api支持的瀏覽器環(huán)境才是王道,而框架不是。我們之所以需要引入各類的框架、工具庫去實(shí)現(xiàn)各種優(yōu)秀的設(shè)計(jì)與思想,比如組件化,本質(zhì)上是因?yàn)樵粗苯犹峁?duì)應(yīng)的方式或是api,所以才需要框架去構(gòu)建棋盤之上的又一層規(guī)則體系,來實(shí)現(xiàn)開發(fā)者的訴求。

        而框架這種在瀏覽器原生規(guī)則之上又一層較高程度的封裝,在帶來便利高效的同時(shí),不可避免的帶來兩個(gè)缺陷:

        • 性能的下降,這也是為什么上面說有時(shí)原生的直接操作指標(biāo)要優(yōu)于框架。下面是一些關(guān)于處理dom的react vs js的對(duì)比:

        (圖1:桌面chrome; 圖2:平板chrome; 圖3:移動(dòng)端chrome;)(下圖:桌面chrome下react vs js 內(nèi)存比較)

        • 框架環(huán)境的隔離,例如vue的組件庫沒辦法很好的銜接在react的項(xiàng)目中(也許你會(huì)說vuera或微前端,但事實(shí)上ROI和性能并不好,開發(fā)和維護(hù)的成本較高)。

        那么如果原生可以提供某些api,是不是就可以一定程度上替代框架的某些功能,在擁有便利高效的同時(shí),跨平臺(tái)、跨框架的使用,還能較大限度的保持原生的性能?

        這就是接下來要聊到的是web component和其所能帶來的可能甚至是變革。

        認(rèn)識(shí)web component

        web component

        狹義的來說,web component是瀏覽器環(huán)境提供的一些新的原生支持的api和模版。廣義的說,它是一套可以支持原生實(shí)現(xiàn)組件化的技術(shù)。從MDN的描述中可以看到,web component的誕生,是為了解決代碼復(fù)用、組件自定義、復(fù)用管理等問題。

        回看上文中,我們對(duì)框架優(yōu)勢(shì)的分析羅列,可以發(fā)現(xiàn)解決這些開發(fā)痛點(diǎn)的方案早已存在,也就是與之對(duì)應(yīng)的框架優(yōu)勢(shì)中的組件化。那么根據(jù)上面的分析,既然原生支持了,是不是意味著可以顛覆框架?這種想法是有些沖動(dòng)的,單純依靠原生的api去顛覆框架是不現(xiàn)實(shí)的,能顛覆框架的也必須是框架,因?yàn)槊恳粋€(gè)框架都意味著對(duì)應(yīng)的生態(tài)(路由管理、狀態(tài)管理、dom性能優(yōu)化管理等)。如果有一天,當(dāng)前框架中的大部分優(yōu)秀的設(shè)計(jì)與思想被原生環(huán)境所吸收并支持,那么在此基礎(chǔ)上衍生的框架,才能真正具備替代當(dāng)前三大框架的能力,成為前端唯一一類框架。

        而現(xiàn)在,我們雖然還是無法舍棄框架擁抱原生,但是我們可以將其中的一部分進(jìn)行替代,使之擁有框架提供的優(yōu)勢(shì),又能避免因框架而導(dǎo)致的缺陷。

        原生組件化能否替代框架組件化?

        我們先來看看組件化的特點(diǎn):

        • 高內(nèi)聚,低耦合
        • 標(biāo)記鮮明易維護(hù)
        • 塊狀接口易擴(kuò)展

        再看看依據(jù)組件化的規(guī)范,框架組件化提供給我們最直觀的體驗(yàn):

        • 高效復(fù)用
        • 作用域及樣式隔離
        • 自定義開發(fā)
        • 鉤子函數(shù)(生命周期)
        • ......

        最后我們來看看web component給我們提供了什么:

        • Custom elements:自定義元素,通過使用對(duì)應(yīng)的api,用戶可以在不依賴框架的情況下,開發(fā)原生層面的自定義元素,最關(guān)鍵的是,它將包含獨(dú)立的生命周期,以及提供了自定義屬性的監(jiān)聽。這就意味著它也同樣具備了較高的可操作性。
        • Shadow DOM:影子dom(最大的特點(diǎn)是不暴露給全局),你可以通過對(duì)應(yīng)的api,將shadow dom附加給你的自定義元素,并控制其相關(guān)功能。利用shadow dom的特性,起到隔離的作用,使特性保密,不用再擔(dān)心所編寫的腳本及樣式與文檔其他部分沖突。
        • HTML模版:通過<template/><slot/>去實(shí)現(xiàn)內(nèi)容分發(fā)?;蛘吣憧梢曰貞浺幌聉ue的插槽(slot)和react的props.children。但事實(shí)上,真的是vue最先創(chuàng)立的slot嗎?看下面~

        從上述這些原生api所提供給我們的種種特性,說明web component同樣可以滿足我們對(duì)組件的自定義及復(fù)用、與文檔其他部分隔離、生命周期的鉤子函數(shù),甚至是內(nèi)容分發(fā)等這些訴求。

        那么至少從理論的角度上說,web component是完全有能力替代框架組件化的,這意味著開發(fā)者可以在不使用的框架的前提下進(jìn)行組件化開發(fā),而且開發(fā)出的組件可以無縫嵌入使用了框架的項(xiàng)目中。有趣的是在最新發(fā)布的vue3.2中,也初步引入了對(duì)于web component的使用:

        兼容性

        作為開發(fā)者,面對(duì)新的強(qiáng)大的api,在充滿熱情的同時(shí),更需要關(guān)注其可用性和普及范圍。我們可以通過can i use去查看它的兼容性:https://caniuse.com/?search=web%20component。從中我們可以看到:

        1. Custom elements兼容性

        2. Shadow DOM兼容性

        3. HTML templates兼容性

        自主定制元素和自定義內(nèi)置元素

        在Custom elements兼容性的描述中,我們看到兩個(gè)概念,如下:

        • 自主定制元素:獨(dú)立元素;它們不繼承自內(nèi)置的 HTML 元素。
        • 自定義內(nèi)置元素:這些元素繼承并擴(kuò)展了內(nèi)置的 HTML 元素。

        那么這里怎么去理解自主定制元素自定義內(nèi)置元素?我們可以從具體的code實(shí)現(xiàn)上進(jìn)行觀察:

        • 自主定制元素
        js: 
        ... 
        customElements.define('custom-elements'class)
        ... 
        html: 
        <body> 
        ... 
        <custom-elements></custom-elements> 
        ... 
        </body>
         
        • 自定義內(nèi)置元素
        js:
        ...
        customElements.define('custom-elements'class{ extends: 'p' });
        ...
        html:
        <body>
        ...
        <p is="custom-elements"></p>
        ...
        </body>

        可以看到從聲明上是沒有太大區(qū)別的,都是通過 customElements.define 去定義聲明,并且需要一個(gè) class 去構(gòu)建內(nèi)部的生命周期與屬性監(jiān)聽。區(qū)別之處在于自定義內(nèi)置元素需要在后面的配置項(xiàng)中設(shè)置要繼承的內(nèi)置HTML 元素(指原生的元素)。

        而最大的區(qū)別是在于使用上,自主定制元素其實(shí)就是一個(gè)完整的自定義組件,可以讓我們?cè)诓灰蕾嚾魏慰蚣艿那疤嵯聦?shí)現(xiàn)組件化。而自定義內(nèi)置組件,可以理解為是對(duì)所繼承的原生元素的改造(如上述code呈現(xiàn),聲明定義自定義組件時(shí),指定繼承的原生元素,后續(xù)使用該原生元素時(shí),通過is屬性引用聲明的自定義組件,就可以改造該原生元素,使其擁有生命周期、自定義組件和作用域隔離的功能)。

        web component api的使用

        自定義組件的聲明和使用

        所依賴的主要接口是CustomElementRegistry,該接口提供了,用作支持自定義組件的使用和聲明:

        • window.customElements.define。

        該方法用來聲明自定義組件,接受3個(gè)參數(shù),無返回值:

        1. name:將要全局注冊(cè)的自定義組件名字(必須是中劃線的形式)。
        2. constructor:一個(gè)類,如果聲明的是自主定制元素,則必須繼承自HTMLElement;如果聲明的是自定義內(nèi)置元素,則必須繼承它將要擴(kuò)展的原生元素所屬的類(如要擴(kuò)展div,那就必須繼承HTMLDivElement)。并且類的構(gòu)造函數(shù)中,必須執(zhí)行super。
        3. options:一個(gè)可選的配置對(duì)象,只有在聲明自定義內(nèi)置元素時(shí)使用,且當(dāng)前只有一個(gè)配置項(xiàng)extends,值為將要擴(kuò)展的原生元素的標(biāo)簽名。

        聲明示例:

        //自主定制元素 
        class CustomEle extends HTMLElement 
          constructor() { 
            super(); 
            ... 
          } 

        customElements.define('custom-ele', CustomEle); 
         
        //自定義內(nèi)置元素,如果要擴(kuò)展div的話 
        class CustomEleBuiltIn extends HTMLDivElement 
          constructor() { 
            super(); 
            ... 
          } 

        customElements.define('custom-ele-build-in', CustomEleBuiltIn, { extends'div' }); 

        使用的方式也是多樣的。可以通過document.createElement的方式使用,也可以直接書寫在html中。使用示例:

        //自主定制元素 
        const customEle = document.createElement('custom-ele'); 
        customEle.setAttribute('img''https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fcdn.duitang.com%2Fuploads%2Fitem%2F201409%2F06%2F20140906020558_h4VfY.png'); 
        customEle.setAttribute('text''我是一段懸停說明'); 
        document.querySelector('#app').appendChild(customEle); 
        customElements.define('custom-ele', CustomEle); 
        //或 
        customElements.define('custom-ele', CustomEle); 
        const customEle = new CustomEle(); 
        customEle.setAttribute('img''https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fcdn.duitang.com%2Fuploads%2Fitem%2F201409%2F06%2F20140906020558_h4VfY.png'); 
        customEle.setAttribute('text''我是一段懸停說明'); 
        document.querySelector('#app').appendChild(customEle); 
        //或 
        <custom-ele img="https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fcdn.duitang.com%2Fuploads%2Fitem%2F201409%2F06%2F20140906020558_h4VfY.png" text="我是一段懸停說明"> 
         
        //自定義內(nèi)置元素,如果要擴(kuò)展div的話 
        customElements.define('custom-ele-build-in', CustomEleBuiltIn, { extends: 'div' }); 
        const div = document.createElement('div', { is: 'custom-ele-build-in' }); 
        div.setAttribute('img', 'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fcdn.duitang.com%2Fuploads%2Fitem%2F201409%2F06%2F20140906020558_h4VfY.png'); 
        div.setAttribute('text', '我是一段懸停說明'); 
        document.querySelector('#app').appNode.appendChild(div); 
        //或 
        <div is="custom-ele-build-in" img="https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fcdn.duitang.com%2Fuploads%2Fitem%2F201409%2F06%2F20140906020558_h4VfY.png" text="我是一段懸停說明" /> 

        這里的幾 種使用方式其實(shí)還是有差異的,在初始化的時(shí)候,直接引用的方式可以在構(gòu)造階段就拿到掛載的各個(gè)屬性;但是采用create的方式時(shí),構(gòu)造階段無法第一時(shí)間獲取屬性,當(dāng)然,利用生命周期的鉤子函數(shù),也是解決該問題的。

        • window.customElements.get。

        該方法用來獲取自定義組件的構(gòu)造函數(shù),接受一個(gè)參數(shù),即聲明過的自定義組件的name,返回構(gòu)造函數(shù)。

        const getCustomConstructorBefore = customElements.get('custom-ele'); 
        console.log('getCustomConstructor-before', getCustomConstructorBefore);//undefined 
        customElements.define('custom-ele', CustomEle); 
        const getCustomConstructorAfter = customElements.get('custom-ele'); 
        console.log('getCustomConstructor-after', getCustomConstructorAfter);//CustomEle 
        • window.customElements.upgrade。

        該方法是用來更新掛載主文檔之前的包含shadow dom的自定義組件的,接受一個(gè)參數(shù),即包含了shadow dom的自定義組件節(jié)點(diǎn),無返回值。(自定義組件在被append到主文檔的時(shí)候,會(huì)觸發(fā)自動(dòng)更新)。

        //先創(chuàng)建了自定義元素 
        const customEle = document.createElement('custom-ele'); 
        customEle.setAttribute('img''https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fcdn.duitang.com%2Fuploads%2Fitem%2F201409%2F06%2F20140906020558_h4VfY.png'); 
        customEle.setAttribute('text''我是一段懸停說明'); 
        //后聲明自定義元素 
        customElements.define('custom-ele', CustomEle); 
        //結(jié)果為false,null 
        console.log(customEle instanceof CustomEle, customEle.shadowRoot); 
        //進(jìn)行更新節(jié)點(diǎn) 
        customElements.upgrade(customEle);//或document.querySelector('#app').appendChild(customEle); 
        //true,#document-fragment 
        console.log(customEle instanceof CustomEle, customEle.shadowRoot); 
        • window.customElements.whenDefined。

        該方法是用來檢測(cè)并提供自定義組件被定義聲明完畢的時(shí)機(jī)得,接受一個(gè)參數(shù),即自定義元素的name,返回值是一個(gè)promise(只檢測(cè)自定義組件是否被defined,不檢測(cè)是否被掛載于主文檔)。若提供的name無效,則觸發(fā)promise的catch。

        //創(chuàng)建了自定義元素dom 
        const customEle = document.createElement('custom-ele'); 
        customEle.setAttribute('img''https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fcdn.duitang.com%2Fuploads%2Fitem%2F201409%2F06%2F20140906020558_h4VfY.png'); 
        customEle.setAttribute('text''我是一段懸停說明'); 
        //用來判斷關(guān)閉定時(shí)器得標(biāo)識(shí) 
        let isStop = false
        //獲取自定義組件定義完畢的時(shí)機(jī) 
        customElements.whenDefined('custom-ele').then(() => { 
          console.log('定義完畢'); 
          isStop = true
        }); 
        //一個(gè)用于觀察得計(jì)時(shí)器 
        const timer = setInterval(() => { 
          if (isStop) { 
            clearInterval(timer); 
            return
          } 
          console.log(Math.floor(Date.now() / 1000)); 
        }, 1000); 
        //延遲3秒進(jìn)行自定義組件的定義及聲明 
        setTimeout(() => { 
          customElements.define('custom-ele', CustomEle); 
        }, 3000

        自定義組件的生命周期

        • constructor

        自定義組件的第一個(gè)生命周期,用來初始化自定義組件本身。觸發(fā)的時(shí)機(jī)在自定義組件被document.createElement的時(shí)候(前提是組件已經(jīng)被customElements.define過,如果組件是先create,后defined,那么constructor的執(zhí)行時(shí)機(jī)在append到主文檔里時(shí))。

        class CustomEle extends HTMLElement 
          constructor() { 
            super(); 
            console.log('constructor被執(zhí)行'); 
            ...... 
          } 

         
        customElements.define('custom-ele', CustomEle); 
        const customEle = document.createElement('custom-ele'); 
        • connectedCallback

        在組件被成功添加到主文檔時(shí)觸發(fā)的生命周期,在constructor之后。

        class CustomEle extends HTMLElement 
          constructor() { 
            super(); 
            console.log('constructor被執(zhí)行'); 
            ...... 
          }  
          connectedCallback () { 
            console.log('connectedCallback被執(zhí)行'); 
          } 

         
        customElements.define('custom-ele', CustomEle); 
        const customEle = document.createElement('custom-ele'); 
        document.querySelector('#app').appendChild(customEle); 
        • attributeChangedCallback

        自定義組件最關(guān)鍵的一個(gè)生命周期。觸發(fā)時(shí)機(jī)在組件屬性被增加、刪除或修改的時(shí)候。如果你是在組件被append之前設(shè)置了屬性,那么就會(huì)在connectedCallback之前觸發(fā);反之,則在connectedCallback之后觸發(fā)。需要配合靜態(tài)方法observedAttributes來使用,只有注冊(cè)在observedAttributes中的屬性才會(huì)被監(jiān)聽。

        class CustomEle extends HTMLElement 
          constructor() { 
            super(); 
            console.log('constructor被執(zhí)行'); 
            ...... 
          }  
          connectedCallback () { 
            console.log('connectedCallback被執(zhí)行'); 
          } 
          static get observedAttributes () { return [ 'img''text' ]; } 
          attributeChangedCallback (name, oldValue, newValue) { 
            console.log('attributeChangedCallback', name) 
            if (name === 'img') { 
              this.shadowRoot.querySelector('img').src = this.getAttribute('img'); 
            } 
            if (name === 'text') { 
              this.shadowRoot.querySelector('.info').textContent = this.getAttribute('text'); 
            } 
          } 

         
        customElements.define('custom-ele', CustomEle); 
        customEle.setAttribute('img''https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fcdn.duitang.com%2Fuploads%2Fitem%2F201409%2F06%2F20140906020558_h4VfY.png'); 
        customEle.setAttribute('text''我是一段懸停說明'); 
        const customEle = document.createElement('custom-ele'); 
        document.querySelector('#app').appendChild(customEle); 
        • adoptedCallback

        當(dāng)元素被移動(dòng)到新的文檔時(shí),被調(diào)用。即元素是另一個(gè)文檔的元素,而adoptedCallback是新文檔下的自定義組件的回調(diào)。

        //聲明自定義組件的類 
        class CustomEle extends HTMLElement 
          constructor() { 
            super(); 
            ......   
          } 
          adoptedCallback () { 
            console.log('adoptedCallback被執(zhí)行'); 
          } 

        //創(chuàng)造場(chǎng)景,增加iframe,即舊文檔 
        appNode.innerHTML = '<iframe></iframe>'
        const p = document.createElement('p'); 
        p.innerHTML = 'iframe'
        appNode.querySelector('iframe').contentWindow.document.body.appendChild(p); 
         
        //新文檔中創(chuàng)建自定義組件 
        const customEle = document.createElement('custom-ele'); 
        customEle.setAttribute('img''https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fcdn.duitang.com%2Fuploads%2Fitem%2F201409%2F06%2F20140906020558_h4VfY.png'); 
        customEle.setAttribute('text''我是一段懸停說明'); 
        customElements.define('custom-ele', CustomEle); 
        appNode.appendChild(customEle); 
         
        //將元素從舊文檔遷移到新文檔 
        setTimeout(() => { 
          console.log('開始對(duì)元素進(jìn)行adoptNode操作'
          const node = appNode.querySelector('iframe').contentWindow.document.body.firstElementChild; 
          appNode.appendChild(document.adoptNode(node)) 
        }, 2000); 

        該回調(diào)函數(shù)并不常用,了解即可。

        • disconnectedCallback

        自定義組件的最后一個(gè)生命周期,觸發(fā)的時(shí)機(jī)在組件被成功從主文檔移除時(shí)。

        class CustomEle extends HTMLElement 
          constructor() { 
            super(); 
            ......   
          }  
          disconnectedCallback () { 
            console.log('disconnectedCallback被執(zhí)行'); 
          } 

         
        customElements.define('custom-ele', CustomEle); 
        const customEle = document.createElement('custom-ele'); 
        document.querySelector('#app').appendChild(customEle); 
        setTimeout(() => { 
          appNode.removeChild(customEle); 
        }, 2000

        注意:瀏覽器關(guān)閉或tabs關(guān)閉,不會(huì)觸發(fā)disconnectedCallback。

        Shadow DOM的使用

        其作用是將標(biāo)記結(jié)構(gòu)、樣式和行為隱藏起來,并與頁面上的其他代碼相隔離。Shadow DOM 都不是一個(gè)新事物,在過去的很長(zhǎng)一段時(shí)間里,瀏覽器用它來封裝一些元素的內(nèi)部結(jié)構(gòu),回憶一下video標(biāo)簽內(nèi)部被隱藏起來的控制按鈕們。

        • 為元素附加Shadow DOM:ele.attachShadow

        attachShadow接受一個(gè)對(duì)象參數(shù),只需關(guān)注一個(gè)配置屬性mode,如果設(shè)置為open,表示可以從外部獲取Shadow DOM內(nèi)部的元素;如果設(shè)置為closed,則表示隱藏Shadow DOM內(nèi)部,例如<video>。

        class CustomEle extends HTMLElement 
          constructor() { 
            super(); 
            const shadow = this.attachShadow({ mode'open' }); 
            ...... 
          } 

        customElements.define('custom-ele', CustomEle); 
        const customEle = document.createElement('custom-ele'); 
        document.querySelector('#app').appendChild(customEle); 
        console.log(customEle.shadowRoot) 

        若mode設(shè)置為closed:

        • 操作元素的Shadow DOM并添加樣式

          當(dāng)為一個(gè)元素附加了Shadow DOM后,就可以使用同操作正常dom一樣的方法去操作了。示例如下:

        class CustomEle extends HTMLElement 
          constructor() { 
            super(); 
            const shadow = this.attachShadow({ mode'open' }); 
         
            const wrapper = document.createElement('span'); 
            wrapper.setAttribute('class''wrapper'); 
         
            const icon = document.createElement('span'); 
            icon.setAttribute('class''icon'); 
         
            const info = document.createElement('span'); 
            info.setAttribute('class''info'); 
         
            const text = this.getAttribute('text'); 
            info.textContent = text; 
         
            const img = document.createElement('img'); 
            img.src = this.getAttribute('img'); 
            icon.appendChild(img); 
         
            const style = document.createElement('style'); 
            // console.log('CustomEle', style.isConnected); 
            style.textContent = 
              .wrapper { 
                position: relative; 
              } 
              .info { 
                font-size: 0.8rem; 
                width: 200px; 
                display: inline-block; 
                border: 1px solid black; 
                padding: 10px; 
                background: white; 
                border-radius: 10px; 
                opacity: 0; 
                transition: 0.6s all; 
                position: absolute; 
                bottom: 20px; 
                left: 10px; 
                z-index: 3; 
              } 
              img { 
                width: 1.2rem; 
              } 
              .icon:hover + .info, .icon:focus + .info { 
                opacity: 1; 
              } 
            `

            shadow.appendChild(style); 
            // console.log('CustomEle', style.isConnected); 
         
            shadow.appendChild(wrapper); 
            wrapper.appendChild(icon); 
            wrapper.appendChild(info); 
          } 

        const customEle = document.createElement('custom-ele'); 
        customEle.setAttribute('img''https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fcdn.duitang.com%2Fuploads%2Fitem%2F201409%2F06%2F20140906020558_h4VfY.png'); 
        customEle.setAttribute('text''我是一段懸停說明'); 
        customElements.define('custom-ele', CustomEle); 
        document.querySelector('#app').appendChild(customEle); 

        如果想添加樣式表,則可以把上述代碼中的代碼:

        const style = document.createElement('style'); 
            // console.log('CustomEle', style.isConnected); 
            style.textContent = 
              .wrapper { 
                position: relative; 
              } 
              .info { 
                font-size: 0.8rem; 
                width: 200px; 
                display: inline-block; 
                border: 1px solid black; 
                padding: 10px; 
                background: white; 
                border-radius: 10px; 
                opacity: 0; 
                transition: 0.6s all; 
                position: absolute; 
                bottom: 20px; 
                left: 10px; 
                z-index: 3; 
              } 
              img { 
                width: 1.2rem; 
              } 
              .icon:hover + .info, .icon:focus + .info { 
                opacity: 1; 
              } 
            `

            shadow.appendChild(style); 

        替換為:

        const linkElem = document.createElement('link'); 
        linkElem.setAttribute('rel''stylesheet'); 
        linkElem.setAttribute('href''style.css');//樣式的地址 
         
        shadow.appendChild(linkElem); 

        需要注意的是:由于link元素不會(huì)打斷 shadow root 的繪制, 因此在加載樣式表時(shí)可能會(huì)出現(xiàn)未添加樣式內(nèi)容(FOUC),導(dǎo)致閃爍。

        模版

        • template

        使用包裹的內(nèi)容不會(huì)在頁面上顯示,但是卻可以被js引用到。這就意味著有些內(nèi)容我們不用重復(fù)構(gòu)建多遍,使用<template></template>構(gòu)建一遍,然后多次引用處理就好了。

        class CustomEle extends HTMLElement 
          constructor() { 
            super(); 
            console.log('constructor被執(zhí)行'); 
            const shadow = this.attachShadow({ mode'open' }); 
         
            let template = document.getElementById('my-paragraph'); 
            if (template) { 
              let templateContent = template.content; 
              shadow.appendChild(templateContent.cloneNode(true)); 
            } 
            ...... 
          } 

        appNode.innerHTML = '<template id="my-paragraph"><style>p {color: white;background-color: #666;padding: 5px;}</style><p>My paragraph</p></template>'
        const customEle = document.createElement('custom-ele'); 
        customEle.setAttribute('img''https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fcdn.duitang.com%2Fuploads%2Fitem%2F201409%2F06%2F20140906020558_h4VfY.png'); 
        customEle.setAttribute('text''我是一段懸停說明'); 
        customElements.define('custom-ele', CustomEle); 
        appNode.appendChild(customEle); 
        • slot
          • 在template的基礎(chǔ)上,更加靈活的內(nèi)容分發(fā),可以配合template使用(在template中定義占位符,然后將template的內(nèi)容clone到shadow DOM中)。也可以直接在shadow DOM中添加占位符。

        然后在自定義組件的innerhtml中使用即可。

        class CustomEle extends HTMLElement 
          constructor() { 
            super(); 
            console.log('constructor被執(zhí)行'); 
            const shadow = this.attachShadow({ mode'open' }); 
         
            let template = document.getElementById('my-paragraph'); 
            if (template) { 
              let templateContent = template.content; 
              shadow.appendChild(templateContent.cloneNode(true)); 
            } 
            const slot2 = document.createElement('slot'); 
            slot2.setAttribute('name''newText2'); 
            shadow.appendChild(slot2); 
            ...... 
          } 

        appNode.innerHTML = '<template id="my-paragraph"><style>p {color: white;background-color: #666;padding: 5px;}</style><slot name="newText1"></slot></template><custom-ele><p slot="newText1">newText1</p></custom-ele>'
        const customEle = document.createElement('custom-ele'); 
        customEle.innerHTML = '<p slot="newText2">newText2</p>'
        customElements.define('custom-ele', CustomEle); 
        appNode.appendChild(customEle); 
        • slotchange:用于監(jiān)聽shadow DOM中的slot插入或移除的事件。
        class CustomEle extends HTMLElement 
          constructor() { 
            super(); 
            let template = document.getElementById('my-paragraph'); 
            if (template) { 
              let templateContent = template.content; 
              shadow.appendChild(templateContent.cloneNode(true)); 
            } 
            const slots = shadow.querySelectorAll('slot'); 
            slots.forEach(slot => { 
              slot.addEventListener('slotchange'function (e
                console.log('slotchange', slot.name, e); 
              }); 
            }); 
            ...... 
          } 

         
        appNode.innerHTML = '<template id="my-paragraph">' + 
          '<style>p {color: white;background-color: #666;padding: 5px;}</style>' + 
          '<slot name="newText1"></slot>' + 
          '<slot name="spanText"></slot>' + 
          '</template>' + 
          '<h3>' + 
          '<custom-ele class="newText1Box">' + 
          '<p slot="newText1">newText1</p>' + 
          '<span slot="spanText">spanText</span>' + 
          '</custom-ele>' + 
          '</h3>'
        setTimeout(() => { 
          document.querySelector('.newText1Box').removeChild(document.querySelector('.newText1Box p')); 
          //或 
          document.querySelector('.newText1Box p').removeAttribute('slot'); 
        }, 2000
        在添加slot時(shí)(直接插入包含slot屬性的元素或給已插入的元素增加slot屬性)或刪除slot時(shí)(直接remove包含slot屬性的元素或給已插入的元素removeAttribute slot屬性),都會(huì)觸發(fā)slotchange事件。 

        相關(guān)的其他api

        • element.attachShadow(opt):用來給指定元素掛載shadow DOM。

        opt的配置項(xiàng):

        • mode:如果為open,表示可以在外部通過element.shadowRoot獲取shadow DOM節(jié)點(diǎn)。并且方法會(huì)返回shadow DOM對(duì)象。如果為closed,表示不允許外部訪問shadow DOM節(jié)點(diǎn),并且方法返回null。
        • delegatesFocus:表示是否減輕自定義元素的聚焦性能問題。當(dāng)shadow DOM中不可聚焦的部分被點(diǎn)擊時(shí), 讓第一個(gè)可聚焦的部分成為焦點(diǎn), 并且shadow host將提供所有可用的 :focus 樣式.
        • css偽類:
          • :defined:表示所有內(nèi)置元素及已經(jīng)通過customElements.define注冊(cè)的元素。
          • :host:只能在shadow DOM的樣式表內(nèi)書寫。表示當(dāng)前所在的自定義組件的所有實(shí)例及shadow DOM下所有的元素。
          • :host([選擇器]):只能在shadow DOM的樣式表內(nèi)書寫。是:host的增強(qiáng),表示:host()所在的自定義組件的所有實(shí)例中選擇器符合括號(hào)中名稱的實(shí)例及其包含的shadow DOM下屬所有元素。
          • :host-context([選擇器]):只能在shadow DOM的樣式表內(nèi)書寫。是:host的增強(qiáng),表示:host()-context所在的自定義組件的所有實(shí)例的父元素中選擇器符合括號(hào)中名稱的實(shí)例及其包含的shadow DOM下屬所有元素。
          • :slotted([選擇器]):只能在shadow DOM的樣式表內(nèi)書寫。表示: slotted()所在的自定義組件的所有實(shí)例中選擇器符合括號(hào)中名稱的slot元素,若選擇器為*,則表示命中所有slot。
        • 節(jié)點(diǎn)相關(guān)拓展
          • getRootNode:使用方式為ele. getRootNode(opt),opt中包含一個(gè)屬性composed,為true時(shí),檢索到的根元素為document;為false時(shí),如果ele是屬于shadow DOM,那么檢索到shadow DOM,否則檢索到document。
          • isConnected:是元素的一個(gè)只讀屬性接口。返回元素是否與dom樹連接的boolean值。即是否被append到主文檔中。
        • event擴(kuò)展
          • composed屬性:用來指示該事件是否可以從 Shadow DOM 傳遞到一般的 DOM(測(cè)試后發(fā)現(xiàn)不論是普通DOM還是shadow DOM均為true)。
          • path屬性:返回事件的路徑。如果shadow root是使用mode為closed創(chuàng)建的,則不包括shadow樹中的節(jié)點(diǎn)(測(cè)試后發(fā)現(xiàn)盡管shadowdom設(shè)置了mode為closed,依然能獲取完整的path)。
        • 關(guān)于slot
          • ele.assignedSlot:用來獲取ele元素上代表插入slot的元素。但如果ele.attachShadow中的mode是closed為closed時(shí),返回null。
          • ele.slot:用來獲取元素上slot的name值。
        • ......

        相關(guān)的庫及網(wǎng)站

        • webcomponents.org — site featuring web components examples, tutorials, and other information.
        • Hybrids — Open source web components library, which favors plain objects and pure functions over class and this syntax. It provides a simple and functional API for creating custom elements.
        • Polymer — Google's web components framework — a set of polyfills, enhancements, and examples. Currently the easiest way to use web components cross-browser.
        • Snuggsi.es — Easy Web Components in ~1kB Including polyfill — All you need is a browser and basic understanding of HTML, CSS, and JavaScript classes to be productive.
        • Slim.js — Open source web components library — a high-performant library for rapid and easy component authoring; extensible and pluggable and cross-framework compatible.
        • Smart.js — Web Components library with simple API for creating cross-browser custom elements.
        • Stencil — Toolchain for building reusable, scalable design systems in web components.

        參考

        • https://developer.mozilla.org/zh-CN/docs/Web/Web_Components

        • https://medium.com/jspoint/the-anatomy-of-web-components-d6afedb81b37

        • https://www.ruanyifeng.com/blog/2019/08/web_components.html 

        • https://html.spec.whatwg.org/multipage/custom-elements.html#custom-elements

        • https://developers.google.cn/web/fundamentals/web-components 

        • https://objectpartners.com/2015/11/19/comparing-react-js-performance-vs-native-dom/ 

        • https://bugs.webkit.org/show_bug.cgi?id=182671 

        ?? 

        便內(nèi),對(duì)^_^

         、點(diǎn)、 ~

        關(guān)號(hào) 前端Sharing ~



        瀏覽 43
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評(píng)論
        圖片
        表情
        推薦
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
          
          

            1. 不卡A片 四虎永久免费地址 | 免费做爱网站 | 男女亲吻强摸下面视频 | 久久久久国产免费观看 | 中国黄色一级毛片 |