組件化是前端工程化重要的一環(huán),UI 和 交互(或邏輯)的復(fù)用極大的提高開(kāi)發(fā)效率以及減少代碼冗余。
目前開(kāi)源的組件庫(kù)都是特定于框架的,比如:基于 Vue 的 Element UI,基于 React 的 Ant Design 等。也就是說(shuō)這些組件庫(kù)無(wú)法做到跨框架使用。
Web Component 是一組 web 原生 API ,可以創(chuàng)建可復(fù)用的自定義元素 (custom elements)。組件完全是原生 JavaScript API 開(kāi)發(fā)的,可以跨框架使用。此外,web components 并不是一個(gè)單一的規(guī)范,而是三個(gè)獨(dú)立的 web 技術(shù)的集合。
- Custom elements(自定義元素)
- Shadow DOM (影子元素)
- HTML template (HTML 模版)
基本實(shí)現(xiàn)思路:
- 創(chuàng)建類(lèi)或者函數(shù)來(lái)實(shí)現(xiàn)組件的邏輯、交互功能
- 創(chuàng)建 Shadow DOM 并附加在自定義元素上,往 Shadow DOM 中添加要展示的元素
- 通過(guò) customElements.define 方法注冊(cè)
- 在頁(yè)面中使用注冊(cè)的自定義元素
Custom elements
customElements存在于 window 全局對(duì)象上,對(duì)自定義元素提供支持,包含四個(gè) API:define、get、whenDefined、 upgrade
customElements.define
自定義元素通過(guò) customElements.define 方法注冊(cè),包含三個(gè)參數(shù):
- 自定義元素名,格式:短線連接的字符串
- 自定義元素構(gòu)造器
- 可選的,含有 extends 屬性的對(duì)象。指定所創(chuàng)建的元素繼承自哪個(gè)內(nèi)置元素,可以繼承任何內(nèi)置元素
可以創(chuàng)建兩種類(lèi)型的自定義元素:
1. 自主定制元素
獨(dú)立元素,不會(huì)從內(nèi)置 HTML 元素繼承
class WebCTitle extends HTMLElement {
constructor() {
super()
this.attachShadow({ mode: 'open' })
this.style()
this.render()
}
style() {
const style = document.createElement('style')
style.textContent = `
.title {
color: red;
font-size: 20px;
}
`
this.shadowRoot.appendChild(style)
}
render() {
const template = document.createElement('template')
template.innerHTML = `
<div class="title">Hello!</div>
`
this.shadowRoot.appendChild(template.content.cloneNode(true))
}
}
customElements.define('webc-title', WebCTitle)
Usage
<webc-title></webc-title>
2. 自定義內(nèi)置元素
元素繼承并擴(kuò)展自?xún)?nèi)置 HTML 元素
class WebCTitle extends HTMLParagraphElement {
// 邏輯同上
}
customElements.define('webc-title', WebCTitle, {
extends: 'p'
})
Usage
<p is="webc-title"></p>
自定義元素標(biāo)準(zhǔn)需要滿(mǎn)足的條件:
- 必須用短線連接
- 元素名必須小寫(xiě)
- 不要多次注冊(cè)
- 元素不能自閉合
customElements.get
獲取自定義元素的類(lèi),一般用于擴(kuò)展第三方組件庫(kù)。比如:
const WebCProp = customElements.get('webc-prop')
customElements.define('webc-props-plus', class extends WebCProp{
constructor() {
super()
this.click = () => {}
}
})
whenDefined
一般是將 <script>
標(biāo)簽放在頁(yè)面底部,為的是讓瀏覽器渲染引擎先解析 DOM,然后再解析 JavaScript。
當(dāng)渲染引擎讀取到自定義元素的時(shí)候,并不知道它是什么元素(此時(shí)注冊(cè)腳本還沒(méi)執(zhí)行),一般來(lái)說(shuō)當(dāng)渲染引擎碰到一個(gè)不認(rèn)識(shí)的元素的時(shí)候,會(huì)認(rèn)為這是一個(gè)無(wú)效的元素。但是,為了原生元素區(qū)分,自定義元素的命名規(guī)則必須包含短線,所有當(dāng)渲染引擎解析一個(gè)帶有短線的非原生元素時(shí),會(huì)認(rèn)為是一個(gè)未定義的自定義元素,不會(huì)當(dāng)作一個(gè)無(wú)效元素。當(dāng)執(zhí)行到注冊(cè)自定義元素的代碼時(shí),就會(huì)將之前未定義的元素標(biāo)記為定義的元素。
定義的元素對(duì)應(yīng)的偽類(lèi)選擇器就是 :defined
,未定義的元素對(duì)應(yīng)的偽類(lèi)選擇器就是 :not(:defined)
通過(guò)這個(gè)偽類(lèi)選擇器,可以在定義元素之前的空白時(shí)間內(nèi),設(shè)置自定義元素的加載樣式。
whenDefine 是元素定義后觸發(fā)的回調(diào),通常用于異步注冊(cè)組件的時(shí)候。
<style>
/* :not() 匹配不符合條件的元素 */
:not(:defined) {
display: block
width: 200px;
height: 200px;
color: red;
border: 1px solid pink;
}
:defined {
color: blue;
}
</style>
<when-define>loading...</when-define>
<script src="./whenDefine.js"></script>
class WhenDefine extends HTMLElement {
constructor() {
super()
this.attachShadow({ mode: 'open' })
}
render() {
const template = document.createElement('template')
template.innerHTML = `<div>hello</div>`
this.shadowRoot.appendChild(template.content.cloneNode(true))
}
connectedCallback() {
this.render()
}
}
setTimeout(() => {
customElements.define('when-define', WhenDefine)
}, 3000)
customElements.whenDefined('when-define').then(() => {
console.log('注冊(cè)完成')
})
upgrade
upgrade 升級(jí),如果在定義元素之前先通過(guò) js 創(chuàng)建了元素,則元素實(shí)例并不是自定義元素類(lèi)的實(shí)例,則通過(guò) upgrade 將其升級(jí):
// 創(chuàng)建
const webTitle = document.createElement('webc-title')
// 定義并注冊(cè)
class WebTitle extends HTMLElement {}
customElements.define('web-title', WebTitle)
webTitle instanceof WebTitle // false
// 升級(jí)元素
customElements.upgrade(webTitle)
webTitle instanceof WebTitle // true
生命周期
構(gòu)造函數(shù)中可以指定自定義元素的生命周期,將會(huì)在不同階段調(diào)用。具體包括四個(gè):
connectedCallback
:當(dāng) custom element 首次被插入文檔 DOM 時(shí),被調(diào)用。disconnectedCallback
:當(dāng) custom element 從文檔 DOM 中刪除時(shí),被調(diào)用。adoptedCallback
:當(dāng) custom element 被移動(dòng)到新的文檔時(shí),被調(diào)用。attributeChangedCallback
: 當(dāng) custom element 增加、刪除、修改自身屬性時(shí),被調(diào)用。
function updateStyle(ele) {
const shadow = ele.shadowRoot
shadow.querySelector('style').textContent = `
.title {
color: #fff;
font-size: 24px;
text-align: center;
}
.dv {
width: ${ele.getAttribute('w')}px;
height: ${ele.getAttribute('h')}px;
background-color: ${ele.getAttribute('c')};
}
`
}
class WebCTitle extends HTMLElement {
static get observedAttributes() {
return ['w', 'h', 'c']
}
constructor() {
super()
this.attachShadow({ mode: 'open' })
this.style()
this.render()
}
style() {
const style = document.createElement('style')
this.shadowRoot.appendChild(style)
}
render() {
const template = document.createElement('template')
template.innerHTML = `
<div class="title">
<div class="dv">Hello!</div>
</div>
`
this.shadowRoot.appendChild(template.content.cloneNode(true))
}
connectedCallback() {
updateStyle(this)
}
disconnectedCallback() {
console.log('從文檔移除了')
}
adoptedCallback() {}
attributeChangedCallback(name, oldValue, newValue) {
console.log(name, oldValue, newValue)
updateStyle(this)
}
}
當(dāng)元素屬性變化后,要觸發(fā)attributeChangedCallback()
回調(diào)函數(shù),必須通過(guò) 構(gòu)造器的靜態(tài)屬性 observedAttributes()
的 get 函數(shù)來(lái)實(shí)現(xiàn),函數(shù)返回一個(gè)數(shù)組,包含需要監(jiān)聽(tīng)的屬性名稱(chēng):
static get observedAttributes() {
return ['w', 'h', 'c']
}
slot
slot
屬性返回已插入元素所在的 Shadow DOM slot 的名稱(chēng)。
connectedCallback() {
console.log(this.querySelector('p').slot) // desc
}
Shadow DOM
Shadow DOM 是很重要的 API,可以將結(jié)構(gòu)、樣式和行為隱藏起來(lái),獨(dú)立的將 DOM 附加到一個(gè)元素上與頁(yè)面上的其他代碼隔離。在微前端 qiankun 框架中就是通過(guò) shadow DOM 來(lái)實(shí)現(xiàn)不同容器樣式隔離的,還有 video 標(biāo)簽的按鈕和控制器。
Shadow DOM 術(shù)語(yǔ):
Shadow host:一個(gè)常規(guī) DOM 節(jié)點(diǎn),Shadow DOM 會(huì)被附加到這個(gè)節(jié)點(diǎn)上
Shadow tree:Shadow DOM 內(nèi)部的 DOM 樹(shù)
Shadow boundary:Shadow DOM 結(jié)束的地方,也是常規(guī) DOM 開(kāi)始的地方
Shadow root: Shadow tree 的根節(jié)點(diǎn)
用法
Element.attachShadow
Element.attachShadow() 方法將一個(gè) shadow root 附加到一個(gè)元素上。該方法接受一個(gè)對(duì)象參數(shù),對(duì)象的屬性為 mode,值為 open 或 closed。
- open:可以通過(guò) js API Element.shadowRoot 屬性獲取到 Shadow DOM
- closed:無(wú)法通過(guò) js 獲取 shadowRoot
將 shadow DOM 附加到元素后,就可以對(duì) shadow DOM 進(jìn)行常規(guī)的 DOM 操作了。
注意:Shadow DOM 內(nèi)部的樣式不會(huì)影響到外部,可以做到樣式隔離。
shadowRoot
shadowRoot 就是通過(guò) elements.attachShadow() 附加在元素上的 shadow DOM 的根結(jié)點(diǎn)。它是 ShadowRoot 的實(shí)例,繼承關(guān)系是 ShadowRoot --> DocumentFragment --> Node。所以具有 DOM 的常規(guī)屬性和方法。專(zhuān)屬屬性包括:
shadowRoot.host
附加的宿主 DOM 元素
shadowRoot.innerHTML
shadowRoot 內(nèi)部的 DOM 樹(shù)
shadowRoot.mode
只讀,值為 open OR closed
Templates & Slots
Templates
當(dāng)某段模版重復(fù)使用時(shí),可以通過(guò) template 模版去定義,生成一個(gè)文檔片段,并且不會(huì)顯示在頁(yè)面中。可以通過(guò) js 獲取,然后添加到 DOM 中。
<template id="tpl">
<div class="title">
<div class="dv">hello</div>
</div>
</template>
const template = document.querySelector('#tpl')
console.dir(template.content)
修改 render 方法
render() {
const template = document.querySelector('#tpl')
this.shadowRoot.appendChild(template.content.cloneNode(true))
}
template.content 為 DocumentFragment 的實(shí)例,該構(gòu)造器繼承自 Node。可以通過(guò) cloneNode 方法拷貝文檔片段,添加到 shadow DOM 中。這樣的話其實(shí)可以把 style 也定義在 template 中。
<template id="tpl">
<style>
.title {
color: #fff;
font-size: 24px;
text-align: center;
}
.dv {
width: 100px;
height: 100px;
background-color: red;
}
</style>
<div class="title">
<div class="dv">hello</div>
</div>
</template>
class WebCTitle extends HTMLElement {
constructor() {
super()
this.attachShadow({ mode: 'open' })
this.render()
}
render() {
const template = document.querySelector('#tpl')
this.shadowRoot.appendChild(template.content.cloneNode(true))
}
}
Slots
template 適用于固定 html 片段,但是在某些場(chǎng)景下元素不靈活。web component 支持 slots,類(lèi)似于 Vue 中的 slot。并且支持具名插槽。
<template id="tpl">
<div class="title">
<div class="dv">
<slot>默認(rèn)標(biāo)題</slot>
<slot name="desc">默認(rèn)描述</slot>
</div>
</div>
</template>
<webc-title>
<span>標(biāo)題</span>
<p slot="desc">描述信息</p>
</webc-title>
slotchange
如果 添加/刪除 了插槽元素,瀏覽器將監(jiān)視插槽并更新渲染。另外,由于不復(fù)制 light DOM 節(jié)點(diǎn),而是僅在插槽中進(jìn)行渲染,所以?xún)?nèi)部的變化是立即可見(jiàn)的。因此無(wú)需執(zhí)行任何操作即可更新渲染。但如果組件想知道插槽的更改,那么可以用 slotchange
事件。
slotchange事件會(huì)在插槽第一次填充時(shí)觸發(fā),并且在插槽元素的 添加/刪除/替換 操作(而不是其子元素)時(shí)觸發(fā),插槽是
event.target
class WebCList extends HTMLElement {
constructor() {
super()
this.attachShadow({ mode: 'open' })
this.render()
}
render() {
const template = document.createElement('template')
template.innerHTML = `
<div>
<h1><slot name="title">默認(rèn)標(biāo)題</slot></h1>
<ul>
<slot name="item"></slot>
</ul>
</div>
`
this.shadowRoot.appendChild(template.content.cloneNode(true))
}
connectedCallback() {
// 默認(rèn)執(zhí)行一次
this.shadowRoot.firstElementChild
.querySelector('ul')
.addEventListener('slotchange', e => {
// e.target 為 slot
// console.dir(e.target)
// console.dir(e.target.name)
// slot.assignedNodes({flatten: true/false}) – 分配給插槽的 DOM 節(jié)點(diǎn)。默認(rèn)情況下,flatten 選項(xiàng)為 false。如果顯式地設(shè)置為 true,則它將更深入地查看扁平化 DOM ,如果嵌套了組件,則返回嵌套的插槽,如果未分配節(jié)點(diǎn),則返回備用內(nèi)容。
// console.dir(e.target.assignedNodes())
// 分配給插槽的 DOM 元素(與上面相同,但僅元素節(jié)點(diǎn))
console.log(e.target.assignedElements())
console.log(e.target.assignedElements()[1].assignedSlot) // 返回節(jié)點(diǎn)的插槽
})
}
}
customElements.define('webc-list', WebCList)
<webc-list id="list">
<h3 slot="title">TODO list</h3>
<li slot="item">mike</li>
<li slot="item">rose</li>
<li slot="item">jack</li>
</webc-list>
2 秒后動(dòng)態(tài)增加 li 元素
const list = document.querySelector('#list')
setTimeout(() => {
const li = document.createElement('li')
li.setAttribute('slot', 'item')
li.textContent = 'mark'
list.appendChild(li)
}, 2000)
Shadow DOM 樣式
shadow DOM 可以包含 <style>
和 <link rel="stylesheet" href="">
標(biāo)簽。在使用 link 時(shí),樣式表是 HTTP 緩存的,因此不會(huì)為使用同一模板的多個(gè)組件重新下載樣式表。
:host
:host
選擇器可以選擇 shadow 宿主(包含 shadow 樹(shù)的元素)
例如,我們正在創(chuàng)建 <webc-dialog>
元素,并且想讓它居中。那么需要對(duì) <webc-dialog>
元素本身設(shè)置樣式。這就需要用 :host 設(shè)置
<template id="tpl">
<style>
/* 這些樣式將從內(nèi)部應(yīng)用到 custom-dialog 元素上 */
:host {
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
display: inline-block;
border: 1px solid red;
padding: 10px;
}
</style>
<slot></slot>
</template>
<script>
const tpl = document.querySelector('#tpl')
customElements.define(
'webc-dialog',
class extends HTMLElement {
connectedCallback() {
this.attachShadow({ mode: 'open' }).append(tpl.content.cloneNode(true))
}
}
)
</script>
<webc-dialog> Hello! </webc-dialog>
:host(selector)
與 :host
相同,但僅在 shadow 宿主與 selector
匹配時(shí)才應(yīng)用樣式。
例如,當(dāng) <webc-dialog>
具有 centered
屬性時(shí)才將其居中:
<webc-dialog centered> Hello! </webc-dialog>
:host-context(selector)
與 :host
相同,但僅當(dāng)外部文檔中的 shadow 宿主或它的任何祖先節(jié)點(diǎn)與 selector
匹配時(shí)才應(yīng)用樣式。
例如,:host-context(.dark-theme)
只有在 <webc-dialog>
或者 <webc-dialog>
的任何祖先節(jié)點(diǎn)上有 dark-theme
類(lèi)時(shí)才匹配:
<body class="dark-theme">
<!-- :host-context(.dark-theme) 只應(yīng)用于 .dark-theme 內(nèi)部的 webc-dialog-->
<custom-dialog> hello </custom-dialog>
</body>
shadow 宿主
shadow 宿主( <webc-dialog>
本身)駐留在 light DOM 中,因此它受到文檔 CSS 規(guī)則的影響。
如果在局部的 :host
和文檔中都給一個(gè)屬性設(shè)置樣式,那么文檔樣式優(yōu)先。
<style>
webc-dialog {
padding: 0;
}
</style>
這樣就可以在 :host
規(guī)則中設(shè)置組件的 “默認(rèn)” 樣式,然后在文檔中覆蓋它們。唯一的例外是當(dāng)局部屬性被標(biāo)記 !important
時(shí),對(duì)于這樣的屬性,局部樣式優(yōu)先。
Slot 內(nèi)容樣式
占槽元素來(lái)自 light DOM,所以使用文檔樣式。局部樣式不會(huì)影響占槽內(nèi)容。
在下面的例子中,按照文檔樣式,占槽的 <span>
是粗體,但是它不從局部樣式中獲取 background
:
<style>
span {
font-weight: 600;
}
</style>
<webc-card>
<div slot="username">
<span>Jack</span>
</div>
</webc-card>
<script>
customElements.define(
'webc-card',
class extends HTMLElement {
connectedCallback() {
this.attachShadow({ mode: 'open' })
this.shadowRoot.innerHTML = `
<style>
span {
background: red;
}
</style>
Name: <slot name="username"></slot>
`
}
}
)
</script>
結(jié)果是粗體,但背景不是紅色。
如果想在組件中設(shè)置占槽元素的樣式,有兩種選擇。
- 可以對(duì)
<slot>
本身進(jìn)行樣式化,并借助 CSS 繼承:
customElements.define(
'webc-card',
class extends HTMLElement {
connectedCallback() {
this.attachShadow({ mode: 'open' })
this.shadowRoot.innerHTML = `
<style>
slot[name="username"] {
font-weight: bold;
}
</style>
Name: <slot name="username"></slot>
`
}
}
)
-
使用
::slotted(selector)
偽類(lèi)。它根據(jù)兩個(gè)條件來(lái)匹配元素:
- 這是一個(gè)占槽元素,來(lái)自于 light DOM。插槽名并不重要,任何占槽元素都可以,但只能是元素本身,而不是它的子元素 。
- 該元素與
selector
匹配。
在例子中,::slotted(div)
正好選擇了 <div slot="username">
,但是沒(méi)有選擇它的子元素:
<webc-card>
<div slot="username">
<div>jack</div>
</div>
</webc-card>
<script>
customElements.define(
'webc-card',
class extends HTMLElement {
connectedCallback() {
this.attachShadow({ mode: 'open' })
this.shadowRoot.innerHTML = `
<style>
::slotted(div) {
border: 1px solid red;
}
</style>
Name: <slot name="username"></slot>
`
}
}
)
</script>
CSS 變量
通過(guò)給自定義元素中定義 CSS 變量,在 shadow DOM 中的 style 中使用 CSS 變量,這種方式用在開(kāi)發(fā)三方組件庫(kù)時(shí),可以做到樣式自定義。
事件
冒泡
Shadow DOM 內(nèi)部觸發(fā)的事件大都可以向上冒泡。如果是一個(gè) slot 元素,并且事件發(fā)生在內(nèi)部某個(gè)地方,那么會(huì)冒泡到 <slot> 并繼續(xù)向上。
event.composedPath
使用 event.composedPath() 獲得原始事件目標(biāo)的完整路徑及所有 shadow 元素。比如:
customElements.define(
'webc-dialog',
class extends HTMLElement {
connectedCallback() {
this.attachShadow({ mode: 'open' }).append(tpl.content.cloneNode(true))
this.shadowRoot.querySelector('slot').addEventListener('click', e => {
console.log(e.composedPath())
})
}
}
)
對(duì)于 span 上的點(diǎn)擊事件,調(diào)用 event.composedPath()
會(huì)返回一個(gè)數(shù)組,[span, slot, div, document-fragment, webc-dialog, body, html, document, Window]
event.composed
大多數(shù)事件能成功冒泡到 shadow DOM 邊界。很少有事件不能冒泡到 shadow DOM 邊界。
由事件對(duì)象的 composed
屬性控制。如果 composed
是 true
,那么事件就能穿過(guò)邊界。否則它僅能在 shadow DOM 內(nèi)部捕獲。
自定義事件
組件中事件的處理以及向組件外部暴露事件通過(guò)自定義事件來(lái)處理。
當(dāng) dispatch 自定義事件需要設(shè)置 bubbles 和 composed 為 true 使的事件冒泡。
比如 shadow 內(nèi)部觸發(fā)了某個(gè)事件,需要暴露出 API 以供用戶(hù)使用,則通過(guò) dispath 事件,并可以傳遞參數(shù)。
CustomEvent
構(gòu)造器,參數(shù)一是自定義事件類(lèi)型,參數(shù)二對(duì)象,detail 為傳遞的參數(shù)。
customElements.define(
'webc-dialog',
class extends HTMLElement {
connectedCallback() {
this.attachShadow({ mode: 'open' }).append(tpl.content.cloneNode(true))
this.shadowRoot.querySelector('slot').addEventListener('click', e => {
this.dispatchEvent(
new CustomEvent('tab-click', {
bubbles: true,
composed: true,
detail: 123
})
)
})
}
}
)
Usage
<webc-dialog id="dialog"> <span> Hello! </span> </webc-dialog>
<script>
document.querySelector('#dialog').addEventListener('tab-click', e => {
// ...
})
</script>
注意
大部分內(nèi)建事件的 composed 是 true,有些內(nèi)建事件是 false。比如:
-
mouseenter
,mouseleave
-
load
,unload
,abort
,error
select
slotchange
處理數(shù)據(jù)的方式
Attributes
Attributes 是和HTML相關(guān)的概念,是我們定義HTML元素(即HTML標(biāo)記)特征的方式,同樣適用于 web components。
對(duì)于一下元素的, width、height、src、alt是attr。
<img src="./xx.png" alt="hello" width="200" height="200" />
當(dāng)渲染引擎解析 HTML 代碼以創(chuàng)建 DOM 對(duì)象時(shí),它會(huì)識(shí)別標(biāo)準(zhǔn)屬性并從中創(chuàng)建 DOM 屬性。但這只限于標(biāo)準(zhǔn)屬性中,而不是自定義屬性。此外,并非所有元素的標(biāo)準(zhǔn)屬性都相同。例如,id是所有元素通用的標(biāo)準(zhǔn)屬性,而alt屬性只是img上的。
對(duì)于不自動(dòng)映射的自定義屬性,采用一下方法進(jìn)行操作:
element.hasAttributes()
: 元素是否至少具有一個(gè)屬性
element.hasAttribute(name)
: 元素是否具有某個(gè)屬性
element.setAttribute(name, value)
:給元素設(shè)置屬性
element.getAttribute(name)
:返回名為name屬性的值,如果不存在則返回 null
element.getAttributeNames()
:返回元素屬性組成的數(shù)組
element.toggleAttribute(name)
:切換給定元素的某個(gè)布爾值屬性的狀態(tài)(如果屬性不存在則添加屬性,屬性存在則移除屬性)
<webc-list title="hello"></custom-list>
<script>
class WebcList extends HTMLElement {
constructor() {
super();
console.log(this.getAttribute("title"));
}
}
customElements.define("webc-list", WebcList);
</script>
構(gòu)造器方法內(nèi)部的 this 即自定義元素的實(shí)例。
使用類(lèi)的靜態(tài) getter 方法 observedAttributes
列出要監(jiān)聽(tīng)的屬性,當(dāng)屬性變化后會(huì)觸發(fā)生命周期attributeChangedCallback
函數(shù)。
優(yōu)點(diǎn):直觀、簡(jiǎn)單
Properties
Properties是與 JavaScript 相關(guān)的概念。它們是 DOM Node 接口的一部分。
prop可以是任何類(lèi)型的值,并且屬性名區(qū)分大小寫(xiě)。
prop 反映射 到attr
對(duì)于標(biāo)準(zhǔn)attr,attr 會(huì)和 prop 建立映射關(guān)系。 prop 是 HTML attr的JavaScript的表示。就意味著當(dāng)一個(gè)prop被修改時(shí),attr也會(huì)改變,反之亦然。
對(duì)于自定義元素,需要在定義自定義元素時(shí)明確地執(zhí)行此操作。
通過(guò)為我們希望將其值映射到其同名attr的prop定義 getter 和 setter 方法來(lái)做到這一點(diǎn)。
class {
set color(value){
this.setAttribute('color', value)
}
get color(){
this.getAttribute('color')
}
}
假設(shè)我們有一個(gè)自定義元素,其類(lèi)有一個(gè)名為的屬性color,我們希望反映它的同名屬性。給出這種情況,代碼如下:
<img src="aa.jpg"/>
setTimeout(() => {
document.querySelector('img').src="bb.jpg"
},1500)
以上示例,img的src會(huì)在1.5s后改變。
瀏覽器引擎會(huì)為標(biāo)準(zhǔn)的 HTML attr 創(chuàng)建 prop,但不會(huì)為自定義元素創(chuàng)建。
使用 Attributes 適用于傳遞字符串。否則無(wú)論傳遞什么類(lèi)型的數(shù)據(jù)都會(huì)被轉(zhuǎn)成字符串,比如數(shù)字、函數(shù)、對(duì)象、數(shù)組等。
傳遞對(duì)象需要通過(guò) JSON.stringify 序列化成字符串,在組件中通過(guò) JSON.parse 反序列化成對(duì)象。這種方式很不合理,當(dāng)對(duì)象很大時(shí),不僅繁瑣而且 DOM 結(jié)構(gòu)冗余。
既然 DOM 也是對(duì)象,那么是不是可以給其任意的添加刪除屬性?當(dāng)然可以。
const dialog = document.querySelector('#dialog')
dialog.list = [1, 2, 3]
console.log(dialog.list)
這種方式不僅可以傳遞對(duì)象,甚至可以傳遞任何類(lèi)型的數(shù)據(jù)。
優(yōu)點(diǎn):可以處理任何數(shù)據(jù)類(lèi)型。更適合處理復(fù)雜數(shù)據(jù)類(lèi)型
自定義事件
就是以上提到的自定義事件
event bus
這種方式不再是監(jiān)聽(tīng)自定義元素的事件,而是定義了一個(gè)全局的事件總線,這樣就可以在任何地方使用。類(lèi)似于 Node 中的 EventEmitter 模塊,不同的是讓 DOM 管理事件。
class EventBus {
constructor() {
this._bus = document.createElement('div')
}
regisger(event, callback) {
this._bus.addEventListener(event, callback)
}
remove(event, callback) {
this._bus.removeEventListener(event, callback)
}
fire(event, detail = {}) {
this._bus.dispatchEvent(new CustomEvent(event, detail))
}
}
const bus = new EventBus()
bus.regisger('clicked', () => {
console.log(123)
})
bus.fire('clicked', { value: 23 })
優(yōu)勢(shì):適合組件之間的通信
總結(jié)
web components 的諸多好處:
封裝
封裝是 web components最重要的特性和好處。封裝確保我們的代碼與組件所屬頁(yè)面中已經(jīng)存在的任何框架或功能的任何其他元素隔離,從而避免沖突和不可控的行為。
可重用性
封裝和 ESM 實(shí)現(xiàn)了可重用性。使得開(kāi)發(fā)者能夠編寫(xiě)可在許多站點(diǎn)和平臺(tái)上使用的可重用組件。
靈活性
可以通過(guò)多種方式自定義 Web 組件。例如,使用attributes/properties自定義行為,使用slot自定義內(nèi)容,使用 CSS 變量自定義樣式。提供了很大的靈活性,使得一個(gè)原始組件可以有很多不同的形態(tài)。
表現(xiàn)
web components 為以前只能使用第三方庫(kù)提供的某些功能提供了標(biāo)準(zhǔn)規(guī)范。這樣可以免除外部依賴(lài)。同時(shí)意味著可以減少我們的代碼和捆綁包的復(fù)雜性和大小,從而縮短加載頁(yè)面時(shí)間。
兼容性
現(xiàn)代瀏覽器都有很好的兼容。
對(duì)于舊版瀏覽器中不可用的功能,可以使用 polyfill,例如 https://www.webcomponents.org