WebComponent是个什么东西?
web component是官方定义的自定义组件实现方式,它可以让开发者不依赖任何第三方框架(如Vue,React)来实现自定义页面组件;达到组件复用效果。
怎么使用webcomponent来实现自定义组件
接下来,我会用一步一步实现一个简单的自定义组件,该组件包含一个button和ul列表,点击按钮会动态添加列表项。
先来看下最终的效果
定义和使用web component三部曲
-
继承HTMLElement实现自定义类
class MyButton extends HTMLElement { constructor() { super(); } } 复制代码
-
定义组件模板
<template> </template> 复制代码
-
注册自定义元素
window.customElements.define(name, ComponentClass) 复制代码
来完成一个最简单的自定义组件
-
定义自定组件
class MyButton extends HTMLElement { constructor () { super(); const template = document.getElementById('mybutton'); const content = template.content.cloneNode(true); this.appendChild(content); } } 复制代码
-
定义组件模板
<template id="mybutton"> <button>Add</button> </template> 复制代码
-
注册组件
window.customElements.define('my-button', MyButton); 复制代码
-
使用组件
<body> <my-button></my-button> </body> 复制代码
通过以上四个步骤,就可以定义一个自定义组件并成功渲染到页面。
最终效果如下
总结一下这一小节的知识点
- 使用es6类继承HTMLElement类,而且要在子类的构造函数中调用super(),在构造函数中获取template内容,并添加在当前实例中
- 定义组件模板,使用<template>标签
- 通过全局api
window.customElements.define()
注册自定义组件,不然这个组件是无法使用 - 像使用一般标签元素一样使用自定义组件
有一点需要注意的,const content = template.content.cloneNode(true);
这一行代码深度复制了一份模板,这是因为这个模板可能在多个地方被使用,如果直接使用模板本身,就会相互影响。
以下是完整代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Web Component</title>
</head>
<body>
<my-button></my-button>
<template id="mybutton">
<button>Add</button>
</template>
</body>
<script>
class MyButton extends HTMLElement {
constructor () {
super()
const template = document.getElementById('mybutton');
const content = template.content.cloneNode(true);
this.appendChild(content);
}
}
window.customElements.define('my-button', MyButton)
</script>
</html>
复制代码
接下来我们一步一步的完善和丰富这个自定义组件。
给组件添加样式
我们在上面仅仅定义了一个组件的模板骨架,并没有使用CSS来修饰我们的组件,在template中可以为组件添加样式。
<style>
button {
width: 60px;
height: 30px;
cursor: pointer;
color: blue;
border: 0;
border-radius: 5px;
background-color: #F0F0F0;
}
</style>
<template>
<button>
Add
</button>
</template>
复制代码
添加样式后的自定义按钮
怎么接收外部参数?
一个组件如果仅仅是具备上面的能力,那么复用性就会很差,因为它无法根据参数动态变化;WebComponent可以接收外部参数,那么开发者就能根据参数来对组件进行不同的处理与渲染。
- 在自定义组件上添加需要传递的属性
<body>
<my-button
text="Hello"
></my-button>
</body>
复制代码
- 自定义组件类中获取属性值,并按需使用
class MyButton extends HTMLElement {
constructor () {
super()
const template = document.getElementById('mybutton');
const content = template.content.cloneNode(true);
+ const text = this.getAttribute('text');
+ content.querySelector('#btn').innerText = text;
this.appendChild(content);
}
}
复制代码
添加到组件上的属性可以通过this.getAttribute(name)
获取。我们可以给组件添加任意多的属性。
最终渲染的结果如下
怎么给外部发送参数?
如果组件需要和外部交互,就需要具备向外部发送数据的能力。
有两种方式,1)在自定义组件上添加自定义方法,并在组件内监听DOM元素事件,在需要的地方调用组件的自定义方法;2)使用元素的自定义事件
1) 添加自定义方法
class MyButton extends HTMLElement {
constructor () {
super()
const template = document.getElementById('mybutton');
const content = template.content.cloneNode(true);
const button = content.querySelector('#btn');
const text = this.getAttribute('text');
button.innerText = text;
+ button.addEventListener('click', (evt) => {
+ this.onClick("Hello from within the Custom Element");
+ })
this.appendChild(content);
}
}
复制代码
document.querySelector('my-button').onClick = value => {
console.log(value);
}
复制代码
2)使用元素自定义事件
class MyButton extends HTMLElement {
constructor () {
super()
const template = document.getElementById('mybutton');
const content = template.content.cloneNode(true);
const button = content.querySelector('#btn');
const text = this.getAttribute('text');
button.innerText = text;
button.addEventListener('click', (evt) => {
+ this.dispatchEvent(
+ new CustomEvent('onClick', {
+ detail: 'Hello fom within the Custom Element'
+ })
)
})
this.appendChild(content);
}
}
+ document.querySelector('my-button').addEventListener('onClick', (value) =>
+ console.log(value));
复制代码
可以像使用Vue插槽slot一样定义原生slot
现在来思考以下,能不能像使用Vue的slot一样,在webComponent中使用插槽呢?同样也是可以的,实际上,Vue是参考了webComponent的规范实现slot的。下面就看看如何在web component中使用slot。
首先要在模板里定义一个slot标签插槽占位,而且插槽可以添加默认显示内容
<template id="mybutton">
<style>
button {
color:blue;
border: 0;
border-radius: 5px;
background-color: brown;
}
</style>
<button id="btn">My Button</button>
+ <p><slot name="my-text">My Default Text</slot></p>
</template>
复制代码
然后使用组件的时候,给内容添加slot属性,指定需要填充的slot
<my-button>
+ <p>Another Text from outside</p>
</my-button>
复制代码
最后渲染出来的效果如下
监听属性变化
在“怎么接收外部参数?”一节,我们知道怎么给组件传递数据;如果传递进来的数据发生变化了,我们期望的结果是组件内容也会随之变化,和Vue数据绑定一样;在web component中也能实现类似的功能。
先来认识三个新函数
static get observedAttributes()
attributeChangedCallback(name, oldVal, newVal)
render()
第一个函数是静态get函数,它的作用是定义那些属性需要被监听;
第二个函数是属性变化时的回调函数,也就是说,每一个被监听的属性,只要属性值发生变化,都会调用这个函数;
第三个函数是渲染函数,属性更新后,如果要重新渲染组件,就要调用这个函数。
下面就来实现如何监听属性并更新组件(以下所有函数都是添加到自定义组件类中,也就是本文例子的MyButton类)
- 定义需要监听的属性
static get observedAttributes() {
return ['text'];
}
复制代码
- 定义回调函数
attributeChangedCallback(name, oldVal, newVal) {
this[name] = newVal;
this.render();
}
复制代码
- 重新渲染
render() {
this.$button.innerText = this.text;
}
复制代码
this.$button
需要在构造函数中定义
完整的类定义如下
class MyButton extends HTMLElement {
constructor () {
super()
const template = document.getElementById('mybutton');
const content = template.content.cloneNode(true);
const button = content.querySelector('#btn');
const text = this.getAttribute('text');
+ this.$button = button;
button.innerText = text;
button.addEventListener('click', (evt) => {
this.dispatchEvent(
new CustomEvent('onClick', {
detail: 'Hello fom within the Custom Element'
})
)
})
this.attachShadow({ mode: 'open' }).appendChild(content);
}
+ static get observedAttributes() {
+ return ['text'];
+ }
+ attributeChangedCallback(name, oldVal, newVal) {
+ this[name] = newVal;
+ this.render();
+ }
+ render() {
+ this.$button.innerText = this.text;
+ }
}
复制代码
可以通过代码动态改变自定义组件上的属性值,下面通过在Chrome devtool来模拟一下
定义属性的set、get函数
在上面的例子中,有两个不是很方便的点,1)每次属性更新,都需要在attributeChangedCallback
函数中手动赋值;2)属性只能添加到DOM元素上;我们希望能直接在JavaScript代码里像给对象赋值一样改变属性值。
给每一个需要被监听的属性添加get函数
get text () {
return this.getAttribute('text');
}
复制代码
再定义属性的set函数
set text (value) {
this.setAttribute('text', value);
}
复制代码
现在我们就能直接给组件对象定义属性并赋值,而不需要定义在组件DOM元素上。如下
const mybutton = document.querySelector('my-button');
mybutton.text = 'Hi!';
复制代码
生命周期
除了上面提到的知识点,我们还需要了解一下web component的生命周期,可以在自定义组件的不同时期做我们需要的操作。
connectedCallback
当web component被添加到DOM时,会调用这个回调函数,这个函数只会被执行一次。可以在这个回调函数中完成一些初始化操作,比如更加参数设置组件的样式。
disconnectedCallback
当web component从文档DOM中删除时执行。
adoptedCallback
当web component被移动到新文档时执行。
attributeChangedCallback
被监听的属性发生变化时执行。
下面是完整的代码
<!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>Document</title>
</head>
<body>
<my-button>
<p slot="my-text">Another text from outside</p>
</my-button>
<template id="mybutton">
<style>
button {
width: 60px;
height: 30px;
cursor: pointer;
color: blue;
border: 0;
border-radius: 5px;
background-color: #F0F0F0;
}
</style>
<div>
<button id="btn">Add</button>
<p><slot name="my-text">My Default Text</slot></p>
</div>
</template>
</body>
<script>
class MyButton extends HTMLElement {
constructor () {
super()
const template = document.getElementById('mybutton');
const content = template.content.cloneNode(true);
const button = content.querySelector('#btn');
this.$button = button;
button.addEventListener('click', (evt) => {
this.dispatchEvent(
new CustomEvent('onClick', {
detail: 'Hello fom within the Custom Element'
})
)
})
this.attachShadow({ mode: 'open' }).appendChild(content);
}
get text () {
return this.getAttribute('text');
}
set text (value) {
this.setAttribute('text', value);
}
static get observedAttributes() {
return ['text'];
}
attributeChangedCallback(name, oldVal, newVal) {
this.render();
}
render() {
this.$button.innerText = this.text;
}
}
const mybutton = document.querySelector('my-button');
mybutton.addEventListener('onClick', (value) => {
console.log(value)
mybutton.text = value
});
window.customElements.define('my-button', MyButton)
</script>
</html>
复制代码
止步于此?
通过上面的步骤,我们已经把web component的必须知识点都过了一遍;不过我们是直接把组件定义在HTML文档里,这样在实际项目中无法达到复用的效果,因为我们不可能把代码直接些到其他人的页面上,而是通过引用的方式来调用,所以我们要实现组件模块化。
可以把web component定义在一个js文件中,然后通过script标签被其他人引用。下面就来对前面的组件进行改造。
使用模板字符串定义组件模板、编写自定义组件类、注册,新建一个MyButton.js文件
完整的代码如下
const template = document.createElement('template');
template.innerHTML = `
<style>
button {
width: 60px;
height: 30px;
cursor: pointer;
color: blue;
border: 0;
border-radius: 5px;
background-color: #F0F0F0;
}
</style>
<div>
<button id="btn">Add</button>
<p id="message"><slot name="my-text">My Default Text</slot></p>
<ul id="text-list"></ul>
</div>
`;
const Texts = [
'My lady, Hello!',
'BuiBuiBui',
'BiliBili',
'Haiwei is NO.1'
]
class MyButton extends HTMLElement {
constructor () {
super()
const content = template.content.cloneNode(true);
const button = content.querySelector('#btn');
const textList = content.querySelector('#text-list');
this.$button = button;
this.$message = content.querySelector('#message');
button.addEventListener('click', (evt) => {
const li = document.createElement('li');
li.innerText = Texts[Math.floor(Math.random() * 4)];
textList.appendChild(li);
this.dispatchEvent(
new CustomEvent('onClick', {
detail: 'Hello fom within the Custom Element'
})
)
})
this.attachShadow({ mode: 'open' }).appendChild(content);
}
get text () {
return this.getAttribute('text');
}
set text (value) {
this.setAttribute('text', value);
}
static get observedAttributes() {
return ['text'];
}
attributeChangedCallback(name, oldVal, newVal) {
this.render();
}
render() {
this.$message.innerText = this.text;
}
}
window.customElements.define('my-button', MyButton)
复制代码
在文档中调用
<!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>Document</title>
<script src="./MyButton.js"></script>
</head>
<body>
<my-button>
<p slot="my-text">Another text from outside</p>
</my-button>
</body>
<script>
const mybutton = document.querySelector('my-button');
mybutton.addEventListener('onClick', (value) => {
console.log(value)
mybutton.text = value.detail
});
</script>
</html>
复制代码
到这里,我们就完成了一个可供复用的web component了。