一、Web Components的概念
Web Components是网页组件,一次创建多处使用
二、Web Components 的组成
- Custom elements(自定义元素):一组js API,允许定义
Custom elements及其行为,按需使用 - Shadow DOM(影子DOM):一组js API,将封装的影子DOM添加到主文档中,并且和主文档分开呈现,保持影子DOM中元素的功能私有,不与主文档中其他部分发生冲突
- HTML templates(HTML模板):
<templete>和<slot>
三、兼容性
四、Custom Elements
Web Components有个很重要的特性,开发者能够将页面功能封装成Custom Elements
4.1 Custom Elements的分类:
根据是否继承HTML元素,将Custom Elements分为两类
Autonomous custom elements
独立的元素,不继承其他内建的HTML元素
<my-card>card</my-card>
const card = document.createElement('my-card')
card.innerHTML = '我的卡片'
document.body.appendChild(card)
Customized built-in elements
继承自基本的HTML元素,创建时必须依赖现有的HTML元素,通过is属性指定Custom Element的名称
<p is="my-card">块级标签</p>
document.createElement('p', { is: 'my-card' })
4.2 CustomElementRegistry 对象
CustomElementRegistry 对象用来注册和操作自定义标签,通过window.customElements获取CustomElementRegistry 对象
CustomElementRegistry.define
用来创建一个自定义标签,语法如下
customElements.define(name, constructor, options)
参数:
- name:自定义标签名。注意:它不能是单个单词,且其中必须要有短横线,例如
my-card - contructor:自定义元素构造器,控制元素的表现形式、行为和生命周期等
- options:一个包含
extends属性的配置对象,可选,指定所创建的元素继承自哪个内置元素,可以继承任何内置元素
返回值:undefined
class MyCard extends HTMLParagraphElement {
constructor() {
super()
}
}
customElements.define('my-card', MyCard, { extends: 'p' })
CustomElementRegistry.get
返回自定义标签的构造函数
const constructor = customElements.get('my-card')
CustomElementRegistry.getName
CustomElementRegistry.upgrade
更新root子树中所有包含影子DOM的自定义元素,语法:customElements.upgrade(root)
不调用upgrade方法:
const el = document.createElement('my-card')
class MyCard extends HTMLElement {}
customElements.define('my-card', MyCard)
console.log(el instanceof MyCard) // false
调用upgrade方法:
const el = document.createElement('my-card')
class MyCard extends HTMLElement {}
customElements.define('my-card', MyCard)
customElements.upgrade(el)
console.log(el instanceof MyCard) // true
CustomElementRegistry.whenDefined
如果自定义元素还未定义,返回定义自定义元素时将会执行的promise; 如果自定义元素已定义,那么立即执行返回的promise
class MyCard extends HTMLParagraphElement {
constructor() {
super()
}
}
customElements.whenDefined('my-card').then(() => {
console.log(`my-card 被注册`)
})
console.log('my-card 注册前')
customElements.define('my-card', MyCard, { extends: 'p' })
console.log('my-card 注册后')
// my-card 注册前
// my-card 注册后
// my-card 被注册
当define执行后,再次执行以下代码,则会立即执行resolve方法
customElements.whenDefined('my-card').then(res => {
console.log(res)
console.log(`my-card 被注册`)
})
4.3 创建Custom Elements
创建和使用Autonomous custom elements
class MyCard extends HTMLElement {
constructor() {
super()
let shadow = this.attachShadow({ mode: 'open' })
let containerEle = document.createElement('div')
containerEle.style.display = 'flex'
containerEle.style.flexDirection = 'column'
containerEle.style.margin = '100px'
containerEle.style.border = '1px solid #aaa'
const headerEle = document.createElement('div')
headerEle.innerText = '名片'
headerEle.style.padding = '10px'
headerEle.style.borderBottom = '1px solid blue'
const nameEle = document.createElement('div')
nameEle.innerText = '姓名:都敏俊'
nameEle.style.padding = '10px'
const sexEle = document.createElement('div')
sexEle.innerText = '性别:男神'
sexEle.style.padding = '10px'
containerEle.appendChild(headerEle)
containerEle.appendChild(nameEle)
containerEle.appendChild(sexEle)
shadow.appendChild(containerEle)
}
}
customElements.define('my-card', MyCard)
使用:
<my-card />
效果:
创建和使用Customized built-in elements
class MyCard extends HTMLDivElement {
constructor() {
super()
let shadow = this.attachShadow({ mode: 'open' })
let containerEle = document.createElement('div')
containerEle.style.display = 'flex'
containerEle.style.flexDirection = 'column'
containerEle.style.margin = '100px'
containerEle.style.border = '1px solid #aaa'
const headerEle = document.createElement('div')
headerEle.innerText = '名片'
headerEle.style.padding = '10px'
headerEle.style.borderBottom = '1px solid blue'
const nameEle = document.createElement('div')
nameEle.innerText = '姓名:都敏俊'
nameEle.style.padding = '10px'
const sexEle = document.createElement('div')
sexEle.innerText = '性别:男神'
sexEle.style.padding = '10px'
containerEle.appendChild(headerEle)
containerEle.appendChild(nameEle)
containerEle.appendChild(sexEle)
shadow.appendChild(containerEle)
}
}
customElements.define('my-card', MyCard, { extends: 'div' })
使用:
<div is="my-card"></div>
效果:
总结
| 自定义标签类型 | 继承自 | define方法是否需要第三个参数 | 使用 |
|---|---|---|---|
| Autonomous custom elements | 只能继承HTMLElement | 不需要 | 直接使用自定义标签名作为标签名,如<my-card /> |
| Customized built-in elements | 继承可用的基本HTML标签类,如HTMLDivElement | 必须要传入第三个参数,一般为{ extends: '标签名' } | 组件构造函数类继承类的基本标签名+is='自定义标签名',如<div is="my-card"></div> |
五、Shadow DOM
在初涉前端时,就好奇像<input />、<select></select>、<audio></audio>、<video></video>这些标签,用起来很简单,但是在页面上的布局和功能却很丰富,当时给自己的解释是:这些标签都是系统控制渲染的
现在再想想,这些解释有些道理,但是没有说到根本原因,前端老手的解释应该是:这些元素都是以组件的形式存在,所展现出来的布局和功能都是在组件内部定义好的,使用该组件时,内部的布局会渲染在页面上。
既然说input是组件,那为什么开发者工具中看到的input只是一个普通标签
如果想要看到各个组件内部的DOM结构,操作步骤:
5.1 Shadow DOM的概念
Shadow DOM是HTML的一个规范,允许开发者封装自己的HTML标签、css样式和特定的js代码,创建类似video这样的自定义标签
5.2 Shadow DOM的结构
以shadow-root为起始根节点,在这个根节点的下方,可以是任意元素,和普通的DOM元素一样
5.3 Shadow DOM术语
- Shadow host:一个常规DOM节点,shadow DOM会附加到这个节点上
- Shadow tree:shadow DOM内部的DOM树
- Shadow boundary:shadow DOM的分界线
- Shadow root:shadow tree的根节点
5.4 用法
使用Element.attachShadow()方法给指定的元素挂载一个shadow DOM,并且返回shadowRoot的引用
attachShadow的参数:
- 是一个对象,并且必须指定mode属性,值为open或者closed,open时会返回
shadowRoot的引用,closed返回null - delegatesFocus,默认为false,可以设置为true,自动聚焦
<style>
.content {
color: blue;
text-decoration: underline;
}
</style>
<shadow-display>
<div style="background-color: #f0f0f0">插槽</div>
</shadow-display>
<hr />
<shadow-display>
<div style="background-color: red">插槽</div>
</shadow-display>
<hr />
<script>
class ShadowDisplay extends HTMLElement {
constructor() {
super()
const shadow = this.attachShadow({ mode: 'open', delegatesFocus: true })
shadow.innerHTML = `
<style>
:host { display: block; all: initial; }
.content { font-size: 20px; color: pink; }
</style>
<div class="content">
<p>这是Shadow DOM中的内容,不受外部样式影响</p>
<input type="text" placeholder="请输入内容" />
<slot></slot>
</div>
`
}
}
customElements.define('shadow-display', ShadowDisplay)
const element = document.querySelector('shadow-display')
const shadowRoot = element.shadowRoot
const contentElement = shadowRoot.querySelector('.content')
contentElement.style.color = 'green'
console.log(element.shadowRoot)
5.5 shadow host的css选择器
5.5.1 :host伪类选择器就是自定义标签元素,只在shadowDOM中有效
<shadow-display> </shadow-display>
<script>
class ShadowDisplay extends HTMLElement {
constructor() {
super()
const shadow = this.attachShadow({ mode: 'open' })
shadow.innerHTML = `
<style>
:host { display: block; width: 200px; height: 200px; border: 3px solid black; }
:host .content { font-size: 22px; color: pink; }
</style>
<div class="content">
<p>这是Shadow DOM中的内容,不受外部样式影响</p>
</div>
`
}
}
customElements.define('shadow-display', ShadowDisplay)
</script>
此例中的:host就是shadow-display元素
5.5.2 :host()伪类函数,选择包含特定选择器的自定义元素
:host可以替换为:host(.custom-shadow),要求自定义元素必须设置custom-shadow类名
5.5.3 :host-context()伪类函数,选择有特定选择器父元素的自定义元素
:host-context()找到自定义元素,并且该元素的父级包含特定的选择器
<div id="container">
<shadow-display id="custom-shadow"> </shadow-display>
</div>
<shadow-display id="custom-shadow"> </shadow-display>
<script>
class ShadowDisplay extends HTMLElement {
constructor() {
super()
const shadow = this.attachShadow({ mode: 'open' })
shadow.innerHTML = `
<style>
:host-context(#container) { display: block; width: 200px; height: 200px; border: 3px solid black; }
:host .content { font-size: 22px; color: pink; }
</style>
<div class="content">
<p>这是Shadow DOM中的内容,不受外部样式影响</p>
</div>
`
}
}
customElements.define('shadow-display', ShadowDisplay)
</script>
六、HTML templates
6.1 templates的概念
templates是html页面中使用的一组元素标签,即<template></template>,在解析的过程中会被处理,不会显示在页面上,可以被js获取到
6.2 简单使用
<p>我是p标签</p>
<template>我是template标签</template>
使用js可以获取到template,并渲染到页面上
const tem = document.querySelector('template')
document.body.appendChild(tem.content)
七、slots
<template id="user-intro-template">
<div class="header">自我介绍</div>
<div class="details">我的名字是 <slot name="userName">都敏俊希</slot>。</div>
</template>
<user-intro-template></user-intro-template>
<user-intro-template>
<span slot="userName">张三</span>
</user-intro-template>
<user-intro-template>
<span slot="userName">李四</span>
</user-intro-template>
<script>
class UserIntroTemplate extends HTMLElement {
constructor() {
super()
const template = document.getElementById('user-intro-template')
const templateContent = template.content
this.attachShadow({ mode: 'open' }).appendChild(templateContent.cloneNode(true))
}
}
customElements.define('user-intro-template', UserIntroTemplate)
</script>
通过这个例子,可以得知:slots是给模板元素传值,增强可扩展性
7.1 slot的name属性
带有name的slot标签就是“具名插槽”,在使用插槽的标签上使用slot="age"指定具体的插槽
<template id="user-intro-template">
<div class="header">自我介绍</div>
<div class="details">我的名字是 <slot name="userName">都敏俊希</slot>。</div>
<div class="details">我的年龄是 <slot name="age">20</slot>。</div>
</template>
<user-intro-template>
<span slot="userName">张三</span>
<span slot="age">19</span>
</user-intro-template>
和vue插槽的相比,这些是一样的:
- 匿名插槽和具名插槽
- 没有传值的情况下,会使用预设内容
上面的例子是在shadowDOM中使用slots的,如果在正常的文档流中使用,那么不具有插槽的效果。上面的例子中,slot标签是定义在template中的,其实使用div也是可以的,但template的好处是它不渲染在页面上
八、demo:使用Web Component写一个身份证组件
<template id="IdCardWrapper">
<style>
:host {
display: inline-block;
width: 400px;
height: 240px;
border: 1px solid black;
border-radius: 10px;
}
.row {
margin-top: 20px;
margin-left: 20px;
display: flex;
align-items: center;
}
.row .label {
font-size: 12px;
color: blue;
margin-right: 10px;
}
</style>
<div class="row">
<div class="label">姓名</div>
<div class="value"><slot name="userName">都敏俊希</slot></div>
</div>
<div class="row">
<div class="label">性别</div>
<div class="value"><slot name="gender">男</slot></div>
</div>
<div class="row">
<div class="label">出生</div>
<div class="value"><slot name="birth">2000年1月1日</slot></div>
</div>
<div class="row">
<div class="label">住址</div>
<div class="value"><slot name="address">xx省xx市</slot></div>
</div>
<div class="row">
<div class="label">公民身份号码</div>
<div class="value"><slot name="idNumber">123456789123456789</slot></div>
</div>
</template>
<id-card>
<span slot="userName">千颂伊</span>
<span slot="gender">女</span>
<span slot="birth">2005年1月1日</span>
<span slot="address">安徽省合肥市</span>
<span slot="idNumber">34260120050101001X</span>
</id-card>
<id-card></id-card>
<script>
class IdCard extends HTMLElement {
constructor() {
super()
this.shadow = this.attachShadow({ mode: "open" })
let tempEle = document.getElementById("IdCardWrapper")
this.shadow.appendChild(document.importNode(tempEle.content, true))
}
}
customElements.define("id-card", IdCard)
</script>