其实Web components的规范已经提出好多年了,但是依赖浏览器的底层实现,还有生态的发展,跟其他JS框架相比,一直没有火起来,尤其是国内,国外用得要多一些。最近研究了一下,以备不时之需。毕竟程序员要一直保持学习的状态…出来的技术或多或少了解一下。
写了一个系列文章,分为一下三篇,会陆续更新:
好了,现在开始正式的介绍。
概念
像React、Vue这样的前端框架都有组件化开发的概念,但是这些都依赖一个运行时代码。那么浏览器能否从底层上就支持组件化呢,于是制定web规范的组委会提出了一些规范,来让浏览器可以原生支持组件化的开发方式。这样,对浏览器厂商来说,需要实现这些规范,对程序员来说就是调用一些API。
主要包括以下三个规范:
- Custom elements (标准html规范)
通过一组JS API创建自定义html标签,也就是拓展了现有的内置html标签。并且提供了类似vue、react的生命周期钩子函数等,能够在适当的时机进行一些操作。
- Shadow DOM (标准DOM规范)
通过一组JS API 能创建一棵shadow dom tree,可以插入到当前html文档中。主要提供了CSS和DOM scoping。shadow dom tree内部的css以及DOM与文档其他部分是完全隔离的,因此内部也能用更通用的CSS选择器,而不必担心命名冲突,避免全局污染。
另外shadow root中的元素通过document.querySelector
无法被获取到,只能通过先拿到shadow dom tree的根节点shadowRoot,用shadowRoot.querySelector
这种方式访问。
可以看到,Shadow DOM主要作用是提供了封装性。
备注:document范围内的内容都被称为light DOM,shadow root中的内容被称为shadow DOM。
- HTML templates (标准HTML规范)
通过<template>
、<slot>
直接在HTML 文件定义可复用的结构,在文档中不会被渲染但是能通过DOM API获取:
<template id="myTpl">
<p class="name">Name</p>
<slot></slot>
</template>
复制代码
这三个规范并非一定要一起使用,只不过一起使用能更好地封装一个自定义组件。
兼容性和适用场景
- Custom Elements
- Shadow DOM
- HTML Templates
对于不兼容的浏览器可以使用polyfills: webcomponents
/
polyfills,能够支持到IE11。
由于web components依赖浏览器底层的实现,而不限定于某个特定的前端框架,例如Vue、React、Angular等,因此比较适合需要跨团队共享的UI组件,这类组件用web components来开发,就可以不用限定consumer适用的框架类型了。
另外,像Vue、React等的生态发展比较完善,提供了诸如路由、状态存储等周边,开发效率上也比用原生的web components高,因此web components基本也是需要与前端框架配合使用而不会取代这些流行的前端框架。
具体用法
这块讲得相对有点细,如果不想一开始纠结细节的,直接拉到最下面看一个完整的Button组件例子。另外我还写了一个Dialog,Dropdown组件,上传到github上了,欢迎down下来看,git传送门:my-web-component。
Custom elements
注册自定义元素
customElements.define(tagName, constructor, options)
复制代码
- tagName:组件名称,采用kebab-case规则,且必须带
-
。 - constructor:在其中对组件进行具体定义。采用es6 class的方式定义,如果不需要对现有的html标签进行扩展则继承
HTMLElement
,以确保其拥有内置HTML标签的所有默认方法和属性。
class MyButton extends HTMLElement {
constructor() {
super()
// do something
}
}
customElements.define('my-button', MyButton)
复制代码
- options:可选,目前只有一个选项
extends
,当创建的元素需要继承某个已有的内置元素时。此时需要extends
特定的标签类,例如HTMLButtonElement
。
class MyButton extends HTMLButtonElement {
consotructor() {
super()
// do something
}
}
customElements.define('my-button', MyButton, {extends: 'button'})
复制代码
使用继承了内置元素的自定义组件:
在html中使用:
<button is="my-button">click</button>
复制代码
在js中使用:
document.createElement('button', {is: 'my-button'})
复制代码
生命周期钩子
- connectedCallback
当custom element首次被插入文档DOM时被调用。
- disconnectedCallback
当custom element从文档DOM中删除时被调用。
- adoptedCallback
当custom element被移动到新的文档时被调用。(例如Iframe和主窗口之间)
- attributeChangedCallback
当custom element上的attributes进行了添加、修改、删除时被调用。(当用户初次使用标签并传入属性时,attributeChangedCallback
就会被调用一次。)
使用方法:
// 1. 该静态方法中返回需要观察的attributes名
static get observedAttributes() {return ['attr1', 'attr2']; }
// 2. 当观察的attributes发生变动时触发该钩子
attributeChangedCallback(attrName, oldValue, newValue) {
// do something
}
复制代码
注意: attributes只接收string类型的。当需要传入对象类型时(例如Object、Array),要么JSON.stringify
,要么通过通过JS获取到元素并且设值为property,推荐采用第二种方式,尤其是传递function时:
const myButton = document.querySelector('my-button');
myButton.label = 'Click Me';
复制代码
此时attributeChangedCallback
无法监听元素property的变化,需要通过setter同步attribute
:
set label(value) {
this.setAttribute('label', value);
}
复制代码
如果是引用类型的数据,则完全没有必要再同步到attribute上了。可以直接在setter中更新渲染等操作而不是通过attributeChangedCallback
。
Shadow DOM
挂载shadow Dom
组件内部创建Shadow DOM:
const shadowRoot = element.attachShadow({mode: 'open'})
复制代码
该方法在自定义元素内部挂载了一棵Shadow DOM Tree,并且返回对ShadowRoot的引用。
例子:
// 获取页面的
const $root = document.getElementById('root');
const shadow = $root.attachShadow({mode: 'open'});
const $p = document.createElement('p');
$p.innerText = '创建一个 shadow 节点';
shadow.appendChild($p);
复制代码
- element
可以是一切自定义标签或者允许的现有html标签。PS: 并非所有标签下都能挂载Shadow DOM,这里给出了一个可以在下面挂载Shadow DOM的元素列表:MDN Element.attachShadow()
shadowRoot可以像其他DOM节点一样调用appendChild
方法添加子节点。
- mode
有两个值,主要就是用来控制外部是否能过获取到shadowRoot从而对shadow dom tree内部进行节点访问(例如添加或者修改等):
- open: 表示shadowDOM subtree之外的JS代码能访问到其内部的元素,例如通过
element.shadowRoot
能获取到shadowRoot的引用。 - closed:反之。当调用
element.shadowRoot
时返回null
。
- shadowRoot
是shadow DOM tree的根节点,整棵树相较与原本的节点树会进行单独渲染。
shadowRoot继承了Node
,因此拥有普通htmlNode节点的属性和方法。此外还有一些自己的一些属性和方法:MDN ShadowRoot
HTML template
提供了组件模板,slot等功能,可以给slot指定名字变成具名slot,熟悉Vue的同学应该比较清楚,就跟Vue里slot的用法一毛一样。
通过JS API来获取模板内容并插入到shadow dom tree中:
const templateContent = document.querySelector('#tpl-id').content
// 由于模板需复用,在将模板内容添加到节点里时,需cloneNode
const shadowRoot = this.attachShadow({mode: 'open'})
shdowRoot.appendChild(templateContent.cloneNode(true))
复制代码
完整的Button组件例子
下面是一个简单的自定义button元素例子:
const myButtonTemplate = document.createElement('template');
myButtonTemplate.innerHTML = `
<style>
.container {
padding: 8px;
}
button {
display: block;
overflow: hidden;
position: relative;
padding: 0 16px;
font-size: 16px;
font-weight: bold;
text-overflow: ellipsis;
white-space: nowrap;
cursor: pointer;
outline: none;
width: 100%;
height: 40px;
box-sizing: border-box;
border: 1px solid #a1a1a1;
background: #ffffff;
box-shadow: 0 2px 4px 0 rgba(0,0,0, 0.05), 0 2px 8px 0 rgba(161,161,161, 0.4);
color: #363636;
cursor: pointer;
}
</style>
<div class="container">
<button>Label</button>
</div>
`;
class MyButton extends HTMLElement {
constructor() {
super();
this._shadowRoot = this.attachShadow({ mode: 'open' });
this._shadowRoot.appendChild(myButtonTemplate.content.cloneNode(true));
this.$button = this._shadowRoot.querySelector('button')
// custom event
this.$button.addEventListener('click', () => {
this.dispatchEvent(new CustomEvent('clicked', {
detail: {
clickMessage: 'Hello from within the Custom Element'
}
}))
})
}
// Reflecting an attribute to a property
get label() {
return this.getAttribute('label')
}
// Reflecting a property to an attribute
set label(value) {
this.setAttribute('label', value)
}
static get observedAttributes() {
return ['label']
}
attributeChangedCallback(name, oldValue, newValue) {
this.render()
}
render() {
this.$button.innerHTML = this.label
}
}
window.customElements.define('my-button', MyButton);
复制代码
在html中的使用:
<body>
<my-button label="Click Me!"></my-button>
<script src="./components/MyButton.js"></script>
<script>
document.querySelector('my-button').addEventListener('onClick', (event) => {
console.log('event', event)
})
</script>
</body>
复制代码
git传送门:my-web-component
这里上传了更完整的代码,还包括Dialog和Dropdown组件,其中Dropdown组件给出了如何处理引用类型的数据。