前沿
先说说为什么要实测下Web Components。都知道现在其实有Vue、React很成熟框架,那有没有认真考虑过为什么要使用框架呢,其实最终目的就只是减少工作量
,框架可以减少重复的代码,但框架也引进了不少原生不支持的东西,增加了心智负担。当看到Web Components我就在想能不能尝试替代下Vue,毕竟Vue最大的特点就是组件封装复用。
Web Components构成
从构成来看和vue的模版非常的相似,主要以下三部分的组成:
- Custom element(自定义元素) :一组 JavaScript API,允许你定义 custom elements 及其行为,然后可以在你的用户界面中按照需要使用它们。
- Shadow DOM(影子 DOM) :一组 JavaScript API,用于将封装的"影子"DOM 树附加到元素(与主文档 DOM 分开呈现)并控制其关联的功能。通过这种方式,你可以保持元素的功能私有,这样它们就可以被脚本化和样式化,而不用担心与文档的其他部分发生冲突。
- HTML template(HTML 模板):
<template>
和<slot>
元素使你可以编写不在呈现页面中显示的标记模板。然后它们可以作为自定义元素结构的基础被多次重用。
使用
网上博客或者教程讲的都比较八股文,很多点都没有提及。先讲基本使用再讲一些可能你没有思考的点。
自定义组件 WebCompent.js
js
class ExtendP extends HTMLElement {
constructor() {
super()
// 创建模版
const template = document.createElement('template')
template.innerHTML = `
<style>
article {
width: 20%;
margin: 20px auto;
border: solid 1px gray;
padding: 8px;
}
header {
background: lightblue;
color: #fff;
font-size: 24px;
border: solid 1px lightblue;
}
</style>
<article>
<header>
<slot name='title'>博客标题</slot>
</header>
<section>
<slot name='cont'>博客内容博客内容博客内容博客内容博客内容博客内容...</slot>
</section>
</article>
`
// 获取影子dom
const iRoot = this.attachShadow({ mode: 'open' })
//将模版添加到影子dom中
iRoot.appendChild(template.content.cloneNode(true))
}
static get observedAttributes() {
return ["color", "size"];
}
get size() {
return this.getAttribute('size');
}
sayHello() {
console.log('Hello customElements');
}
connectedCallback() {
console.log("自定义组件添加至页面。");
this.ready();
}
disconnectedCallback() {
console.log("自定义组件从页面中移除。");
}
adoptedCallback() {
console.log("自定义组件移动至新页面。");
}
attributeChangedCallback(name, oldValue, newValue) {
console.log(`属性 ${name} 已变更${oldValue}。${newValue}`);
}
}
customElements.define('extend-p', ExtendP)
- 自定义元素必须继承HTMLElement(或者其子类)
- createElement('template')创建模版
- 使用attachShadow获取影子dom
- appendChild添加模版内容,使用cloneNode,避免模版污染
- customElements.define('extend-p', ExtendP)定义一个组件名,以便使用
最基本的自定义组件到此其实已经完成。
扩展点:
- 最初我使用的是WebCompent.html的形式去写这个自定义组件,这样有一个好处,可以直接使用
<template></template>
标签去写样式和标签,这样就能支持编辑器提示、排版,而不用像上面一样只能使用字符串模版了。但在html中导入另一html做不到(link标签现在不支持直接导入html了),当然非要的话可以使用get请求加载,但个人感觉这样就失去了意义。- 监听属性变化必须要使用
static get observedAttributes
声明需要监听哪些属性,然后就能在attributeChangedCallback
回掉中监听到了。- 也是有生命周期的不过就三个,相对简单,上面有写。
- 也是支持插槽slot的,和vue一样使用
<slot name='cont'></slot>
在html中使用
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>test</title>
<script src="./WebCompent.js" defer></script>
</head>
<body>
<extend-p id="extendP" size="100">
<div slot="title">zidingyititle</div>
</extend-p>
</body>
<script>
const extendP = document.getElementById('extendP')
extendP.ready = () => {
extendP.sayHello();
extendP.setAttribute('size', 200)
}
window.onload = (e) => {
extendP.setAttribute('color', 'b')
extendP.sayHello();
}
</script>
</html>
- 引入自定义个人建议加上defer,原因后面再讲。
- 元素标签使用和vue基本相同,就是组件define时的名字
- slot="title"指定插槽名字
- 在vue的组件中直接
import "./components/WebCompent/WebCompent.js";
就能直接像上面一样使用
使用很简单,但这里又有几个非常坑的地方:
- 正常情况下,自定义组件内部会有业务,父组件肯定也有一些业务,需要暴露给自组件的。就像上面自定义组件中被添加到页面connectedCallback后,需要通知父组件,所以调用了this.ready()方法,这个方法是父组件定义的,ready的定义一定要早于自定义组件。如果connectedCallback执行比ready定义早就会报错,所以在1中要加个defer。
- 还有如果要调用自定义组件,必须要自定义组件创建完成。所以上面我调用的时候定义了ready方法,等connectedCallback后执行,或者等页面onload后执行。结合上面的,定义要在前,调用要在后,父子交错,还是挺割裂的。
- 属性监听一定要使用setAttribute去修改,直接extendP.attributes['size']=200修改是不会触发监听的。
- slot传值没百度到,应该没有。
结尾
怎么说呢,功能上来讲也算七七八八,基本的也都有了。感觉下来单控件的实现应该不错,代码原生,不需要编译。最主要的问题是使用上还有一些割裂的感觉;如果按现在的去定义一些方法、属性,使用时会不知道从何下手(并不是代码有多难),代码提示没有,基本只能靠作者文档。