什么是原生HTML組件

什么是原生HTML組件?針對(duì)這個(gè)問題,這篇文章詳細(xì)介紹了相對(duì)應(yīng)的分析和解答,希望可以幫助更多想解決這個(gè)問題的小伙伴找到更簡(jiǎn)單易行的方法。

目前成都創(chuàng)新互聯(lián)已為1000+的企業(yè)提供了網(wǎng)站建設(shè)、域名、虛擬主機(jī)、網(wǎng)站改版維護(hù)、企業(yè)網(wǎng)站設(shè)計(jì)、福山網(wǎng)站維護(hù)等服務(wù),公司將堅(jiān)持客戶導(dǎo)向、應(yīng)用為本的策略,正道將秉承"和諧、參與、激情"的文化,與客戶和合作伙伴齊心協(xié)力一起成長(zhǎng),共同發(fā)展。

嘿!看看這幾年啊,Web 前端的發(fā)展可是真快?。?/p>

想想幾年前,HTML 是前端開發(fā)者的基本技能,通過各式各樣的標(biāo)簽就可以搭建一個(gè)可用的網(wǎng)站,基本交互也不是問題。如果再來點(diǎn) CSS,嗯,金黃酥脆,美味可口。這時(shí)候再撒上幾把 JavaScript,簡(jiǎn)直讓人欲罷不能。

隨著需求的增長(zhǎng),HTML 的結(jié)構(gòu)越來越復(fù)雜,大量重復(fù)的代碼使得頁面改動(dòng)起來異常困難,這也就孵化了一批批模版工具,將公共的部分抽取出來變?yōu)楣步M件。再后來,隨著 JavaScript 的性能提升,JavaScript 的地位越來越高,不再只是配菜了,前端渲染的出現(xiàn)降低了服務(wù)端解析模版的壓力,服務(wù)端只要提供靜態(tài)文件和 API 接口就行了嘛。再然后,前端渲染工具又被搬回了服務(wù)端,后端渲染出現(xiàn)了(黑人問號(hào)???)

總之,組件化使得復(fù)雜的前端結(jié)構(gòu)變得清晰,各個(gè)部分獨(dú)立起來,高內(nèi)聚低耦合,使得維護(hù)成本大大降低。

那么,你有聽說過原生 HTML 組件嗎?

四大 Web 組件標(biāo)準(zhǔn)

在說原生 HTML 組件之前,要先簡(jiǎn)單介紹一下四大 Web 組件標(biāo)準(zhǔn),四大 Web 組件標(biāo)準(zhǔn)分別為:HTML Template、Shadow DOM、Custom Elements 和 HTML Imports。實(shí)際上其中一個(gè)已經(jīng)被廢棄了,所以變成“三大”了。

HTML Template 相信很多人都有所耳聞,簡(jiǎn)單的講也就是 HTML5 中的 <template> 標(biāo)簽,正常情況下它無色無味,感知不到它的存在,甚至它下面的 img 都不會(huì)被下載,script 都不會(huì)被執(zhí)行。<template> 就如它的名字一樣,它只是一個(gè)模版,只有到你用到它時(shí),它才會(huì)變得有意義。

Shadow DOM 則是原生組件封裝的基本工具,它可以實(shí)現(xiàn)組件與組件之間的獨(dú)立性。

Custom Elements 是用來包裝原生組件的容器,通過它,你就只需要寫一個(gè)標(biāo)簽,就能得到一個(gè)完整的組件。

HTML Imports 則是 HTML 中類似于 ES6 Module 的一個(gè)東西,你可以直接 import 另一個(gè) html 文件,然后使用其中的 DOM 節(jié)點(diǎn)。但是,由于 HTML Imports 和 ES6 Module 實(shí)在是太像了,并且除了 Chrome 以外沒有瀏覽器愿意實(shí)現(xiàn)它,所以它已經(jīng)被廢棄并不推薦使用了。未來會(huì)使用 ES6 Module 來取代它,但是現(xiàn)在貌似還沒有取代的方案,在新版的 Chrome 中這個(gè)功能已經(jīng)被刪除了,并且在使用的時(shí)候會(huì)在 Console 中給出警告。警告中說使用 ES Modules 來取代,但是我測(cè)試在 Chrome 71 中 ES Module 會(huì)強(qiáng)制檢測(cè)文件的 MIME 類型必須為 JavaScript 類型,應(yīng)該是暫時(shí)還沒有實(shí)現(xiàn)支持。

什么是原生HTML組件

Shadow DOM

要說原生 HTML 組件,就要先聊聊 Shadow DOM 到底是個(gè)什么東西。

大家對(duì) DOM 都很熟悉了,在 HTML 中作為一個(gè)最基礎(chǔ)的骨架而存在,它是一個(gè)樹結(jié)構(gòu),樹上的每一個(gè)節(jié)點(diǎn)都是 HTML 中的一部分。DOM 作為一棵樹,它擁有著上下級(jí)的層級(jí)關(guān)系,我們通常使用“父節(jié)點(diǎn)”、“子節(jié)點(diǎn)”、“兄弟節(jié)點(diǎn)”等來進(jìn)行描述(當(dāng)然有人覺得這些稱謂強(qiáng)調(diào)性別,所以也創(chuàng)造了一些性別無關(guān)的稱謂)。子節(jié)點(diǎn)在一定程度上會(huì)繼承父節(jié)點(diǎn)的一些東西,也會(huì)因兄弟節(jié)點(diǎn)而產(chǎn)生一定的影響,比較明顯的是在應(yīng)用 CSS Style 的時(shí)候,子節(jié)點(diǎn)會(huì)從父節(jié)點(diǎn)那里繼承一些樣式。

而 Shadow DOM,也是 DOM 的一種,所以它也是一顆樹,只不過它是長(zhǎng)在 DOM 樹上的一棵特殊的紫薯,啊不,子樹。

什么?DOM 本身不就是由一棵一棵的子樹組成的嗎?這個(gè) Shadow DOM 有什么特別的嗎?

Shadow DOM 的特別之處就在于它致力于創(chuàng)建一個(gè)相對(duì)獨(dú)立的一個(gè)空間,雖然也是長(zhǎng)在 DOM 樹上的,但是它的環(huán)境卻是與外界隔離的,當(dāng)然這個(gè)隔離是相對(duì)的,在這個(gè)隔離空間中,你可以選擇性地從 DOM 樹上的父節(jié)點(diǎn)繼承一些屬性,甚至是繼承一棵 DOM 樹進(jìn)來。

利用 Shadow DOM 的隔離性,我們就可以創(chuàng)造原生的 HTML 組件了。

實(shí)際上,瀏覽器已經(jīng)通過 Shadow DOM 實(shí)現(xiàn)了一些組件了,只是我們使用過卻沒有察覺而已,這也是 Shadow DOM 封裝的組件的魅力所在:你只管寫一個(gè) HTML 標(biāo)簽,其他的交給我。(是不是有點(diǎn)像 React 的 JSX 啊?)

我們來看一看瀏覽器利用 Shadow DOM 實(shí)現(xiàn)的一個(gè)示例吧,那就是 video 標(biāo)簽:

<video controls src="./video.mp4" width="400" height="300"></video>

我們來看一下瀏覽器渲染的結(jié)果:

什么是原生HTML組件

等一下!不是說 Shadow DOM 嗎?這和普通 DOM 有啥區(qū)別???

在 Chrome 中,Elements 默認(rèn)是不顯示內(nèi)部實(shí)現(xiàn)的 Shadow DOM 節(jié)點(diǎn)的,需要在設(shè)置中啟用:

什么是原生HTML組件

什么是原生HTML組件

注:瀏覽器默認(rèn)隱藏自身的 Shadow DOM 實(shí)現(xiàn),但如果是用戶通過腳本創(chuàng)造的 Shadow DOM,是不會(huì)被隱藏的。

然后,我們就可以看到 video 標(biāo)簽的真面目了:

什么是原生HTML組件

在這里,你可完全像調(diào)試普通 DOM 一樣隨意調(diào)整 Shadow DOM 中的內(nèi)容(反正和普通 DOM 一樣,刷新一下就恢復(fù)了)。

我們可以看到上面這些 shadow DOM 中的節(jié)點(diǎn)大多都有 pseudo 屬性,根據(jù)這個(gè)屬性,你就可以在外面編寫 CSS 樣式來控制對(duì)應(yīng)的節(jié)點(diǎn)樣式了。比如,將上面這個(gè) pseudo="-webkit-media-controls-overlay-play-button" 的 input 按鈕的背景色改為橙色:

video::-webkit-media-controls-overlay-play-button {
  background-color: orange;
}

什么是原生HTML組件

由于 Shadow DOM 實(shí)際上也是 DOM 的一種,所以在 Shadow DOM 中還可以繼續(xù)嵌套 Shadow DOM,就像上面那樣。

瀏覽器中還有很多 Element 都使用了 Shadow DOM 的形式進(jìn)行封裝,比如 <input>、<select>、<audio> 等,這里就不一一展示了。

由于 Shadow DOM 的隔離性,所以即便是你在外面寫了個(gè)樣式:div { background-color: red !important; },Shadow DOM 內(nèi)部的 div 也不會(huì)受到任何影響

也就是說,寫樣式的時(shí)候,該用 id 的時(shí)候就用 id,該用 class 的時(shí)候就用 class,一個(gè)按鈕的 class 應(yīng)該寫成 .button 就寫成 .button。完全不用考慮當(dāng)前組件中的 id、class 可能會(huì)與其他組件沖突,你只要確保一個(gè)組件內(nèi)部不沖突就好——這很容易做到。

這解決了現(xiàn)在絕大多數(shù)的組件化框架都面臨的問題:Element 的 class(className) 到底怎么寫?用前綴命名空間的形式會(huì)導(dǎo)致 class 名太長(zhǎng),像這樣:.header-nav-list-sublist-button-icon;而使用一些 CSS-in-JS 工具,可以創(chuàng)造一些唯一的 class 名稱,像這樣:.Nav__welcomeWrapper___lKXTg,這樣的名稱仍舊有點(diǎn)長(zhǎng),還帶了冗余信息。

ShadowRoot

ShadowRoot 是 Shadow DOM 下面的根,你可以把它當(dāng)做 DOM 中的 <body> 一樣看待,但是它不是 <body>,所以你不能使用 <body> 上的一些屬性,甚至它不是一個(gè)節(jié)點(diǎn)。

你可以通過 ShadowRoot 下面的 appendChild、querySelectorAll 之類的屬性或方法去操作整個(gè) Shadow DOM 樹。

對(duì)于一個(gè)普通的 Element,比如 <p>,你可以通過調(diào)用它上面的 attachShadow 方法來創(chuàng)建一個(gè) ShadowRoot(還有一個(gè) createShadowRoot 方法,已經(jīng)過時(shí)不推薦使用),attachShadow 接受一個(gè)對(duì)象進(jìn)行初始化:{ mode: 'open' },這個(gè)對(duì)象有一個(gè) mode 屬性,它有兩個(gè)取值:'open' 和 'closed',這個(gè)屬性是在創(chuàng)造 ShadowRoot 的時(shí)候需要初始化提供的,并在創(chuàng)建 ShadowRoot 之后成為一個(gè)只讀屬性。

mode: 'open' 和 mode: 'closed' 有什么區(qū)別呢?在調(diào)用 attachShadow 創(chuàng)建 ShadowRoot 之后,attachShdow 方法會(huì)返回 ShadowRoot 對(duì)象實(shí)例,你可以通過這個(gè)返回值去構(gòu)造整個(gè) Shadow DOM。當(dāng) mode 為 'open' 時(shí),在用于創(chuàng)建 ShadowRoot 的外部普通節(jié)點(diǎn)(比如 <p>)上,會(huì)有一個(gè) shadowRoot 屬性,這個(gè)屬性也就是創(chuàng)造出來的那個(gè) ShadowRoot,也就是說,在創(chuàng)建 ShadowRoot 之后,還是可以在任何地方通過這個(gè)屬性再得到 ShadowRoot,繼續(xù)對(duì)其進(jìn)行改造;而當(dāng) mode 為 'closed' 時(shí),你將不能再得到這個(gè)屬性,這個(gè)屬性會(huì)被設(shè)置為 null,也就是說,你只能在 attachShadow 之后得到 ShadowRoot 對(duì)象,用于構(gòu)造整個(gè) Shadow DOM,一旦你失去對(duì)這個(gè)對(duì)象的引用,你就無法再對(duì) Shadow DOM 進(jìn)行改造了。

可以從上面 Shadow DOM 的截圖中看到 #shadow-root (user-agent) 的字樣,這就是 ShadowRoot 對(duì)象了,而括號(hào)中的 user-agent 表示這是瀏覽器內(nèi)部實(shí)現(xiàn)的 Shadow DOM,如果使用通過腳本自己創(chuàng)建的 ShadowRoot,括號(hào)中會(huì)顯示為 open 或 closed 表示 Shadow DOM 的 mode。

什么是原生HTML組件

瀏覽器內(nèi)部實(shí)現(xiàn)的 user-agent 的 mode 為 closed,所以你不能通過節(jié)點(diǎn)的 ShadowRoot 屬性去獲得其 ShadowRoot 對(duì)象,也就意味著你不能通過腳本對(duì)這些瀏覽器內(nèi)部實(shí)現(xiàn)的 Shadow DOM 進(jìn)行改造。

HTML Template

有了 ShadowRoot 對(duì)象,我們可以通過代碼來創(chuàng)建內(nèi)部結(jié)構(gòu)了,對(duì)于簡(jiǎn)單的結(jié)構(gòu),也許我們可以直接通過 document.createElement 來創(chuàng)建,但是稍微復(fù)雜一些的結(jié)構(gòu),如果全部都這樣來創(chuàng)建不僅麻煩,而且代碼可讀性也很差。當(dāng)然也可以通過 ES6 提供的反引號(hào)字符串(const template = `......`;)配合 innerHTML 來構(gòu)造結(jié)構(gòu),利用反引號(hào)字符串中可以任意換行,并且 HTML 對(duì)縮進(jìn)并不敏感的特性來實(shí)現(xiàn)模版,但是這樣也是不夠優(yōu)雅,畢竟代碼里大段大段的 HTML 字符串并不美觀,即便是單獨(dú)抽出一個(gè)常量文件也是一樣。

這個(gè)時(shí)候就可以請(qǐng) HTML Template 出場(chǎng)了。我們可以在 html 文檔中編寫 DOM 結(jié)構(gòu),然后在 ShadowRoot 中加載過來即可。

HTML Template 實(shí)際上就是在 html 中的一個(gè) <template> 標(biāo)簽,正常情況下,這個(gè)標(biāo)簽下的內(nèi)容是不會(huì)被渲染的,包括標(biāo)簽下的 img、style、script 等都是不會(huì)被加載或執(zhí)行的。你可以在腳本中使用 getElementById 之類的方法得到 <template> 標(biāo)簽對(duì)應(yīng)的節(jié)點(diǎn),但是卻無法直接訪問到其內(nèi)部的節(jié)點(diǎn),因?yàn)槟J(rèn)他們只是模版,在瀏覽器中表現(xiàn)為 #document-fragment,字面意思就是“文檔片段”,可以通過節(jié)點(diǎn)對(duì)象的 content 屬性來訪問到這個(gè) document-fragment 對(duì)象。

什么是原生HTML組件

通過 document-fragment 對(duì)象,就可以訪問到 template 內(nèi)部的節(jié)點(diǎn)了,通過 document.importNode 方法,可以將 document-fragment 對(duì)象創(chuàng)建一份副本,然后可以使用一切 DOM 屬性方法替換副本中的模版內(nèi)容,最終將其插入到 DOM 或是 Shadow DOM 中。

<div id="div"></div>
<template id="temp">
  <div id="title"></div>
</template>
const template = document.getElementById('temp');
const copy = document.importNode(template.content, true);
copy.getElementById('title').innerHTML = 'Hello World!';

const div = document.getElementById('div');
const shadowRoot = div.attachShadow({ mode: 'closed' });
shadowRoot.appendChild(copy);

HTML Imports

有了 HTML Template,我們已經(jīng)可以方便地創(chuàng)造封閉的 Web 組件了,但是目前還有一些不完美的地方:我們必須要在 html 中定義一大批的 <template>,每個(gè)組件都要定義一個(gè) <template>。

此時(shí),我們就可以用到已經(jīng)被廢棄的 HTML Imports 了。雖然它已經(jīng)被廢棄了,但是未來會(huì)通過 ES6 Modules 的形式再進(jìn)行支持,所以理論上也只是換個(gè)加載形式而已。

通過 HTML Imports,我們可以將 <template> 定義在其他的 html 文檔中,然后再在需要的 html 文檔中進(jìn)行導(dǎo)入(當(dāng)然也可以通過腳本按需導(dǎo)入),導(dǎo)入后,我們就可以直接使用其中定義的模版節(jié)點(diǎn)了。

已經(jīng)廢棄的 HTML Imports 通過 <link> 標(biāo)簽實(shí)現(xiàn),只要指定 rel="import" 就可以了,就像這樣:<link rel="import" href="./templates.html">,它可以接受 onload 和 onerror 事件以指示它已經(jīng)加載完成。當(dāng)然也可以通過腳本來創(chuàng)建 link 節(jié)點(diǎn),然后指定 rel 和 href 來按需加載。Import 成功后,在 link 節(jié)點(diǎn)上有一個(gè) import 屬性,這個(gè)屬性中存儲(chǔ)的就是 import 進(jìn)來的 DOM 樹啦,可以 querySelector 之類的,并通過 cloneNode 或 document.importNode 方法創(chuàng)建副本后使用。

未來新的 HTML Imports 將會(huì)以 ES6 Module 的形式提供,可以在 JavaScript 中直接 import * as template from './template.html';,也可以按需 import,像這樣:const template = await import('./template.html');。不過目前雖然瀏覽器都已經(jīng)支持 ES6 Modules,但是在 import 其他模塊時(shí)會(huì)檢查服務(wù)端返回文件的 MIME 類型必須為 JavaScript 的 MIME 類型,否則不允許加載。

Custom Elements

有了上面的三個(gè)組件標(biāo)準(zhǔn),我們實(shí)際上只是對(duì) HTML 進(jìn)行拆分而已,將一個(gè)大的 DOM 樹拆成一個(gè)個(gè)相互隔離的小 DOM 樹,這還不是真正的組件。

要實(shí)現(xiàn)一個(gè)真正的組件,我們就需要用到 Custom Elements 了,就如它的名字一樣,它是用來定義原生組件的。

Custom Elements 的核心,實(shí)際上就是利用 JavaScript 中的對(duì)象繼承,去繼承 HTML 原生的 HTMLElement 類(或是具體的某個(gè)原生 Element 類,比如 HTMLButtonElement),然后自己編寫相關(guān)的生命周期函數(shù),處理成員屬性以及用戶交互的事件。

看起來這和現(xiàn)在的 React 很像,在 React 中,你可以這樣創(chuàng)造一個(gè)組件:class MyElement extends React.Component { ... },而使用原生 Custom Elements,你需要這樣寫:class MyElement extends HTMLElement { ... }。

Custom Elements 的生命周期函數(shù)并不多,但是足夠使用。這里我將 Custom Elements 的生命周期函數(shù)與 React 進(jìn)行一個(gè)簡(jiǎn)單的對(duì)比:

constructor(): 構(gòu)造函數(shù),用于初始化 state、創(chuàng)建 Shadow DOM、監(jiān)聽事件之類。

對(duì)應(yīng) React 中 Mounting 階段的大半部分,包括:constructor(props)、static getDerivedStateFromProps(props, state) 和 render()。

在 Custom Elements 中,constructor() 構(gòu)造函數(shù)就是其原本的含義:初始化,和 React 的初始化類似,但它沒有像 React 中那樣將其拆分為多個(gè)部分。在這個(gè)階段,組件僅僅是被創(chuàng)建出來(比如通過 document.createElement()),但是還沒有插入到 DOM 樹中。

connectedCallback(): 組件實(shí)例已被插入到 DOM 樹中,用于進(jìn)行一些展示相關(guān)的初始化操作。

對(duì)應(yīng) React 中 Mounting 階段的最后一個(gè)生命周期:componentDidMount()。

在這個(gè)階段,組件已經(jīng)被插入到 DOM 樹中了,或是其本身就在 html 文件中寫好在 DOM 樹上了,這個(gè)階段一般是進(jìn)行一些展示相關(guān)的初始化,比如加載數(shù)據(jù)、圖片、音頻或視頻之類并進(jìn)行展示。

attributeChangedCallback(attrName, oldVal, newVal): 組件屬性發(fā)生變化,用于更新組件的狀態(tài)。

對(duì)應(yīng) React 中的 Updating 階段:static getDerivedStateFromProps(props, state)、shouldComponentUpdate(nextProps, nextState)、render()、getSnapshotBeforeUpdate(prevProps, prevState) 和 componentDidUpdate(prevProps, prevState, snapshot)。

當(dāng)組件的屬性(React 中的 props)發(fā)生變化時(shí)觸發(fā)這個(gè)生命周期,但是并不是所有屬性變化都會(huì)觸發(fā),比如組件的 class、style 之類的屬性發(fā)生變化一般是不會(huì)產(chǎn)生特殊交互的,如果所有屬性發(fā)生變化都觸發(fā)這個(gè)生命周期的話,會(huì)使得性能造成較大的影響。所以 Custom Elements 要求開發(fā)者提供一個(gè)屬性列表,只有當(dāng)屬性列表中的屬性發(fā)生變化時(shí)才會(huì)觸發(fā)這個(gè)生命周期函數(shù)。

這個(gè)屬性列表通過組件類上的一個(gè)靜態(tài)只讀屬性來聲明,在 ES6 Class 中使用一個(gè) getter 函數(shù)來實(shí)現(xiàn),只實(shí)現(xiàn) getter 而不實(shí)現(xiàn) setter,getter 返回一個(gè)常量,這樣就是只讀的了。像這樣:

class AwesomeElement extends HTMLElement {
  static get observedAttributes() {
    return ['awesome'];
  }
}

disconnectedCallback(): 組件被從 DOM 樹中移除,用于進(jìn)行一些清理操作。

對(duì)應(yīng) React 中的 Unmounting 階段:componentWillUnmount()。

adoptedCallback(): 組件實(shí)例從一個(gè)文檔被移動(dòng)到另一個(gè)文檔。

這個(gè)生命周期是原生組件獨(dú)有的,React 中沒有類似的生命周期。這個(gè)生命周期函數(shù)也并不常用到,一般在操作多個(gè) document 的時(shí)候會(huì)遇到,調(diào)用 document.adoptNode() 函數(shù)轉(zhuǎn)移節(jié)點(diǎn)所屬 document 時(shí)會(huì)觸發(fā)這個(gè)生命周期。

在定義了自定義組件后,我們需要將它注冊(cè)到 HTML 標(biāo)簽列表中,通過 window.customElements.define() 函數(shù)即可實(shí)現(xiàn),這個(gè)函數(shù)接受兩個(gè)必須參數(shù)和一個(gè)可選參數(shù)。第一個(gè)參數(shù)是注冊(cè)的標(biāo)簽名,為了避免和 HTML 自身的標(biāo)簽沖突,Custom Elements 要求用戶自定義的組件名必須至少包含一個(gè)短杠 -,并且不能以短杠開頭,比如 my-element、awesome-button 之類都是可以的。第二個(gè)參數(shù)是注冊(cè)的組件的 class,直接將繼承的子類類名傳入即可,當(dāng)然也可以直接寫一個(gè)匿名類:

window.customElements.define('my-element', class extends HTMLElement {
  ...
});

注冊(cè)之后,我們就可以使用了,可以直接在 html 文檔中寫對(duì)應(yīng)的標(biāo)簽,比如:<my-element></my-element>,也可以通過 document.createElement('my-element') 來創(chuàng)建,用法與普通標(biāo)簽幾乎完全一樣。但要注意的是,雖然 html 標(biāo)準(zhǔn)中說部分標(biāo)簽可以不關(guān)閉或是自關(guān)閉(<br> 或是 <br />),但是只有規(guī)定的少數(shù)幾個(gè)標(biāo)簽允許自關(guān)閉,所以,在 html 中寫 Custom Elements 的節(jié)點(diǎn)時(shí)必須帶上關(guān)閉標(biāo)簽。

由于 Custom Elements 是通過 JavaScript 來定義的,而一般 js 文件都是通過 <script> 標(biāo)簽外聯(lián)的,所以 html 文檔中的 Custom Elements 在 JavaScript 未執(zhí)行時(shí)是處于一個(gè)默認(rèn)的狀態(tài),瀏覽器默認(rèn)會(huì)將其內(nèi)容直接顯示出來。為了避免這樣的情況發(fā)生,Custom Elements 在被注冊(cè)后都會(huì)有一個(gè) :defined CSS 偽類而在注冊(cè)前沒有,所以我們可以通過 CSS 選擇器在 Custom Elements 注冊(cè)前將其隱藏起來,比如:

my-element:not(:defined) {
  display: none;
}

或者 Custom Elements 也提供了一個(gè)函數(shù)來檢測(cè)指定的組件是否已經(jīng)被注冊(cè):customElements.whenDefined(),這個(gè)函數(shù)接受一個(gè)組件名參數(shù),并返回一個(gè) Promise,當(dāng) Promise 被 resolve 時(shí),就表示組件被注冊(cè)了。

這樣,我們就可以放心的在加載 Custom Elements 的 JavaScript 的 <script> 標(biāo)簽上使用 async 屬性來延遲加載了(當(dāng)然,如果是使用 ES6 Modules 形式的話默認(rèn)的加載行為就會(huì)和 defer 類似)。

什么是原生HTML組件

Custom Elements + Shadow DOM

使用 Custom Elements 來創(chuàng)建組件時(shí),通常會(huì)與 Shadow DOM 進(jìn)行結(jié)合,利用 Shadow DOM 的隔離性,就可以創(chuàng)造獨(dú)立的組件。

通常在 Custom Elements 的 constructor() 構(gòu)造函數(shù)中去創(chuàng)建 Shadow DOM,并對(duì) Shadow DOM 中的節(jié)點(diǎn)添加事件監(jiān)聽、對(duì)特定事件觸發(fā)原生 Events 對(duì)象。

正常編寫 html 文檔時(shí),我們可能會(huì)給 Custom Elements 添加一些子節(jié)點(diǎn),像這樣:<my-element><h2>Title</h2><p>Content</p></my-element>,而我們創(chuàng)建的 Shadow DOM 又擁有其自己的結(jié)構(gòu),怎樣將這些子節(jié)點(diǎn)放置到 Shadow DOM 中正確的位置上呢?

在 React 中,這些子節(jié)點(diǎn)被放置在 props 的 children 中,我們可以在 render() 時(shí)選擇將它放在哪里。而在 Shadow DOM 中有一個(gè)特殊的標(biāo)簽:<slot>,這個(gè)標(biāo)簽的用處就如同其字面意思,在 Shadow DOM 上放置一個(gè)“插槽”,然后 Custom Elements 的子節(jié)點(diǎn)就會(huì)自動(dòng)放置到這個(gè)“插槽”中了。

有時(shí)我們需要更加精確地控制子節(jié)點(diǎn)在 Shadow DOM 中的位置,而默認(rèn)情況下,所有子節(jié)點(diǎn)都會(huì)被放置在同一個(gè) <slot> 標(biāo)簽下,即便是你寫了多個(gè) <slot>。那怎樣更精確地對(duì)子節(jié)點(diǎn)進(jìn)行控制呢?

默認(rèn)情況下,<slot>Fallback</slot> 這樣的是默認(rèn)的 <slot>,只有第一個(gè)默認(rèn)的 <slot> 會(huì)有效,將所有子節(jié)點(diǎn)全部放進(jìn)去,如果沒有可用的子節(jié)點(diǎn),將會(huì)顯示默認(rèn)的 Fallback 內(nèi)容(Fallback 可以是一棵子 DOM 樹)。

<slot> 標(biāo)簽有一個(gè) name 屬性,當(dāng)你提供 name 后,它將變?yōu)橐粋€(gè)“有名字的 <slot>”,這樣的 <slot> 可以存在多個(gè),只要名字各不相同。此時(shí)他們會(huì)自動(dòng)匹配 Custom Elements 下帶 slot 屬性并且 slot 屬性與自身 name 相同的子節(jié)點(diǎn),像這樣

<template id="list">
  <div>
    <h2>Others</h2>
    <slot></slot>
  </div>
  <div>
    <h2>Animals</h2>
    <slot name="animal"></slot>
  </div>
  <div>
    <h2>Fruits</h2>
    <slot name="fruit"></slot>
  </div>
</template>

<my-list>
  <div slot="animal">Cat</div>
  <div slot="fruit">Apple</div>
  <div slot="fruit">Banana</div>
  <div slot="other">flower</div>
  <div>pencil</div>
  <div slot="animal">Dog</div>
  <div slot="fruit">peach</div>
  <div>red</div>
</my-list>
class MyList extends HTMLElement {
  constructor() {
    super();
    const root = this.attachShadow({ mode: 'open' });
    const template = document.getElementById('list');
    root.appendChild(document.importNode(template.content, true));
  }
}
customElements.define('my-list', MyList);

這樣就可以得到如圖所示的結(jié)構(gòu),#shadow-root (open) 表示這是一個(gè)開放的 Shadow DOM,下面的節(jié)點(diǎn)是直接從 template 中 clone 過來的,瀏覽器自動(dòng)在三個(gè) <slot> 標(biāo)簽下放置了幾個(gè)灰色的 <div> 節(jié)點(diǎn),實(shí)際上這些灰色的 <div> 節(jié)點(diǎn)表示的是到其真實(shí)節(jié)點(diǎn)的“引用”,鼠標(biāo)移動(dòng)到他們上會(huì)顯示一個(gè) reveal 鏈接,點(diǎn)擊這個(gè)鏈接即可跳轉(zhuǎn)至其真實(shí)節(jié)點(diǎn)。

什么是原生HTML組件

這里我們可以看到,雖然 <my-list> 下的子節(jié)點(diǎn)是亂序放置的,但是只要是給定了 slot 屬性,就會(huì)被放置到正確的 <slot> 標(biāo)簽下。注意觀察其中有一個(gè) <div slot="other">flower</div>,這個(gè)節(jié)點(diǎn)由于指定了 slot="other",但是卻找不到匹配的 <slot> 標(biāo)簽,所以它不會(huì)被顯示在結(jié)果中。

在為 Custom Elements 下的 Shadow DOM 設(shè)置樣式的時(shí)候,我們可以直接在 Shadow DOM 下放置 <style> 標(biāo)簽,也可以放置 <link rel="stylesheet">,Shadow DOM 下的樣式都是局部的,所以不用擔(dān)心會(huì)影響到 Shadow DOM 的外部。并且由于這些樣式僅影響局部,所以對(duì)性能也有很大的提升。

在 Shadow DOM 內(nèi)部的樣式中,也有一些特定的選擇器,比如 :host 選擇器,代表著 ShadowRoot,這類似于普通 DOM 中的 :root,并且它可以與其他偽類組合使用,比如當(dāng)鼠標(biāo)在組件上時(shí)::host(:hover),當(dāng)組件擁有某個(gè) class 時(shí)::host(.awesome),當(dāng)組件擁有 disabled 屬性時(shí)::host([disabled])……但是 :host 是擁有繼承屬性的,所以如果在 Custom Elements 外部定義了某些樣式,將會(huì)覆蓋 :host 中的樣式,這樣就可以輕松地實(shí)現(xiàn)各式各樣的“主題風(fēng)格”了。

為了實(shí)現(xiàn)自定義主題,我們還可以使用 Shadow DOM 提供的 :host-context() 選擇器,這個(gè)選擇器允許檢查 Shadow DOM 的任何祖先節(jié)點(diǎn)是否包含指定選擇器。比如如果在最外層 DOM 的 <html> 或 <body> 上有一個(gè) class:.night,則 Shadow DOM 內(nèi)就可以使用 :host-context(.night) 來指定一個(gè)夜晚的主題。這樣可以實(shí)現(xiàn)主題樣式的繼承。

還有一種樣式的定義方式是利用 CSS 變量。我們?cè)?Shadow DOM 中使用變量來指定樣式,比如:background-color: var(--bg-colour, #0F0);,這樣就可以在 Shadow DOM 外面指定 --bg-colour 變量來設(shè)置樣式了,如果沒有指定變量,將使用默認(rèn)的樣式顏色 #0F0。

有時(shí)我們需要在 Shadow DOM 內(nèi)部使用完全自定義的樣式,比如字體樣式、字體大小,如果任由其繼承可能導(dǎo)致布局錯(cuò)亂,而每次在組件外面指定樣式又略顯麻煩,并且也破壞了組件的封裝性。所以,Shadow DOM 提供了一個(gè) all 屬性,只要指定 :host{ all: initial; } 就可以重置所有繼承的屬性。

Demo

Web Components 的 Demo 在網(wǎng)上已經(jīng)有很多了,這是我 2 年前初次接觸 ES6 與 Web Components 的時(shí)候?qū)懙囊粋€(gè) Demo:https://github.com/jinliming2/Calendar-js,一個(gè)日歷,當(dāng)時(shí)還是 v0 的規(guī)范,并且在 Firefox 下還存在會(huì)導(dǎo)致 Firefox 崩潰的 Bug(感覺是 Firefox 在實(shí)現(xiàn) Shadow DOM 時(shí)的 Bug)。目前這個(gè) Demo 已經(jīng)不能在 Firefox 下運(yùn)行了,因?yàn)?Firefox 已經(jīng)刪除了 v0 規(guī)范,開始實(shí)行 v1 標(biāo)準(zhǔn)了,所以近期我可能會(huì)重構(gòu)一下這個(gè) Demo。

關(guān)于什么是原生HTML組件問題的解答就分享到這里了,希望以上內(nèi)容可以對(duì)大家有一定的幫助,如果你還有很多疑惑沒有解開,可以關(guān)注創(chuàng)新互聯(lián)行業(yè)資訊頻道了解更多相關(guān)知識(shí)。

網(wǎng)頁標(biāo)題:什么是原生HTML組件
當(dāng)前鏈接:http://muchs.cn/article10/gphpgo.html

成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供定制開發(fā)網(wǎng)站維護(hù)、網(wǎng)站排名關(guān)鍵詞優(yōu)化、企業(yè)建站、ChatGPT

廣告

聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來源: 創(chuàng)新互聯(lián)

成都seo排名網(wǎng)站優(yōu)化