前言
在前端框架百花齐放的今天,因为各项目使用不同框架导致需要重复进行UI组件架开发成为一个老大难,举个栗子:
打蛋同学需要开发一个人名片,但是他pc端使用的是Vue、移动端使用的是React,那么打蛋就需要开发一个vue卡片组件、React卡片组件,聪明的打蛋会想能不能开发一个跨框架的web组件呢(其实是希望React和Vue能不能来个世纪和解,在React里面支持使用Vue组件😁),那这个时候就请出WebComponent了。
什么是WebComponent❓
MDN上对其定义:Web Component 是一套不同的技术,允许你创建可重用的定制元素(它们的功能封装在你的代码之外)并且在你的 web 应用中使用它们
简单点来说就是WebComponent 可以跨框架使用,并且开发者可以对其进行自定义扩展封装来达到UI组件复用目的,这是一个web标准,是原生浏览器支持的,与框架无关
如何实现WebComponent❓
这里有三个关键技术
- Custom element(自定义元素):一组 JavaScript API,允许你定义 custom elements 及其行为,然后可以在你的用户界面中按照需要使用它们,简单点来说就是允许开发者自定义注册一个web组件。
- Shadow DOM(影子 DOM ):一组 JavaScript API,用于将封装的"影子"DOM 树附加到元素(与主文档 DOM 分开呈现)并控制其关联的功能。通过这种方式,你可以保持元素的功能私有,这样它们就可以被脚本化和样式化,而不用担心与文档的其他部分发生冲突,总结就是使用了Shadow Dom 可以把组件的样式和行为封装在一个独立的容器内,不会影响外部的样式和行为,相当于沙箱隔离。
- HTML template(HTML 模板) :
<template>
和<slot>
元素使你可以编写不在呈现页面中显示的标记模板。然后它们可以作为自定义元素结构的基础被多次重用,简单来说就是这两个标签在html中使用,里面的内容不会展示出来,并且他们可以被扩展使用
简单创建一个WebComponent
vCustomButton.js
//定义一个类继承自HTMLElement
class VCustomButton extends HTMLElement {
constructor() {
super();
//先获取目标节点
const template = document.getElementById('v-custom-button'),
// shadow Dom 这里主要是将样式和行为隔离 使得webComponent的样式以及行为都在一个隔离的容器 不会影响外部样式和行为
// 这一点很像vue 的 scoped 但vue采用的是往节点添加data-xxx属性来区别
// mode决定外部是可以访问该根节点 open是可以 closed是拒绝访问
rootDom = this.attachShadow({ mode: 'open'})
//复制模板
const content = template.content.cloneNode(true)
//将模板内容添加到ShadowDOM中
rootDom.appendChild(content);
}
}
//这里是进行注册
window.customElements.define('v-custom-button', VCustomButton)
使用组件,注意这里不可以直接打开改本地html,file://
访问js会报错,因为file是伪协议,在浏览器下会报跨域错误,这里我使用的的live-server(vscode插件、npm包下载都可)起了本地服务
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>使用webComponent</title>
<script type="module" src="./v-custom-button.js"></script>
</head>
<body>
<!-- 解析器不会将这个模板解析出来 -->
<template id="v-custom-button">
<style>
.v-button {
display: inline-block;
-webkit-appearance: none;
box-sizing: border-box;
box-sizing: content-box;
outline-style: none;
padding: 8px;
background-color: #fff;
border-radius: 4px;
border: 1px solid #467689;
cursor: pointer;
}
</style>
<button class="v-button">这是webComponent按钮</button>
</template>
<v-custom-button id="v-custom-button"/>
</body>
</html>
查看效果,完美🤩
但是我还想自定义传入props、事件行为,又该怎么做呢?
传入props比较简单,只需要新增对应属性名即可
index.html
<v-custom-button id="v-custom-button" buttonText="我在自定义按钮名称"/>
在class构造器里面进行赋值
js
...
const content = template.content.cloneNode(true),
title = content.querySelector('.v-button'); //获取节点
title.innerText = this.getAttribute('buttonText') //赋值
...
来看看效果
也可以使用WebComponent的生命周期回调attributeChangedCallback,这里还要配合observedAttributes一起使用
- observedAttributes,返回需要被监听的属性,返回值是一个数组
- attributeChangedCallback,自定义元素的一个属性被增加、移除或更改时被调用,注意这里的属性必须贝上文observedAttributes的返回值包含
js
...
constructor() { ... }
static get observedAttributes() {
return ['button-text']
}
attributeChangedCallback() {
console.log('触发属性更新了');
const text = this.getAttribute('button-text')
this.$button.innerText = text
}
这里我们看到控制台attributeChangedCallback被执行了
实现下数据驱动视图
这里我们要实现下点击按钮触发按钮文本更新,很简单,这里我们对shadowDom进行点击回调监听,修改buttonText这个属性即可
js
...
constructor(){
...
this.shadowRoot.querySelector('.v-button').addEventListener('click', (e) => {
this.setAttribute('button-text', '我被点击修改了')
})
}
这里我们也可以看到,attributeChangedCallback再次被触发
支持插槽
这里的用法也比较简单,只需要在模板加入 <slot>
index.html
<template id="v-custom-button">
<style>
.v-button {
display: inline-block;
-webkit-appearance: none;
box-sizing: border-box;
box-sizing: content-box;
outline-style: none;
padding: 8px;
background-color: #fff;
border-radius: 4px;
border: 1px solid #467689;
cursor: pointer;
}
</style>
<slot></slot>
<button class="v-button">这是webComponent按钮</button>
</template>
在加载卸载钩子里面执行监听
当组件被加载时候,我们执行onclick监听,当组件被卸载,我们也需要移除监听,webComponent也提供了两个生命周期钩子:
- connectedCallback:当自定义元素第一次被连接到文档 DOM 时被调用,就是被挂载时候使用。
- disconnectedCallback:当自定义元素与文档 DOM 断开连接时被调用,就是组件被卸载时候调用。
js
...
connectedCallback() {
console.log('组件被挂载');
this.shadowRoot.querySelector('.v-button').addEventListener('click', (e) => {
this.setAttribute('button-text', '我被点击修改了')
})
}
disconnectedCallback() {
console.log('组件被卸载载');
this.shadowRoot.querySelector('.v-button').removeEventListener('click')
}
...
让我们来看看,生命周期函数被触发了
结语
从2011年WebComponent概念被初次提及到2012年被实现(也就是template、shadowDom),算起来也有一个轮回了,目前主流的WebComponent框架有谷歌的polymer、腾讯omi,京东的微前端框架mirco-app也是类WebComponent实现的,相信随着微前端框架的流行WebComponent会成为一个趋势,接下来我补全一下代码:
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>使用webComponent</title>
<script type="module" src="./v-custom-button.js"></script>
</head>
<body>
<!-- 解析器不会将这个模板解析出来 -->
<template id="v-custom-button">
<style>
/* 样式支持定义 类似于html: root 可以进行变量声明*/
:host {
--button-bg-color: #fff;
--button-border: 1px solid #467689;
}
.v-button {
display: inline-block;
-webkit-appearance: none;
box-sizing: border-box;
box-sizing: content-box;
outline-style: none;
padding: 8px;
background-color: #fff;
border-radius: 4px;
border: var(--button-border);
cursor: pointer;
}
</style>
<slot></slot>
<button class="v-button">这是webComponent按钮</button>
</template>
<v-custom-button id="v-custom-button" button-text="我在自定义按钮名称">
<span>哈哈哈</span>
</v-custom-button>
</body>
</html>
VCustomButton.js
//定义一个类继承自HTMLElement
class VCustomButton extends HTMLElement {
constructor() {
super();
//先获取目标节点
const template = document.getElementById('v-custom-button'),
// shadow Dom 这里主要是将样式和行为隔离 使得webComponent的样式以及行为都在一个隔离的容器 不会影响尾外部样式
// 这一点很像vue 的 scoped 但vue采用的是往节点添加data-xxx属性来区别
// mode决定外部是可以访问根节点 open是可以 closed是拒绝访问n
shadowRootDom = this.attachShadow({ mode: 'open'});
//复制模板
const content = template.content.cloneNode(true),
button = content.querySelector('.v-button');
this.$button = button
// const text = this.getAttribute('button-text')
// button.innerText = text
shadowRootDom.appendChild(content);
}
static get observedAttributes() {
return ['button-text']
}
attributeChangedCallback() {
console.log('触发属性更新了');
const text = this.getAttribute('button-text')
this.$button.innerText = text
}
connectedCallback() {
console.log('组件被挂载');
this.shadowRoot.querySelector('.v-button').addEventListener('click', (e) => {
this.setAttribute('button-text', '我被点击修改了')
})
}
disconnectedCallback() {
console.log('组件被卸载载');
this.shadowRoot.querySelector('.v-button').removeEventListener('click')
}
}
window.customElements.define('v-custom-button', VCustomButton)