每天 React, Vue, 你知道如何原生實現(xiàn) WebComponent嗎?
大廠技術(shù) 高級前端 Node進階
點擊上方 程序員成長指北,關(guān)注公眾號
回復1,加入高級Node交流群
原文地址:https://juejin.cn/post/7034796986889043999
作者:hpstream_ (感謝小伙伴投稿)
談到WebComponent 很多人很容易想到Vue,React中的組件。但其實H5原生也已經(jīng)支持了組件的編寫。
查看 Web Components MDN 文檔,里面原話如下:
Web Components
Web Components 是一套不同的技術(shù),允許您創(chuàng)建可重用的定制元素(它們的功能封裝在您的代碼之外)并且在您的web應用中使用它們。Web Components旨在解決這些問題 — 它由三項主要技術(shù)組成,它們可以一起使用來創(chuàng)建封裝功能的定制元素,可以在你喜歡的任何地方重用,不必擔心代碼沖突。
Custom elements(自定義元素):一組JavaScript API,允許您定義custom elements及其行為,然后可以在您的用戶界面中按照需要使用它們。
Shadow DOM(影子DOM):一組JavaScript API,用于將封裝的“影子”DOM樹附加到元素(與主文檔DOM分開呈現(xiàn))并控制其關(guān)聯(lián)的功能。通過這種方式,您可以保持元素的功能私有,這樣它們就可以被腳本化和樣式化,而不用擔心與文檔的其他部分發(fā)生沖突。
-
Custom elements(自定義元素):一組JavaScript API,允許您定義custom elements及其行為,然后可以在您的用戶界面中按照需要使用它們。 -
Shadow DOM(影子DOM):一組JavaScript API,用于將封裝的“影子”DOM樹附加到元素(與主文檔DOM分開呈現(xiàn))并控制其關(guān)聯(lián)的功能。通過這種方式,您可以保持元素的功能私有,這樣它們就可以被腳本化和樣式化,而不用擔心與文檔的其他部分發(fā)生沖突。 -
HTML templates(HTML模板):template 和 slot 元素使您可以編寫不在呈現(xiàn)頁面中顯示的標記模板。然后它們可以作為自定義元素結(jié)構(gòu)的基礎(chǔ)被多次重用。
上面的概念難以理解,我們通過一個例子看下如何編寫一個組件;
案例一
-
什么是 HTML templates(HTML模板)?
<template id="btn">
<button class="hp-button">
<slot></slot>
</button>
</template>
-
Custom elements(自定義元素)
class HpButton extends HTMLElement {
constructor() {
super();
//...
}
}
// 定義了一個自定義標簽 組件
window.customElements.define('hp-button', HpButton)
-
Shadow DOM(影子DOM)
let shadow = this.attachShadow({
mode: 'open'
});
let btnTmpl = document.getElementById('btn');
let cloneTemplate = btnTmpl.content.cloneNode(true)
const style = document.createElement('style');
let type = this.getAttribute('type') || 'default';
const btnList = {
'primary': {
background: '#ff0000',
color: '#fff'
},
'default': {
background: '#909399',
color: '#fff'
}
}
style.textContent = `
.hp-button{
outline:none;
border:none;
border-radius:4px;
padding:5px 20px;
display:inline-flex;
background:${btnList[type].background};
color:${btnList[type].color};
cursor:pointer
}
`
// dom操作具備移動型
shadow.appendChild(style)
shadow.appendChild(cloneTemplate)
一個簡單完整的例子
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<style>
:root {
--background-color: black;
--text-color: yellow
}
</style>
<hp-button type="primary">
<input type="text">
按鈕
</hp-button>
<hp-button>珠峰按鈕</hp-button>
<!-- 內(nèi)容是不會被渲染到視圖上,不會影響頁面展示,可以使用模板 -->
<template id="btn">
<button class="hp-button">
<slot></slot>
</button>
</template>
<script>
class HpButton extends HTMLElement {
constructor() {
super();
let shadow = this.attachShadow({
mode: 'open'
});
let btnTmpl = document.getElementById('btn');
let cloneTemplate = btnTmpl.content.cloneNode(true)
const style = document.createElement('style');
let type = this.getAttribute('type') || 'default';
const btnList = {
'primary': {
background: '#ff0000',
color: '#fff'
},
'default': {
background: '#909399',
color: '#fff'
}
}
style.textContent = `
.hp-button{
outline:none;
border:none;
border-radius:4px;
padding:5px 20px;
display:inline-flex;
background:${btnList[type].background};
color:${btnList[type].color};
cursor:pointer
}
`
// dom操作具備移動型
shadow.appendChild(style)
shadow.appendChild(cloneTemplate)
}
}
// 定義了一個自定義標簽 組件
window.customElements.define('hp-button', HpButton)
</script>
</body>
</html>
結(jié)論:原生組件與Vue,React的組件的概念是相似的,但是從寫法上來看有區(qū)別。
深入學習
組件中還有重點的兩部分:生命周期和事件。
生命周期
在custom element的構(gòu)造函數(shù)中,可以指定多個不同的回調(diào)函數(shù),它們將會在元素的不同生命時期被調(diào)用:
-
connectedCallback:當 custom element首次被插入文檔DOM時,被調(diào)用。 -
disconnectedCallback:當 custom element從文檔DOM中刪除時,被調(diào)用。 -
adoptedCallback:當 custom element被移動到新的文檔時,被調(diào)用。 -
attributeChangedCallback: 當 custom element增加、刪除、修改自身屬性時,被調(diào)用。
我們來看一下它們的一下用法示例。下面的代碼出自life-cycle-callbacks示例(查看在線示例:https://mdn.github.io/web-components-examples/life-cycle-callbacks/)。這個簡單示例只是生成特定大小、顏色的方塊。custom element看起來像下面這樣
生命周期的代碼的具體示例:
class Square extends HTMLElement {
// Specify observed attributes so that
// attributeChangedCallback will work
static get observedAttributes() {
return ['c', 'l'];
}
constructor() {
// Always call super first in constructor
super();
const shadow = this.attachShadow({mode: 'open'});
const div = document.createElement('div');
const style = document.createElement('style');
shadow.appendChild(style);
shadow.appendChild(div);
}
connectedCallback() {
console.log('Custom square element added to page.');
updateStyle(this);
}
disconnectedCallback() {
console.log('Custom square element removed from page.');
}
adoptedCallback() {
console.log('Custom square element moved to new page.');
}
attributeChangedCallback(name, oldValue, newValue) {
console.log('Custom square element attributes changed.');
updateStyle(this);
}
}
customElements.define('custom-square', Square);
事件
可以采用 disatchEvent 和 CustomEvent 來實現(xiàn):
document.querySelector('???').dispatchEvent(new CustomEvent('changeName', {
detail: {
name: 1111,
}
}))
折疊面板的案例
-
完成模版部分的定義:
<!-- 沒有實際意義, 不會渲染到頁面上 -->
<template id="collapse_tmpl">
<div class="zf-collapse">
<slot></slot>
</div>
</template>
<template id="collapse_item_tmpl">
<div class="zf-collapse-item">
<div class="title"></div>
<div class="content">
<slot></slot>
</div>
</div>
</template>
-
創(chuàng)建組件
class Collapse extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({
mode: 'open'
});
const tmpl = document.getElementById('collapse_tmpl');
let cloneTemplate = tmpl.content.cloneNode(true);
let style = document.createElement('style');
// :host 代表的是影子的根元素
style.textContent = `
:host{
display:flex;
border:3px solid #ebebeb;
border-radius:5px;
width:100%;
}
.zf-collapse{
width:100%;
}
`
shadow.appendChild(style);
shadow.appendChild(cloneTemplate);
let slot = shadow.querySelector('slot'); // 監(jiān)控slot變化
slot.addEventListener('slotchange', (e) => {
this.slotList = e.target.assignedElements();
this.render();
})
}
static get observedAttributes() { // 監(jiān)控屬性的變化
return ['active']
}
// update
attributeChangedCallback(key, oldVal, newVal) {
if (key == 'active') {
this.activeList = JSON.parse(newVal);
this.render();
}
}
render() {
if (this.slotList && this.activeList) {
[...this.slotList].forEach(child => {
child.setAttribute('active', JSON.stringify(this.activeList))
});
}
}
}
export default Collapse
class CollapseItem extends HTMLElement {
constructor() {
super();
let shadow = this.attachShadow({
mode: 'open'
});
let tmpl = document.getElementById('collapse_item_tmpl');
let cloneTemplate = tmpl.content.cloneNode(true);
let style = document.createElement('style');
this.isShow = true; // 標識自己是否需要顯示
style.textContent = `
:host{
width:100%;
}
.title{
background:#f1f1f1;
line-height:35px;
height:35px;
}
.content{
font-size:14px;
}
`
shadow.appendChild(style)
shadow.appendChild(cloneTemplate);
this.titleEle = shadow.querySelector('.title');
this.titleEle.addEventListener('click', () => {
// 如果將結(jié)果傳遞給父親 組件通信?
document.querySelector('zf-collapse').dispatchEvent(new CustomEvent('changeName', {
detail: {
name: this.getAttribute('name'),
isShow: this.isShow
}
}))
})
}
static get observedAttributes() { // 監(jiān)控屬性的變化
return ['active', 'title', 'name']
}
// update
attributeChangedCallback(key, oldVal, newVal) {
switch (key) {
case 'active':
this.activeList = JSON.parse(newVal); // 子組件接受父組件的數(shù)據(jù)
break;
case 'title':
this.titleEle.innerHTML = newVal; // 接受到title屬性 作為dom的title
break;
case 'name':
this.name = newVal
break;
}
let name = this.name;
if (this.activeList && name) {
this.isShow = this.activeList.includes(name);
this.shadowRoot.querySelector('.content').style.display = this.isShow ? 'block' : 'none'
}
}
}
export default CollapseItem
-
頁面使用:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<zf-collapse>
<zf-collapse-item title="Node" name="1">
<div>nodejs welcome</div>
</zf-collapse-item>
<zf-collapse-item title="react" name="2">
<div>react welcome</div>
</zf-collapse-item>
<zf-collapse-item title="vue" name="3">
<div>vue welcome</div>
</zf-collapse-item>
</zf-collapse>
<!-- 沒有實際意義, 不會渲染到頁面上 -->
<template id="collapse_tmpl">
<div class="zf-collapse">
<slot></slot>
</div>
</template>
<template id="collapse_item_tmpl">
<div class="zf-collapse-item">
<div class="title"></div>
<div class="content">
<slot></slot>
</div>
</div>
</template>
<!-- vite 實現(xiàn)原理 就依賴于 type="module" -->
<script src="./index1.js" type="module"></script>
</body>
</html>
參考資料:
-
web Components MDN -
案例學習:https://github.com/mdn/web-components-examples
我組建了一個氛圍特別好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你對Node.js學習感興趣的話(后續(xù)有計劃也可以),我們可以一起進行Node.js相關(guān)的交流、學習、共建。下方加 考拉 好友回復「Node」即可。
“分享、點贊、在看” 支持一波??
