每天 React, Vue, 你知道如何原生实现 WebComponent吗?
大厂技术 高级前端 Node进阶
点击上方 程序员成长指北,关注公众号
回复1,加入高级Node交流群
原文地址:https://juejin.cn/post/7034796986889043999
作者:hpstream_ (感谢小伙伴投稿)
谈到WebComponent 很多人很容易想到Vue,React中的组件。但其实H5原生也已经支持了组件的编写。
查看 Web Components MDN 文档,里面原话如下:
Web Components
Web Components 是一套不同的技术,允许您创建可重用的定制元素(它们的功能封装在您的代码之外)并且在您的web应用中使用它们。Web Components旨在解决这些问题 — 它由三项主要技术组成,它们可以一起使用来创建封装功能的定制元素,可以在你喜欢的任何地方重用,不必担心代码冲突。
Custom elements(自定义元素):一组JavaScript API,允许您定义custom elements及其行为,然后可以在您的用户界面中按照需要使用它们。
Shadow DOM(影子DOM):一组JavaScript API,用于将封装的“影子”DOM树附加到元素(与主文档DOM分开呈现)并控制其关联的功能。通过这种方式,您可以保持元素的功能私有,这样它们就可以被脚本化和样式化,而不用担心与文档的其他部分发生冲突。
-
Custom elements
(自定义元素):一组JavaScript API,允许您定义custom elements及其行为,然后可以在您的用户界面中按照需要使用它们。 -
Shadow DOM
(影子DOM):一组JavaScript API,用于将封装的“影子”DOM树附加到元素(与主文档DOM分开呈现)并控制其关联的功能。通过这种方式,您可以保持元素的功能私有,这样它们就可以被脚本化和样式化,而不用担心与文档的其他部分发生冲突。 -
HTML templates
(HTML模板):template 和 slot 元素使您可以编写不在呈现页面中显示的标记模板。然后它们可以作为自定义元素结构的基础被多次重用。
上面的概念难以理解,我们通过一个例子看下如何编写一个组件;
案例一
-
什么是 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>
<!-- 内容是不会被渲染到视图上,不会影响页面展示,可以使用模板 -->
<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>
结论:原生组件与Vue,React的组件的概念是相似的,但是从写法上来看有区别。
深入学习
组件中还有重点的两部分:生命周期和事件。
生命周期
在custom element
的构造函数中,可以指定多个不同的回调函数,它们将会在元素的不同生命时期被调用:
-
connectedCallback:当 custom element首次被插入文档DOM时,被调用。 -
disconnectedCallback:当 custom element从文档DOM中删除时,被调用。 -
adoptedCallback:当 custom element被移动到新的文档时,被调用。 -
attributeChangedCallback: 当 custom element增加、删除、修改自身属性时,被调用。
我们来看一下它们的一下用法示例。下面的代码出自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
来实现:
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>
-
创建组件
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'); // 监控slot变化
slot.addEventListener('slotchange', (e) => {
this.slotList = e.target.assignedElements();
this.render();
})
}
static get observedAttributes() { // 监控属性的变化
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', () => {
// 如果将结果传递给父亲 组件通信?
document.querySelector('zf-collapse').dispatchEvent(new CustomEvent('changeName', {
detail: {
name: this.getAttribute('name'),
isShow: this.isShow
}
}))
})
}
static get observedAttributes() { // 监控属性的变化
return ['active', 'title', 'name']
}
// update
attributeChangedCallback(key, oldVal, newVal) {
switch (key) {
case 'active':
this.activeList = JSON.parse(newVal); // 子组件接受父组件的数据
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 实现原理 就依赖于 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学习感兴趣的话(后续有计划也可以),我们可以一起进行Node.js相关的交流、学习、共建。下方加 考拉 好友回复「Node」即可。
“分享、点赞、在看” 支持一波👍