【W(wǎng)eb技術(shù)】1233- 秒懂 Web Component
是什么
Web Components 實(shí)際上一系列技術(shù)的組合,主要包含 3 部分:
自定義元素。 在 HTML 基礎(chǔ)標(biāo)簽外擴(kuò)展自定義標(biāo)簽元素 Shadow DOM。 主要用于將 Shadow DOM 的內(nèi)容與外層 document DOM 隔離 HTML 模板。 使用 來(lái)定義組件模板,使用作為插槽使用
也正是因?yàn)樗且幌盗?API 的組合,所以在使用時(shí),我們要同時(shí)關(guān)注這些 API 的兼容性:



將上面技術(shù)合理使用后,就可以 將功能、邏輯封裝到自定義標(biāo)簽中,通過(guò)復(fù)用這些自定義的組件來(lái)提高開(kāi)發(fā)效率。 聽(tīng)起來(lái)就像是 Vue.js 和 React 做的那一套,實(shí)際上,在使用 Web Components 的時(shí)候,也是很像的。
上手
接下來(lái)我們通過(guò)實(shí)現(xiàn)一個(gè) Web Component 來(lái)學(xué)習(xí)一下怎么使用它吧。

自定義元素
首先,創(chuàng)建一個(gè) index.html,在里面直接調(diào)用 組件。
<body>
??<book-card>book-card>
??<script?src="./BookCard.js">script>
body>
因?yàn)闉g覽器不認(rèn)識(shí) 所以,我們需要在 BookCard.js 里注冊(cè)它,并在 index.html 中引入并執(zhí)行 BookCard.js。
class?BookCard?extends?HTMLElement?{
??constructor()?{
????super();
????const?container?=?document.createElement('div')
????container.className?=?'container'
????const?image?=?document.createElement('img')
????image.className?=?'image'
????image.src?=?"https://pic1.zhimg.com/50/v2-a6d65e05ec8db74369f3a7c0073a227a_200x0.webp"
????const?title?=?document.createElement('p')
????title.className?=?'title'
????title.textContent?=?'切爾諾貝利的祭禱'
????const?desc?=?document.createElement('p')
????desc.className?=?'desc'
????desc.textContent?=?'S·A·阿列克謝耶維奇'
????const?price?=?document.createElement('p')
????price.className?=?'price'
????price.textContent?=?`¥25.00`
????container.append(image,?title,?desc,?price)
????this.appendChild(container)
??}
}
customElements.define('book-card',?BookCard)
上面已經(jīng)實(shí)現(xiàn)了最基礎(chǔ)的 DOM 結(jié)構(gòu)了:

上面一直執(zhí)行
createElement并設(shè)置屬性值的行為是否有點(diǎn)像 React 的React.createElement呢?
HTML 模板
這樣一行一行地生成 DOM 結(jié)構(gòu)不僅寫(xiě)的累,讀的人也很難一下子看明白。為了解決這個(gè)問(wèn)題,我們可以使用 HTML 模板 。直接在 HTML 里寫(xiě)一個(gè) 模板:
<body>
??<template?id="book-card-template">
????<div?class="container">
??????<img?class="image"?src="https://pic1.zhimg.com/50/v2-a6d65e05ec8db74369f3a7c0073a227a_200x0.webp"?alt="">
??????<p?class="title">切爾諾貝利的祭禱p>
??????<p?class="desc">S·A·阿列克謝耶維奇p>
??????<p?class="price">¥25.00p>
????div>
??template>
??<book-card>book-card>
??<script?src="BookCard.js">script>
body>
然后在注冊(cè)組件中直接調(diào)用這個(gè)模板即可:
class?BookCard?extends?HTMLElement?{
??constructor()?{
????super();
????const?templateElem?=?document.getElementById('book-card-template')
????const?clonedElem?=?templateElem.content.cloneNode(true)
????this.appendChild(clonedElem)
??}
}
customElements.define('book-card',?BookCard)
這樣的
書(shū)寫(xiě)方式是不是又有點(diǎn)像我們熟悉的Vue框架了呢?
寫(xiě)樣式
搞定了 DOM 之后,我們就可以寫(xiě)樣式了,直接在 里面新加一個(gè) 元素,然后開(kāi)始寫(xiě) CSS:
<body>
??<template?id="book-card-template">
????<style>
??????p?{?margin:?0?}
??????.container?{?display:?inline-flex;?flex-direction:?column;?border-radius:?6px;?border:?1px?solid?silver;?padding:?16px;?margin-right:?16px?}
??????.image?{?border-radius:?6px;?}
??????.title?{?font-weight:?500;?font-size:?16px;?line-height:?22px;?color:?#222;?margin-top:?14px;?margin-bottom:?9px;?}
??????.desc?{?margin-bottom:?12px;?line-height:?1;?color:?#8590a6;?font-size:?14px;?}
??????.price?{?color:?#8590a6;?font-size:?14px;?}
????style>
????
????<div?class="container">
??????<img?class="image"?src="https://pic1.zhimg.com/50/v2-a6d65e05ec8db74369f3a7c0073a227a_200x0.webp"?alt="">
??????<p?class="title">切爾諾貝利的祭禱p>
??????<p?class="desc">S·A·阿列克謝耶維奇p>
??????<p?class="price">¥25.00p>
????div>
??template>
??<book-card>book-card>
??<script?src="BookCard.js">script>
body>
相信大家都會(huì)寫(xiě)這樣的 CSS,所以為了縮減篇幅就把 CSS 折疊了,最后效果如下:

Shadow DOM
為了不讓 里的 CSS 和全局的 CSS 有沖突,我們可以將組件掛在到 Shadow Root 上,再用 Shadow Root 掛到外層的 document DOM 上,這樣就可以實(shí)現(xiàn) CSS 的隔離啦:
class?BookCard?extends?HTMLElement?{
??constructor()?{
????super();
????this.attachShadow({?mode:?'open'?})
????const?templateElem?=?document.getElementById('book-card-template')
????const?clonedElem?=?templateElem.content.cloneNode(true)
????this.shadowRoot.appendChild(clonedElem)
??}
}
customElements.define('book-card',?BookCard)
打開(kāi)控制臺(tái),可以看到 的 DOM 都被掛到 Shadow Root 上了:

整個(gè) DOM 架構(gòu)大致是這樣的:

Shadow DOM 的一大優(yōu)點(diǎn)是能將 DOM 結(jié)構(gòu)、樣式、行為與 Document DOM 隔離開(kāi),非常適合做組件的封裝,因此它能成為 Web Component 的重要組成部分之一。
Shadow DOM 也經(jīng)常出現(xiàn)在我們?nèi)粘i_(kāi)發(fā)中,比如 元素里的 controls 控件 DOM 結(jié)構(gòu)也是掛在 Shadow Root 下的:

Props
和 Vue 和 React 的組件一樣,我們也可以在 Web Component 上傳遞屬性:
<body>
??<template?id="book-card-template">
????<style>
??????...
????style>
????<div?class="container">
??????<img?class="image"?src=""?alt="">
??????<p?class="title">p>
??????<p?class="desc">p>
??????<p?class="price">p>
????div>
??template>
??<book-card
????data-image="https://pic1.zhimg.com/50/v2-a6d65e05ec8db74369f3a7c0073a227a_200x0.webp"
????data-title="切爾諾貝利的祭禱"
????data-desc="S·A·阿列克謝耶維奇"
????data-price="25.00"
??>book-card>
??
??<script?src="BookCard.js">script>
body>
然后將這些屬性再更新到 DOM 上:
const?prefix?=?'data-'
class?BookCard?extends?HTMLElement?{
??constructor()?{
????super();
????this.attachShadow({?mode:?'open'?})
????const?templateElem?=?document.getElementById('book-card-template')
????const?clonedElem?=?templateElem.content.cloneNode(true)
????clonedElem.querySelector('.container?>?.image').src?=?this.getAttribute(`${prefix}image`)
????clonedElem.querySelector('.container?>?.title').textContent?=?this.getAttribute(`${prefix}title`)
????clonedElem.querySelector('.container?>?.desc').textContent?=?this.getAttribute(`${prefix}desc`)
????clonedElem.querySelector('.container?>?.price').textContent?=?`¥${this.getAttribute(`${prefix}price`)}`
????this.shadowRoot.appendChild(clonedElem)
??}
}
customElements.define('book-card',?BookCard)
一般來(lái)說(shuō),對(duì)于自定義屬性,我們習(xí)慣使用 data-xxx 來(lái)命名。
如果你能進(jìn)一步思考的話,可以直接拿到這些
attributes,然后用來(lái)做 Proxy 代理、響應(yīng)式賦值等操作。嗯,有 Vue 那味了!
const?prefix?=?'data-'
const?attrList?=?Array.from(this.attributes);
const?props?=?attrList.filter(attr?=>?attr.name.startsWith(prefix))
//?監(jiān)聽(tīng)?props
watch(props)
Slot
HTML 模板的另一個(gè)好處是可以像 Vue 一樣使用 。比如,現(xiàn)在我們可以在這個(gè) 最下面添加一個(gè) action 插槽:
<template?id="book-card-template">
??<style>
????...
??style>
??<div?class="container">
????<img?class="image"?src=""?alt="">
????<p?class="title">p>
????<p?class="desc">p>
????<p?class="price">p>
????<div?class="action">
??????<slot?name="action-btn">slot>
????div>
??div>
template>
當(dāng)別人要使用 組件時(shí),就可以通過(guò)插槽的方式來(lái)使用注入他們自定義的內(nèi)容了:
<book-card
??data-image="https://pic1.zhimg.com/50/v2-a6d65e05ec8db74369f3a7c0073a227a_200x0.webp"
??data-title="切爾諾貝利的祭禱"
??data-desc="S·A·阿列克謝耶維奇"
??data-price="25.00"
>
??<button?slot="action-btn"?class="btn?primary">購(gòu)買(mǎi)button>
book-card>
<book-card
????data-image="https://pic1.zhimg.com/50/v2-a6d65e05ec8db74369f3a7c0073a227a_200x0.webp"
????data-title="切爾諾貝利的祭禱"
????data-desc="S·A·阿列克謝耶維奇"
????data-price="25.00"
>
??<button?slot="action-btn"?class="btn?danger">刪除button>
book-card>

交互
我們還能像 React 那樣去給元素綁定事件,比如這里對(duì)這個(gè) action 添加點(diǎn)擊事件:
class?BookCard?extends?HTMLElement?{
??constructor()?{
????super();
????this.attachShadow({?mode:?'open'?})
????const?templateElem?=?document.getElementById('book-card-template')
????const?clonedElem?=?templateElem.content.cloneNode(true)
????...
????clonedElem.querySelector('.container?>?.action').addEventListener('click',?this.onAction)
????this.shadowRoot.appendChild(clonedElem)
??}
??
??onAction?=?()?=>?{
????alert('Hello?World')
??}
}

注意:這里的 onAction 為箭頭函數(shù),因?yàn)樾枰壎?this,將其指向這個(gè) BookCard 組件。
UI 庫(kù)
或許有的同學(xué)已經(jīng)開(kāi)始想:既然這玩意能用來(lái)封裝組件,那是有對(duì)應(yīng)的 Web Component UI 庫(kù)呢?答案是肯定的。
目前比較出名的有微軟出品的 FAST[2]:

有 Google 出品的 Lit[3]:

還有有我廠騰訊的 OMI[4]:

總結(jié)
上面主要給大家分享了一下 Web Component 的一些使用方法??偟膩?lái)說(shuō),Web Component 是一系列 API 的組合:
Custom Element:注冊(cè)和使用組件 Shadow DOM:隔離 CSS HTML template 和 slot:靈活的 DOM 結(jié)構(gòu)
關(guān)于 Web Component 的內(nèi)容,已經(jīng)差不多說(shuō)完了,不知大家看完有什么感受。給我的感覺(jué)是好像提供了原生組件化封裝功能,但是又有好多事沒(méi)有做完,比如我們希望看到的:
像 Vue 那樣的響應(yīng)式地對(duì)數(shù)據(jù)進(jìn)行追蹤 像 Vue 那樣的模板語(yǔ)法、數(shù)據(jù)綁定 像 React 那樣的 DOM Diff 像 React 那樣的 React.createElement的 JSX 寫(xiě)法...
這些我們希望的 “框架特性” Web Component 都是沒(méi)有提供的。這是因?yàn)?Web Component 的內(nèi)容都是由 API 組成,而這些 API 作為規(guī)范要保持功能單一、正交的原則,而不是要做得像 Vue, React 那樣的組件化 “框架”。這也是知乎的 《Web Component 和類(lèi) React、Angular、Vue 組件化技術(shù)誰(shuí)會(huì)成為未來(lái)?》[5] 回答里說(shuō)的:
框架的職責(zé)在于提供一整套的解決方案,而平臺(tái) API 的設(shè)計(jì)要求是絕不能提供一整套的解決方案(即保證零耦合、正交),這是無(wú)法調(diào)和的基本矛盾所在。
Web Component 最主要的好處還是在于原生支持、無(wú)需外部依賴。一個(gè) index.html + main.js 就可以完成組件注冊(cè)以及使用。
目前,它依然在發(fā)展,也能用于生產(chǎn)環(huán)境中,像 single-spa Layout Engine 以及 MicroApp 就是例子,另一個(gè)場(chǎng)景則是可以在 TextArea, Video 這樣的功能組件中使用到 Web Component 來(lái)封裝。
參考資料
項(xiàng)目代碼: https://github.com/haixiangyan/bookcard-web-component
[2]FAST: https://www.fast.design/
[3]Lit: https://lit.dev/
[4]OMI: https://tencent.github.io/omi/
[5]《Web Component 和類(lèi) React、Angular、Vue 組件化技術(shù)誰(shuí)會(huì)成為未來(lái)?》: https://www.zhihu.com/question/58731753/answer/158331367

回復(fù)“加群”與大佬們一起交流學(xué)習(xí)~
點(diǎn)擊“閱讀原文”查看 130+ 篇原創(chuàng)文章
