什么是WebComponent
首先 WebComponent
直译过来第一感觉是 web 组件的意思,实际上它是 HTML5 推出的一个新特征,在这个依赖前端框架的时代,更推崇组件化开发的思想来编写页面。
例如 Vue、React 等框架都基于组件化开发的形式,但它们的组件生态并不互通。要是脱离了 Vue、React 这样的前端框架,原生 JavaScript
就不能自定义组件吗?
WebComponent
就是为了解决这个问题的,WebComponent
是一套规则、一套 API。可以通过这些 API 创建自定义的组件,并且组件是可以重复使用的,封装好的组件可以在网页和 Web 应用程序中进行使用。
并非所有业务场景都需要 Vue、React 这样的框架进行开发,也并非都是工程化,很多业务场景也是依赖原生 JavaScript 和 HTML 的,那么 WebComponent
实现的组件也可以和原生 HTML 一起使用
html
<h1>WebComponent组件化</h1>
<!-- 自定义组件 -->
<custom-component></custom-component>
上面看到 <h1>
标签是原生的 HTML 标签,但是 <custom-component>
标签就是自定义组件的标签了,它不属于 HTML 语义化标签中的任何一个, 最后自定义组件展示的结果如下:
要了解 WebComponent
,首先要从它的三个主要技术切入,其旨在解决创建封装功能的定制元素的问题,可以在任何地方重用,不必担心代码冲突。
- Custom element(自定义元素)
- Shadow DOM(影子 DOM)
- HTML template(HTML 模板)
Custom element
所谓自定义元素,即当内置元素无法为问题提供解决方案时,需要自己动手来创建一个自定义元素来解决,上方的 <custom-component>
就是手动创建的自定义元素。
html
<!-- 自定义组件 -->
<custom-component></custom-component>
css
/* 组件样式 */
.custom-component {
display: inline-block;
padding: 15px;
border: 1px solid red;
border-radius: 5px;
color: blue;
}
js
// 创建自定义组件
class CustomComponent extends HTMLElement {
constructor() {
super();
this.init();
}
init() {
const box = document.createElement('div');// 创建一个div元素
box.className = 'custom-component'; // class样式名称
const text = document.createElement('p'); // 创建一个p元素
text.innerHTML = '这是一个自定义组件'; // 文本内容
box.appendChild(text);
this.appendChild(box); // 挂载到这个自定义组件中
}
}
window.customElements.define('custom-component', CustomComponent);
效果如下:
首先可以看出,自定义组件需要有个类的概念,并且必须继承自内置的 HTMLElement
类,然后定义类一些标记模版,并且执行 this.appendchild
,其中 this 指向了当前类实例。
最后通过 window.customElements.define
方法,将自定义组件挂载到 customElements
上,并且需要给自定义组件起一个名字,上面例子的命名为 custom-component
。
自定义元素的其他相关 API: developer.mozilla.org/zh-CN/docs/...
命名规则
自定义元素命名是有规则的,规则如下:
- 自定义元素的名称必须包含短横线(-)。它确保 html 解析器能够区分常规元素和自定义元素,还能确保 html 标记的兼容性。
- 自定义元素只能一次定义一个,一旦定义无法撤回。
- 自定义元素不能单标记封闭。比如
<custom-component/>
,必须是一对开闭标记,比如<custom-component></custom-component>
。
元素状态
元素的状态是指定义该元素(或者叫做升级该元素)时元素状态的改变,升级过程是异步的。元素内部的状态有如下几种:
- undefined未升级: 即自定义元素还未被 define。
- failed升级失败: 即 define 过了也实例化了,但失败了,会自动按
HTMLUnknownElement
类来实例化。 - uncustomized未定制化: 没有 define 过但却被实例化了,会自动按
HTMLUnknownElement
类来实例化。 - custom升级成功: define 过并且实例化成功了。
到这里,是不是有种 Promise 的感觉了,元素的状态和 Promise 的三种状态及其相似。
css伪类
与自定义元素特别相关的伪类:
:defined
: 匹配任何已定义的元素,包括内置元素和使用window.customElements.define()
定义的自定义元素。
css
:defined {
font-weight: bold;
font-size: 30px;
}
调用方式
除了以标签的形式使用自定义元素,还可以通过 document 的 API 或者 new 构造函数进行调用
js
// createElement形式
const customComponent = document.createElement ('custom-component');
document.body.appendChild(customComponent);
js
// new 构造函数形式
const customComponent = new customComponent()
document.body.appendchild(customComponent)
思考问题: 如果 dom 结构很复杂的组件怎么办呢?
目前 DOM 结构比较简单,所以通过 document.createElement
、appendchild
等方法进行构建还不算复杂。如果 DOM 结果很复杂,一顿使用 createElement
也不是办法,这就要引入 <template>
标记了。
HTML template
WebComponent
的 API 提供了 <template>
标签,可以在它里面使用 HTML 定义 DOM 结构,现在对刚刚的例子进行改造,重新创建一下我们的自定义组件。
html
<custom-component></custom-component>
<template id="custom-id">
<div class="custom-component">
<p>这是一个自定义组件</p>
</div>
</template>
js
class CustomComponent extends HTMLElement {
constructor() {
super();
this.init();
}
init() {
const template = document.getElementById('custom-id');
const content = template.content.cloneNode(true); // 克隆一份
this.appendChild(content);
}
}
改成 template 形式后,是不是更符合我们的开发模式了,这里有两个点需要注意:
- 这里可以用脚本把
<template>
注入网页,这样 JavaScript 脚本跟<template>
就能封装成一个 js 文件,成为独立的组件文件。网页只要加载这个脚本,就能使用<custom-component>
组件。 <template>
标签内的节点必须通过template.content.cloneNode
克隆返回的节点来操作。因为这里获取的 template 不是一个正常的 DOM 结构,template.content
得到的结果是DocumentFragment
节点。并且该模板还要留给其他实例使用,所以不能直接移动其子元素。
props传递
在 Vue 和 React 中使用组件时,经常会涉及到 props 的传递,那么自定义元素也可以接收 props 的传递:
html
<custom-component></custom-component>
<custom-component text="显示这个文本"></custom-component>
这里传入自定义的文本 text,如果有传入 text 内容就展示 text,没有则展示默认值
js
class CustomComponent extends HTMLElement {
constructor() {
super();
this.init();
}
init() {
const template = document.getElementById('custom-id');
const content = template.content.cloneNode(true);
// 新增代码
const textValue = this.getAttribute('text');
if (textValue) {
content.querySelector('.custom-component>p').innerHTML = textValue;
}
this.appendChild(content);
}
}
插槽
我们平常使用组件的时候是可以嵌套的,所以不仅仅需要注意参数注入的形式,还需要兼容嵌套的 children 形式,继续修改自定义组件。
WebComponent
有一个 slot 概念 -- 插槽,它提供了一个"缺口"让给需要嵌套的 DOM 结构,其用法和 Vue 是比较相似的,例如:
html
<!-- 使用插槽 -->
<custom-component>
<p slot="my-slot">这是插槽的内容</p>
</custom-component>
<!-- 不使用插槽 -->
<custom-component text="显示这个文本"></custom-component>
<template id="custom-id">
<div class="custom-component">
<p>这是一个自定义组件</p>
<slot name="my-slot">插槽的默认内容</slot>
</div>
</template>
js
class CustomComponent extends HTMLElement {
constructor() {
super();
this.init();
}
init() {
const template = document.getElementById('custom-id');
const content = template.content.cloneNode(true);
const textValue = this.getAttribute('text');
if (textValue) {
content.querySelector('.custom-component>p').innerHTML = textValue;
}
// 新增代码
const shadow = this.attachShadow({ mode: 'closed' });
// 将template的内容挂载到shadow中
shadow.appendChild(content);
}
}
此时插槽生效了,但是有没有注意到,css 样式却失效了,并且在自定义组件内部多了一个 #shadow-root(closed)
的内容,这点与后面讲到的 shadow DOM
有关。
这是因为使用了 shadow DOM
技术,这和 Vue 组件化开发的 css scoped 也是很相似的,组件外的样式无法影响组件内部样式,所以 css 样式失效了。
现在把 style 标签中样式写到 <template>
内部,样式只对自定义元素内的 DOM 结构生效,并且不会影响外部有同样 class 名称的标签。
html
<p class="custom-component">对外部元素不生效</p>
<template id="custom-id">
<style>
.custom-component {
display: inline-block;
padding: 15px;
border: 1px solid red;
border-radius: 5px;
color: blue;
}
:defined {
font-weight: bold;
font-size: 30px;
}
</style>
<div class="custom-component">
<p>这是一个自定义组件</p>
<slot name="my-slot">插槽的默认内容</slot>
</div>
</template>
思考:如果没有使用shadow DOM,直接将内容挂载到
CustomComponent
中,样式会影响外部元素吗?
此时样式还是写在 <template>
标签内部的,但内容是直接使用 this 挂载,这样自定义元素内的样式是否会影响外部有同样 class 名称的标签呢?
事件
有了参数之后就不能少了事件 Event,现在给 <p>
标签添加一个文本的点击事件,继续对自定义元素来改造升级。
js
class CustomComponent extends HTMLElement {
constructor() {
super();
this.init();
}
init() {
const template = document.getElementById('custom-id');
const content = template.content.cloneNode(true);
const textValue = this.getAttribute('text');
// 获取文本节点
const textDom = content.querySelector('.custom-component>p');
if (textValue) {
textDom.innerHTML = textValue;
}
// 绑定事件
textDom.addEventListener('click', function () {
alert('Hello WebComponent');
});
const shadow = this.attachShadow({ mode: 'closed' });
shadow.appendChild(content);
}
}
Shadow DOM
Shadow DOM 允许将隐藏的 DOM 树附加到常规的 DOM 树中 ---- 它以 shadow root 节点为起始根节点,在这个根节点的下方,可以是任意元素,和普通的DOM元素一样。
把本来 DOM 树中的一部分封装并隐藏起来,隐藏起来的树概念为 Shadow Tree。把它理解成 DOM 上一棵特殊的子树,称之为 shadow tree 或影子树。它是一种特殊的树,树里面也是 DOM,就像我们上面用document.createElement
创建的 DOM 一样。
- 影子宿主(Shadow host) : 影子 DOM 附加到的常规 DOM 节点。
- 影子树(Shadow tree) : 影子 DOM 内部的 DOM 树。
- 影子边界(Shadow boundary) : 影子 DOM 终止,常规 DOM 开始的地方。
- 影子根(Shadow root) : 影子树的根节点。
在目前的自定义元素中,里面的结构已经变成了 Shadow DOM,顺带说下 attachShadow
中的 mode 参数有两种值"open"、"closed";
html
<custom-component id="component-id"></custom-component>
- open: 表示可以通过页面内的 JavaScript 方法来获取 Shadow DOM,如使用 Element.shadowRoot 属性;
js
const myShadowDom = document.getElementById('component-id');
const shadowRoot = myShadowDom.shadowRoot; // shadow root
- closed: 不可以从外部获取 shadow DOM ,Element.shadowRoot 将会返回 null
js
const myShadowDom = document.getElementById('component-id');
const shadowRoot = myShadowDom.shadowRoot; // null
Shadow DOM 的概念在 HTML 中非常常见,例如 HTML 中内置的 <video>
标签,使用该标签渲染一个视频时,会发现页面中除了会呈现一个完整的播放器之外,里面还有播放视频的进度条、播放按钮、音量调节等。
明明只有一个标签,为什么内部有如此丰富的内容呢?
html
<video
src="http://maoyan.meituan.net/movie/videos/854x4804c109134879943f4b24387adc040504b.mp4"
controls
width="500"
>
</video>
这时候需要打开控制台的【设置】
,勾选上【显示用户代理Shadow DOM】
:
然后就可以看到隐藏在 <video>
标签中的shadow root了,其播放控件正是隐藏在 shadow root 内部
因此像 img、button、input、textarea、select、 radio、 checkbox,video 等等这些标签是不可以作为宿主的,因为它们本身内部就已经有shadow DOM了。也就是说上述的替换元素不可以作为根节点,即使强行往标签中插入dom 结构,它也会挂载到 body 中。
html
<input type="text">123</input>
Exparser框架原理
说到这里,开发过小程序的jym是不是觉得很熟悉,在小程序自定义一个组件或者使用某些内置组件时,结构是不是和使用 WebComponent
技术自定义的元素高度相似。
没错,这正是微信小程序中的 Exparser
设计原理,Exparser
是微信小程序的组件组织框架,内置在小程序基础库中,为小程序提供各种各样的组件支撑。内置组件和自定义组件都有 Exparser
组织管理。
Exparser
参考: developers.weixin.qq.com/ebook?actio...