Web Components 上手指南
現(xiàn)在的前端開發(fā)基本離不開 React、Vue 這兩個(gè)框架的支撐,而這兩個(gè)框架下面又衍生出了許多的自定義組件庫:
Element(Vue) Ant Design(React)
什么是 Web Components?
如何使用 Web Components?
Custom elements(自定義元素):一組 JavaScript API,用來創(chuàng)建自定義的 HTML標(biāo)簽,并允許標(biāo)簽創(chuàng)建或銷毀時(shí)進(jìn)行一些操作; Shadow DOM(影子DOM):一組 JavaScript API,用于將創(chuàng)建的 DOM Tree 插入到現(xiàn)有的元素中,且 DOM Tree 不能被外部修改,不用擔(dān)心元素被其他地方影響; HTML templates(HTML模板):通過 <template>、<slot> 直接在 HTML 文件中編寫模板,然后通過 DOM API 獲取。
Custom elements(自定義元素)
自定義元素的名稱,一個(gè) DOMString 標(biāo)準(zhǔn)的字符串,為了防止自定義元素的沖突,必須是一個(gè)帶短橫線連接的名稱(e.g. custom-tag)。 定義自定義元素的一些行為,類似于 React、Vue 中的生命周期。 擴(kuò)展參數(shù)(可選),該參數(shù)類型為一個(gè)對象,且需要包含 extends 屬性,用于指定創(chuàng)建的元素繼承自哪一個(gè)內(nèi)置元素(e.g. { extends: 'p' })。
創(chuàng)建一個(gè)新的 HTML 標(biāo)簽
class HelloUser extends HTMLElement {
constructor() {
// 必須調(diào)用 super 方法
super();
// 創(chuàng)建一個(gè) div 標(biāo)簽
const $box = document.createElement("p");
let userName = "User Name";
if (this.hasAttribute("name")) {
// 如果存在 name 屬性,讀取 name 屬性的值
userName = this.getAttribute("name");
}
// 設(shè)置 div 標(biāo)簽的文本內(nèi)容
$box.innerText = `Hello ${userName}`;
// 創(chuàng)建一個(gè) shadow 節(jié)點(diǎn),創(chuàng)建的其他元素應(yīng)附著在該節(jié)點(diǎn)上
const shadow = this.attachShadow({ mode: "open" });
shadow.appendChild($box);
}
}
// 定義一個(gè)名為 <hello-user /> 的元素
customElements.define("hello-user", HelloUser);
<hello-user name="Shenfq"></hello-user>
擴(kuò)展已有的 HTML 標(biāo)簽
class SkillList extends HTMLUListElement {
constructor() {
// 必須調(diào)用 super 方法
super();
if (
this.hasAttribute("skills") &&
this.getAttribute("skills").includes(',')
) {
// 讀取 skills 屬性的值
const skills = this.getAttribute("skills").split(',');
skills.forEach(skill => {
const item = document.createElement("li");
item.innerText = skill;
this.appendChild(item);
})
}
}
}
// 對 <ul> 標(biāo)簽進(jìn)行擴(kuò)展
customElements.define("skill-list", SkillList, { extends: "ul" });<ul is="skill-list" skills="js,css,html"></ul>
生命周期
connectedCallback:當(dāng)自定義元素被插入到頁面的 DOM 文檔時(shí)調(diào)用。 disconnectedCallback:當(dāng)自定義元素從 DOM 文檔中被刪除時(shí)調(diào)用。 adoptedCallback:當(dāng)自定義元素被移動時(shí)調(diào)用。 attributeChangedCallback: 當(dāng)自定義元素增加、刪除、修改自身屬性時(shí)調(diào)用。
class HelloUser extends HTMLElement {
constructor() {
// 必須調(diào)用 super 方法
super();
// 創(chuàng)建一個(gè) div 標(biāo)簽
const $box = document.createElement("p");
let userName = "User Name";
if (this.hasAttribute("name")) {
// 如果存在 name 屬性,讀取 name 屬性的值
userName = this.getAttribute("name");
}
// 設(shè)置 div 標(biāo)簽的文本內(nèi)容
$box.innerText = `Hello ${userName}`;
// 創(chuàng)建一個(gè) shadow 節(jié)點(diǎn),創(chuàng)建的其他元素應(yīng)附著在該節(jié)點(diǎn)上
const shadow = this.attachShadow({ mode: "open" });
shadow.appendChild($box);
}
connectedCallback() {
console.log('創(chuàng)建元素')
// 5s 后移動元素到 iframe
setTimeout(() => {
const iframe = document.getElementsByTagName("iframe")[0]
iframe.contentWindow.document.adoptNode(this)
}, 5e3)
}
disconnectedCallback() {
console.log('刪除元素')
}
adoptedCallback() {
console.log('移動元素')
}
}
<!-- 頁面插入一個(gè) iframe,將自定義元素移入其中 -->
<iframe width="0" height="0"></iframe>
<hello-user name="Shenfq"></hello-user>

Shadow DOM(影子DOM)



創(chuàng)建 Shadow DOM
<div id="root"></div>
<script>
// 獲取頁面的
const $root = document.getElementById('root');
const $p = document.createElement('p');
$p.innerText = '創(chuàng)建一個(gè) shadow 節(jié)點(diǎn)';
const shadow = $root.attachShadow({mode: 'open'});
shadow.appendChild($p);
</script>

mode 的差異
<div id="root"></div>
<script>
// 獲取頁面的
const $root = document.getElementById('root');
const $p = document.createElement('p');
$p.innerText = '創(chuàng)建一個(gè) shadow 節(jié)點(diǎn)';
const shadow = $root.attachShadow({mode: 'open'});
shadow.appendChild($p);
console.log('is open', $div.shadowRoot);
</script>

<div id="root"></div>
<script>
// 獲取頁面的
const $root = document.getElementById('root');
const $p = document.createElement('p');
$p.innerText = '創(chuàng)建一個(gè) shadow 節(jié)點(diǎn)';
const shadow = $root.attachShadow({mode: 'closed'});
shadow.appendChild($p);
console.log('is closed', $div.shadowRoot);
</script>

HTML templates(HTML模板)
使用模板
<template id="helloUserTpl">
<p class="name">Name</p>
<a target="blank" class="blog">##</a>
</template>
// 通過 ID 獲取標(biāo)簽
const tplElem = document.getElementById('helloUserTpl');
const content = tplElem.content.cloneNode(true);
<hello-user name="Shenfq" blog="http://blog.shenfq.com" />
<script>
class HelloUser extends HTMLElement {
constructor() {
// 必須調(diào)用 super 方法
super();
// 通過 ID 獲取標(biāo)簽
const tplElem = document.getElementById('helloUserTpl');
const content = tplElem.content.cloneNode(true);
if (this.hasAttribute('name')) {
const $name = content.querySelector('.name');
$name.innerText = this.getAttribute('name');
}
if (this.hasAttribute('blog')) {
const $blog = content.querySelector('.blog');
$blog.innerText = this.getAttribute('blog');
$blog.setAttribute('href', this.getAttribute('blog'));
}
// 創(chuàng)建一個(gè) shadow 節(jié)點(diǎn),創(chuàng)建的其他元素應(yīng)附著在該節(jié)點(diǎn)上
const shadow = this.attachShadow({ mode: "closed" });
shadow.appendChild(content);
}
}
// 定義一個(gè)名為 <hello-user /> 的元素
customElements.define("hello-user", HelloUser);
</script>
添加樣式
<template id="helloUserTpl">
<style>
:host {
display: flex;
flex-direction: column;
width: 200px;
padding: 20px;
background-color: #D4D4D4;
border-radius: 3px;
}
.name {
font-size: 20px;
font-weight: 600;
line-height: 1;
margin: 0;
margin-bottom: 5px;
}
.email {
font-size: 12px;
line-height: 1;
margin: 0;
margin-bottom: 15px;
}
</style>
<p class="name">User Name</p>
<a target="blank" class="blog">##</a>
</template>

占位元素
<template id="helloUserTpl">
<p class="name">User Name</p>
<a target="blank" class="blog">##</a>
<!--占位符-->
<slot name="desc"></slot>
</template>
<hello-user name="Shenfq" blog="http://blog.shenfq.com">
<p slot="desc">歡迎關(guān)注公眾號:更了不起的前端</p>
</hello-user>

總結(jié)
瀏覽器原生支持,不需要引入額外的第三方庫; 真正的內(nèi)部私有化的 CSS,不會產(chǎn)生樣式的沖突; 無需經(jīng)過編譯操作,即可實(shí)現(xiàn)的組件化方案,且與外部 DOM 隔離;

評論
圖片
表情
