Web Component 最早的概念可以追溯到 2011 年,到了 2018 年 V1 版本开始被主流浏览器所支持(除了 IE)。到了现在,几乎所有的浏览器都支持了 Web Component。
什么是 Web Component
MDN 上面的解释是 Web Component 是一套不同的技术,允许你创建可重用的定制元素(它们的功能封装在你的代码之外)并且在你的 web 应用中使用它们。
其实挺语义化的,就是组件的意思。相对于 react 和 vue 可以很容易的创建一些组件,原生的最早之前对组件的概念非常弱。Web Component 的一个愿景就是可以使用原生的创建组件,跨越不同团队不同项目不同框架共用组件。
现阶段,有蛮多在使用原生的 Web Component,比如 YouToBe、网页 Photoshop、Microsoft 等。一直在开发业务,没机会使用 Web Component 在实践中开发,只能是了解一下这个概念。
Web Component 三个最核心的概念:
- Custom Elements:自定义元素,一组 JavaScript API,允许你定义 custom elements 及其行为,然后可以在你的用户界面中按照需要使用它们。
- Shadow DOM:影子 DOM,一组 JavaScript API,用于将封装的"影子"DOM 树附加到元素(与主文档 DOM 分开呈现)并控制其关联的功能。通过这种方式,你可以保持元素的功能私有,这样它们就可以被脚本化和样式化,而不用担心与文档的其他部分发生冲突。
- HTML Templates:HTML 模板,template 和 slot 元素使你可以编写不在呈现页面中显示的标记模板。然后它们可以作为自定义元素结构的基础被多次重用。
创建组件,最重要的一些因素:样式隔离、组件传值、回调、生命周期,Web Component 都支持,下面实现一个简单的 button 组件。
load.js
javascript
export default (url) => {
return new Promise((resolve, reject) => {
var xhr = new XMLHttpRequest();
xhr.open("GET", url, true);
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && xhr.status === 200) {
resolve(xhr.responseText);
}
};
xhr.send();
});
};
button.js
javascript
import loadTemplate from'./load.js'
const templateContent = `
<style>
button{
color: rgba(255, 0, 0, 0.44);
background: #fff;
border: solid 1px rgba(255, 0, 0, 0.44);
border-radius: 5px;
line-height: 30px;
}
</style>
<button><slot></slot></button>
`
class Button extends HTMLElement {
constructor() {
super();
loadTemplate('./button.html').then(res => {
this.attachShadow({ mode: 'open' });
const template = document.createElement('template');
//res换成templateContent也可以
template.innerHTML = res;
const content = template.content.cloneNode(true);
this.shadowRoot.appendChild(content);
this.$button = this.shadowRoot.querySelector('button');
this.$button.addEventListener('click', () => {
this.dispatchEvent(new CustomEvent('onCustomClick', {
detail: '这是子组件的数据'
}))
});
})
}
connectedCallback(){
console.log('生命周期connectedCallback');
}
disconnectedCallback(){
console.log('生命周期disconnectedCallback');
}
adoptedCallback(){
console.log('生命周期adoptedCallback');
}
attributeChangedCallback(name, oldVal, newVal){
console.log('生命周期attributeChangedCallback', name, oldVal, newVal);
}
static get observedAttributes() {
return ['text', 'class'];
}
}
customElements.define('w-button', Button);
button.html
xml
<style>
button{
color: rgba(255, 0, 0, 0.44);
background: #fff;
border: solid 1px rgba(255, 0, 0, 0.44);
border-radius: 5px;
line-height: 30px;
}
</style>
<button>
<slot></slot>
<!-- <slot name="real"></slot> -->
</button>
index.html 使用
xml
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="./button.js" type="module"></script>
</head>
<body>
<w-button id="wButton" onclick="parentClick()" text="测试" class="is-class">
<span>这是组件button按钮</span>
<!-- <span slot="real">具名slot</span> -->
</w-button>
<button>这不是组件button</button>
<button onclick="removeButton()">隐藏</button>
<button onclick="attributeChange('text')">改变text属性</button>
<button onclick="attributeChange('class')">改变class属性</button>
</body>
<script>
function test(){
console.log('这是组件button按钮');
}
const wButton = document.getElementById('wButton');
const removeButton = () => {
wButton.remove();
}
const attributeChange = (key) => {
if(key == 'class'){
wButton.setAttribute('class', 'is-测试');
}else{
wButton.setAttribute('text', JSON.stringify({a: 10}));
}
}
const parentClick = () => {
console.log('父组件直接触发');
}
wButton.addEventListener('onCustomClick', e => console.log(e));
</script>
</html>
tips
- 把样式和标签跟 js 分开,参考的是 jQuery 的 load 方法,其实就是用 xhr 加载。如果不分开,可以在 js 里面用字符串模板实现,外部加载可以更好的编写标签和样式,如果使用 webpack、vite 等,可能更容易实现
- 样式和标签隔离,Shadow DOM 天然支持,实现的非常的好
- 组件传值,可以用属性,static get observedAttributes()一定要定义,父组件可以改变组件的值,子组件可以监听值的变化,实现子组件根据数据处理。传递的数据,如果是对象,需要序列化一下,因为只能传递字符串
- 如果单纯的组件样式,触发点击事件可以直接 onclick 绑定,如果是子组件回调,最好使用 new CustomEvent 创建,父组件通过监听获取回调的数据
- new CustomEvent 一定要把数据放在 detail 里面,可以是任意数据类型
- slot 也有具名和不具名,具名插槽在父组件使用正常的标签
- 生命周期直接声明就监听了,adoptedCallback 不知道怎么实现
Web Component 也有一些限制,比如 css 隔离的太彻底,和外部交互比较困难,要做双向数据绑定也比较不容易。不过都不算问题,现在已经有很多实现了 Web Component 的库,可以轻松的使用和构建 Web Component,那些问题都容易解决。个人觉得,Web Component 最大的意义是,可以在不同的框架之间共享组件,比如 Vue、React、Angular,这些框架都有各自的组件,但是 Web Component 的组件可以跨框架使用。
需要意识到,组件化,一般是在一些复用、数据结构稍微复杂的项目使用,组件的目的就是把整体的复杂性解构,从而提高开发效率和可维护性。如果是一些静态为主,加上一些少量交互,就没必要了,反而更不适用。
都 2023 年了,为什么没有哪一家大公司牵头创建 Web Component 生态和推广 Web Component 呢?
欢迎关注订阅号 coding 个人笔记