前言
我们日常开发使用的框架都支持组件化,组件化使得组件得以复用,大幅提高开发效率。浏览器的原生组件 Web Components旨在解决组件复用问题,相比框架更加直接,不用加载任何外部模块,目前大部分浏览器已原生支持Web Components,对于IE、safiri、以及一些老版本浏览器兼容性不太好。
语法
自定义元素
自定义元素必须以短横线拼接。
<zy-button type='primary'>custom按钮</zy-button>
customElements.define()
自定义元素需要使用 JavaScript 定义一个类,所有<zy-button>
都会是这个类的实例。
class ZyButton extends HTMLElement {
constructor() {
super();
}
}
window.customElements.define('zy-button', ZyButton);
template标签
Web Components API 提供了<template>
标签,可以在它里面使用 HTML 定义 DOM。
template标签不会被渲染到页面上,仅为了提供shadowDOM使用,它们可以作为自定义元素结构的基础被多次重用。
<template id="btn">
<button class="zy-button">
<slot>默认按钮</slot>
</button>
</template>
shadowDOM
用于将封装的“影子”DOM树附加到元素(与主文档DOM分开呈现)并控制其关联的功能。
例如<video>
标签,内部有倍速、进度条、音量等元素,它们都被封装到shadowDOM里面,外部无法改变这些元素。
相关API:
- 可以使用
Element.attachShadow()
方法来将一个 shadow root 附加到任何一个元素上。 - 使用 shadow.appendChild(ele) 将 Shadow DOM 附加到一个元素之后。
自定义元素的内容
自定义元素需要继承一个类,在该类中可以监听元素的属性、定义元素的生命周期钩子、定义样式等。
自定义元素的内容:
class ZyButton extends HTMLElement {
constructor() {
// 继承HTMLElement
super()
// 创建shadowDOM
let shadow = this.attachShadow({mode: 'open'});
// 获取template
let btnTmpl = document.getElementById('btn')
// 克隆了它的所有子元素,这是因为可能有多个自定义元素的实例,这个模板还要留给其他实例使用,所以不能直接移动它的子元素。
let cloneTemplate = btnTmpl.content.cloneNode(true)
// this是zy-button元素
let type = this.getAttribute('type') || 'default'
const btnList = {
'primary': {
background: '#409eff',
color: '#fff'
},
'default': {
background: '#909399',
color: '#fff'
}
}
// 创建style元素
const style = document.createElement('style')
// 设置shadowDOM的样式
style.textContent = `
.zy-button{
outline: none;
border:none;
border-radius:4px;
padding:5px 20px;
display:inline-flex;
background: var(--zy-background-color,${btnList[type].background});
color: var(--zy-color,${btnList[type].color});
cursor:pointer;
}
`
// 在shadowDOM中添加style元素
shadow.appendChild(style)
// 在shadowDOM中添加cloneTemplate
shadow.appendChild(cloneTemplate)
}
/**
* 生命周期钩子
*/
connectedCallback() {
console.log('插入到dom时执行的回调')
}
disconnectedCallback() {
console.log('移除dom时执行的回调')
}
adoptedCallback() {
console.log('将组件移动到iframe会执行')
}
attributeChangedCallback(key, oldVal, newVal) {
console.log("属性变化时执行");
}
/**
* 监听属性(监听属性'active'的变化)
*/
static get observedAttributes() {
return ["active"];
}
}
创建collapse自定义组件
目录组织结构
public
├─ webcomponent // class定义相关
│ ├─ collapse-item.js
│ ├─ collapse.js
│ └─ index.js
└─ webcomponent-collapse.html // 自定义组件、template等
webcomponent-collapse.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>collapse</title>
</head>
<body>
<!-- 直接打开html文件,会报错不支持file引入 -->
<!-- 安装live server插件,用插件打开html文件 -->
<!-- live server是一个实时小型服务器 -->
<zy-collapse>
<zy-collapse-item title='Node' name='1'>
<div>nodejs welcome</div>
</zy-collapse-item>
<zy-collapse-item title='react' name='2'>
<div>react welcome</div>
</zy-collapse-item>
<zy-collapse-item title='vue' name='3'>
<div>vue welcome</div>
</zy-collapse-item>
</zy-collapse>
<!-- template 没有实际意义,不会渲染 -->
<template id="collapse_tmpl">
<div class="zy-collapse">
<slot></slot>
</div>
</template>
<template id="collapse_item_tmpl">
<div class="zy-collapse-item">
<div class="title"></div>
<div class="content">
<slot></slot>
</div>
</div>
</template>
<!-- vite实现原理就依赖于 type='module' -->
<script src='./webcomponent/index.js' type="module"></script>
</body>
</html>
自定义的collapse组件类似于element-ui中的collapse组件,外部为<zy-collapse>
,有activeList
属性,为默认展开的items;内部为多个<zy-collapse-item>
,支持展开和折叠,有name属性(标识符)、title属性(标题)。
tip: 直接打开html文件会报错,不支持file引入文件;解决方式:安装live server插件,使用该插件打开html文件
index.js
import Collapse from "./collapse.js";
import CollapseItem from "./collapse-item.js";
window.customElements.define("zy-collapse", Collapse);
window.customElements.define("zy-collapse-item", CollapseItem);
// 设置组件默认显示状态
let defaultActive = ['1', '2'] // name:1、name:2默认展开,name:3 折叠
document.querySelector('zy-collapse').setAttribute('active',JSON.stringify(defaultActive))
// 然后,每个item需要获取到defaultActive 和自己的name属性作比较,如果在里面就显示,不在就隐藏
// 组件通信(监听到自定义事件changeName,获取到子组件传递过来的参数,然后触发defaultActive的更改,子组件监听到defaultActive的修改,进行展开/折叠)
document.querySelector('zy-collapse').addEventListener('changeName', (e) => {
let { isShow, name } = e.detail
console.log(isShow, name)
// isShow为true,就隐藏;反之则展示
if(isShow) {
let index = defaultActive.indexOf(name)
defaultActive.splice(index,1)
} else {
defaultActive.push(name)
}
document.querySelector('zy-collapse').setAttribute('active',JSON.stringify(defaultActive))
})
collapse.js
class Collapse extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: "open" });
const tmpl = document.getElementById("collapse_tmpl");
let cloneTemplate = tmpl.content.cloneNode(true); // true表示克隆所有子节点
const style = document.createElement("style");
style.textContent = `
:host {
display: flex;
border: 2px solid #ebebeb;
border-radius: 5px;
width: 100%;
}
.zy-collapse {
width: 100%;
}
`;
shadow.appendChild(style);
shadow.appendChild(cloneTemplate);
// 监听slot变化(初次渲染,slot赋值时就会监听到)
let slot = shadow.querySelector("slot");
slot.addEventListener("slotchange", (e) => {
// slotList为所有子元素
this.slotList = e.target.assignedElements();
this.render();
});
}
// 监听属性'active'的变化(语法就是这样的)
static get observedAttributes() {
return ["active"];
}
// 属性变化时执行
attributeChangedCallback(key, oldVal, newVal) {
if (key === "active") {
this.activeList = JSON.parse(newVal);
this.render();
}
}
render() {
if (this.slotList && this.activeList) {
// slotList是类数组,用扩展运算符转化成数组
[...this.slotList].forEach(child => {
child.setAttribute('active', JSON.stringify(this.activeList))
})
}
}
}
export default Collapse;
collapse-item.js
class CollapseItem extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: "open" });
const tmpl = document.getElementById("collapse_item_tmpl");
let cloneTemplate = tmpl.content.cloneNode(true); // true表示克隆所有子节点
const 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', () => {
// 将结果传递给父亲;发布订阅模式!!
// 自定义事件 new CustomEvent
document.querySelector('zy-collapse').dispatchEvent(new CustomEvent('changeName', {
detail: {
name: this.getAttribute('name'),
isShow: this.isShow
}
}))
})
}
// 监听属性active、title、name的变化
static get observedAttributes() {
return ["active", "title", "name"];
}
// 属性变化时执行
attributeChangedCallback(key, oldVal, newVal) {
switch (key) {
case "active":
this.activeList = JSON.parse(newVal)
break;
case "title":
this.titleEle.innerHTML = newVal; // 接收到title属性,将它作为 .title 元素的内容
break;
case "name":
this.name = newVal
break;
default:
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;
组件通信方式
在父组件绑定一个自定义事件:
document.querySelector('zy-collapse').addEventListener('changeName', (e) => {
// 获取到子组件传递过来的数据
let { isShow, name } = e.detail
console.log(isShow, name)
})
在子组件中派发自定义事件,并传递参数:
this.titleEle.addEventListener('click', () => {
// 自定义事件 new CustomEvent
document.querySelector('zy-collapse').dispatchEvent(new CustomEvent('changeName', {
detail: {
name: this.getAttribute('name'),
isShow: this.isShow
}
}))
})