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



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

自定義元素
首先,創(chuàng)建一個 index.html,在里面直接調(diào)用 組件。
<body>
??<book-card>book-card>
??<script?src="./BookCard.js">script>
body>
因為瀏覽器不認識 所以,我們需要在 BookCard.js 里注冊它,并在 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)實現(xiàn)了最基礎(chǔ)的 DOM 結(jié)構(gòu)了:

上面一直執(zhí)行
createElement并設(shè)置屬性值的行為是否有點像 React 的React.createElement呢?
HTML 模板
這樣一行一行地生成 DOM 結(jié)構(gòu)不僅寫的累,讀的人也很難一下子看明白。為了解決這個問題,我們可以使用 HTML 模板 。直接在 HTML 里寫一個 模板:
<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>
然后在注冊組件中直接調(diào)用這個模板即可:
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)
這樣的
書寫方式是不是又有點像我們熟悉的Vue框架了呢?
寫樣式
搞定了 DOM 之后,我們就可以寫樣式了,直接在 里面新加一個 元素,然后開始寫 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>
相信大家都會寫這樣的 CSS,所以為了縮減篇幅就把 CSS 折疊了,最后效果如下:

Shadow DOM
為了不讓 里的 CSS 和全局的 CSS 有沖突,我們可以將組件掛在到 Shadow Root 上,再用 Shadow Root 掛到外層的 document DOM 上,這樣就可以實現(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)
打開控制臺,可以看到 的 DOM 都被掛到 Shadow Root 上了:

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

Shadow DOM 的一大優(yōu)點是能將 DOM 結(jié)構(gòu)、樣式、行為與 Document DOM 隔離開,非常適合做組件的封裝,因此它能成為 Web Component 的重要組成部分之一。
Shadow DOM 也經(jīng)常出現(xiàn)在我們?nèi)粘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)
一般來說,對于自定義屬性,我們習(xí)慣使用 data-xxx 來命名。
如果你能進一步思考的話,可以直接拿到這些
attributes,然后用來做 Proxy 代理、響應(yīng)式賦值等操作。嗯,有 Vue 那味了!
const?prefix?=?'data-'
const?attrList?=?Array.from(this.attributes);
const?props?=?attrList.filter(attr?=>?attr.name.startsWith(prefix))
//?監(jiān)聽?props
watch(props)
Slot
HTML 模板的另一個好處是可以像 Vue 一樣使用 。比如,現(xiàn)在我們可以在這個 最下面添加一個 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)別人要使用 組件時,就可以通過插槽的方式來使用注入他們自定義的內(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">購買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 那樣去給元素綁定事件,比如這里對這個 action 添加點擊事件:
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ù),因為需要綁定 this,將其指向這個 BookCard 組件。
UI 庫
或許有的同學(xué)已經(jīng)開始想:既然這玩意能用來封裝組件,那是有對應(yīng)的 Web Component UI 庫呢?答案是肯定的。
目前比較出名的有微軟出品的 FAST[2]:

有 Google 出品的 Lit[3]:

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

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

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