溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

什么是原生HTML組件

發(fā)布時間:2020-07-13 16:14:11 來源:億速云 閱讀:1125 作者:Leah 欄目:web開發(fā)

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

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

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

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

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

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

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

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

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

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

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

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

什么是原生HTML組件

Shadow DOM

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

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

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

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

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

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

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

我們來看一看瀏覽器利用 Shadow DOM 實現(xiàn)的一個示例吧,那就是 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)部實現(xiàn)的 Shadow DOM 節(jié)點的,需要在設(shè)置中啟用:

什么是原生HTML組件

什么是原生HTML組件

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

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

什么是原生HTML組件

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

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

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

什么是原生HTML組件

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

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

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

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

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

ShadowRoot

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

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

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

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

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

什么是原生HTML組件

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

HTML Template

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

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

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

什么是原生HTML組件

通過 document-fragment 對象,就可以訪問到 template 內(nèi)部的節(jié)點了,通過 document.importNode 方法,可以將 document-fragment 對象創(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>,每個組件都要定義一個 <template>。

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

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

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

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

Custom Elements

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

adoptedCallback(): 組件實例從一個文檔被移動到另一個文檔。

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

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

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

注冊之后,我們就可以使用了,可以直接在 html 文檔中寫對應(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ù)幾個標(biāo)簽允許自關(guān)閉,所以,在 html 中寫 Custom Elements 的節(jié)點時必須帶上關(guān)閉標(biāo)簽。

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

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

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

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

什么是原生HTML組件

Custom Elements + Shadow DOM

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

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

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

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

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

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

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

<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) 表示這是一個開放的 Shadow DOM,下面的節(jié)點是直接從 template 中 clone 過來的,瀏覽器自動在三個 <slot> 標(biāo)簽下放置了幾個灰色的 <div> 節(jié)點,實際上這些灰色的 <div> 節(jié)點表示的是到其真實節(jié)點的“引用”,鼠標(biāo)移動到他們上會顯示一個 reveal 鏈接,點擊這個鏈接即可跳轉(zhuǎn)至其真實節(jié)點。

什么是原生HTML組件

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

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

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

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

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

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

Demo

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

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

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI