引用 MDN 的话:Web Components 是一套不同的技术,允许您创建可重用的定制元素(它们的功能封装在您的代码之外)并且在您的 web 应用中使用它们。
简单来说就是官方定义的自定义组件的方式,封装代码,提高代码的复用性。
基本用法
- 首先定义内容模板
template
,可以把它理解为一个内容片段,稍后使用 JS 进行实例化。解析器虽然会处理这个片段,但是不会渲染在页面上。
<template id="myTemplate">
<h1 id="title">这是一个大标题</h1>
</template>
- 再使用该模板,定义一个类继承自
HTMLElement
,复制该模板并append
,这里之所以用cloneNode
复制是为了防止该组件被多处使用时,修改了一处,其他地方也跟着变化,参数为true
表示递归克隆子元素。
class MyComponent extends HTMLElement {
constructor() {
super();
const template = document.querySelector('#myTemplate')
const content = template.content.cloneNode(true)
this.appendChild(content)
}
}
- 注册自定义元素,使用
customElements
上的define
方法。
window.customElements.define('my-component', MyComponent)
- 使用自定义元素。为了区别与非自定义组件,自定义组件名称通常以“-”分隔。
<my-component id="myComponent"></my-component>
此时可以看到页面呈现了刚才定义的内容:
添加样式
直接在模板中添加 style
标签:
<template id="myTemplate">
<style>
h1 {
color: red
}
</style>
<h1>这是一个大标题</h1>
</template>
刷新页面,文字变红:
自定义属性
为了提高可复用性,组件中需要根据传入参数展示不同的内容。
<my-component id="myComponent" title="这是传入的标题"></my-component>
传入一个 title
属性,用 this.getAttribute
方法可以取到该属性。
class MyComponent extends HTMLElement {
constructor() {
super();
const template = document.querySelector('#myTemplate')
const content = template.content.cloneNode(true)
const title = content.querySelector('#title')
const propTitle = this.getAttribute('title')
title.innerText = propTitle
this.appendChild(content)
}
}
此时页面中标题的内容已变为传入的属性:
与父组件通信
其实自定义属性就是父子组件通信的方式,子父组件通信可以用自定义事件来实现。
class MyComponent extends HTMLElement {
constructor() {
super();
const template = document.querySelector('#myTemplate')
const content = template.content.cloneNode(true)
const title = content.querySelector('#title')
const myEvent = new CustomEvent('myEvent', {
detail: '这是子组件传过来的消息'
})
title.addEventListener('click', () => {
this.dispatchEvent(myEvent)
})
this.appendChild(content)
}
}
自定义了一个 myEvent
事件(至于如何自定义事件就不详细展开了),注意携带的消息一定要放在 detail
属性中,放在其他属性会找不到,如果数据很多的话 detail
也可以是一个对象。当点击标题的时候把该事件 dispatch
出去。
const myComponent = document.querySelector('#myComponent')
myComponent.addEventListener('myEvent', val => {
console.log(val)
})
在 myComponent
监听该事件,作出对应的处理。
当点击标题时,打开控制台可以看到该事件的详细信息,刚才传入的信息在 detail
属性中保存。
shadow DOM
当封装一个组件时,组件内部是不应该可以从外部修改的,但是我们现在封装的组件很明显可以在外部修改,在控制台输入 document.getElementById('title').innerText = '我要修改大标题'
发觉标题文字直接被改掉了:
这不符合预期,需要将结构、样式和行为隐藏起来,并与页面上的其他代码相隔离,保证不同的部分不会混在一起。这里就要用到 shadow DOM 。 attachShadow()
方法来将一个 shadow root
附加到任何一个元素上,该方法返回一个 shadow root
。
class MyComponent extends HTMLElement {
constructor() {
super();
const template = document.querySelector('#myTemplate')
const content = template.content.cloneNode(true)
const shadowRoot = this.attachShadow({ mode: 'open' })
shadowRoot.appendChild(content)
}
}
此时页面结构已经发生了变化:
多了一行 shadow-root(open)
。
再对标题进行修改直接报错,因为该 dom 节点已经变成了“shadow”:
CSS伪类
:host
该选择器可以选择组件的根元素。
:host {
display: block;
background: red;
}
在控制台中查看该自定义组件根节点的样式:
此时该组件背景变为了红色:
:defined
表示组件内任何已定义的元素。
:defined {
font-style: red;
}
这时可以看到组件中的每一个元素都被添加了斜体样式:
页面上的文字也变为了斜体:
插槽
插槽可以将内容片段插入到指定的位置。插槽的内容由父组件控制,而插在哪里由子组件控制,提高了代码的复用性。
将需要插入的内容传入组件:
<my-component id="myComponent">
<h2>这是副标题</h2>
</my-component>
把 slot
标签放在想要插入内容片段的位置:
<template id="myTemplate">
<h1 id="title">这是一个大标题</h1>
<slot></slot>
</template>
此时页面中有了副标题:
数据驱动视图
当传入的数据变化时,应当监听数据的变化,并对 dom 作出修改。
这里涉及到两个方法:
static get observedAttributes
该静态方法需要返回一个数组,数组内是需要监听变化的属性名。attributeChangedCallback
当监听的属性变化时,会触发该函数,函数体内是监听的函数变化时需要执行的逻辑。
class MyComponent extends HTMLElement {
constructor() {
super();
const template = document.querySelector('#myTemplate')
const content = template.content.cloneNode(true)
const title = content.querySelector('#title')
this.$title = title
const propTitle = this.getAttribute('title')
title.innerText = propTitle
const shadowRoot = this.attachShadow({ mode: 'open' })
shadowRoot.appendChild(content)
}
static get observedAttributes() {
return ['title']
}
attributeChangedCallback() {
const propTitle = this.getAttribute('title')
this.$title.innerText = propTitle
}
}
给组件绑定一个点击事件,点击时修改传入的属性:
document.querySelector('#myComponent').addEventListener('click', e => {
e.target.setAttribute('title', '传入的标题被修改了')
})
当点击标题时,标题的内容改变了:
生命周期
当组件在页面中被插入和移除时,有对应的处理函数去处理对应的逻辑:
constructor
当组件被创建时connectedCallback
当组件被插入到 dom 中时调用disconnectedCallback
当组件从 dom 中被移除时调用adoptedCallback
当自定义元素移动到新文档时调用attributeChangedCallback
当传入属性改变时调用errorCallback
错误处理函数
写在后面
由于有些功能只有在 shadow DOM 中才会生效,例如 slot
插槽 、 :host
选择器等,所以在封装自定义组件时建议始终使用 shadow DOM 。
这些基本是 Web Components 最常用的一些功能了,用过 Vue 的朋友一定发现这些概念和用法都太像了,确实,Vue 在编写时参考了 Web Components 规范。最近这几年关于 Vue 的争议始终不断,经常能看到关于它模仿其他框架的评论。在我看来这也只是站在了巨人的肩膀上,取其精华去其糟粕,其实这也是它能够如此流行的原因,技术没有高低,好用就行了。