简单例子
// html
<c-button>
click me
</c-button>
// js
// 定义元素类
class CButton extends HTMLElement{
constructor(){
super()
this._root = this.attachShadow({ mode: 'closed' })
this.tmp = document.createElement('template')
this.tmp.innerHTML = `
<style>
.c-button{ border: 0; background: #fff; color: orange }
</style>
<button class='c-button'>
<slot />
</button>
`
this._root.appendChild(this.tmmp.content.cloneNode(true))
}
}
// 注册元素
window.customElements.define('c-button', CButton)
定义
Web Components 是一套不同的技术,允许您创建可重用的定制元素(它们的功能封装在您的代码之外)并且在您的web应用中使用它们。
web components 就是一套提供自定义元素的的技术
组成
- Custom elements(自定义元素) 提供自定义元素内容及行为的能力
- Shadow DOM(影子DOM)提供元素封装性
- HTML templates(HTML模板)提供使用html标签编写自定元素的内容结构
定义元素类
自定义元素通过API window.customElements.define(name, constructor, options)
注册到当前文档中,
该接口主要接收参数:
-
name
元素标签名, 例如: 'c-title' , 使用时: <c-title> </c-title> -
constructor
元素构造函数,需要继承 HTMLElement 类 -
options
配置属性
所以我们需要通过类的方式定义自定义元素
class CustomElement extends HTMLElement {
constructor(){
....
}
}
// 注册
window.customElements.define('', CustomElement)
html 模板
HTML templates 提供了两个新的元素标签
-
<template>
模板容器 -
<slot>
内容插槽
自定元素更接近于我们平常定义的UI组件,将具有一定功能的 html, css, js 封装在元素标签内。所以编写的流程也大致相同。
首先我们先定义内容的基础结构
const root = this.attachShadow({ mode: 'closed' })
const tmp = document.querySelector('template')
tmp.innerHTML = `
// 内容样式
<style>
.c-card{
width: 400px;
height: 300px;
padding: 8px 10px;
background: #fff;
border: 1px solid #eee;
}
</style>
// 内容结构
<div class='c-card'>
<slot />
</div>
`
// 挂载内容
root.appendChild(tmp.content)
除了使用模板字符串,还可以使用 html 模板或 document.createElement 构建内容结构
- html 模板
// html
<template id='tmp'>
<div class='c-card'>
<slot />
</div>
</template>
// js
class Card extends HTMLElement{
construction(){
...
const tmp = document.querySelector('#tmp')
root.appendChild(tmp.content)
}
}
// <template> 内容不会直接显示在html文档中
- createElement
class Card extends HTMLElement{
construction(){
...
const tmp = document.createElement('template')
const slot = document.createElement('slot‘)
card.classList.add('c-card')
card.appendChild(slot)
tmp.addpendChild(card)
root.appendChild(tmp.content)
}
}
获取/自定义属性
- 字段属性
// html
<c-card follow='10'> // 在模板中设置属性值
</c-card>
// js
{
constructor(){
...
const card = document.createElement('div')
// 通过 getAttribute 查询元素上是否设置自定义属性值
const follow = this.getAttribute('title') || 0
console.log('follow: ', follow, typeof follow) // 值类型未字符串
card.innerHTML = follow
}
}
// 使用js设置属性值
const card = document.querySelector('c-card')
card.setAttribute('follow', 20)
这里有几个问题: 1. 通过
getAttribute
获取的值,类型都是字符串。2. 后续修改属性时,没有响应式的修改属性内容, 如果需要响应属性修改,需要配置标签的周期钩子。
- getter / setter
{
constructor(){
this._value = 0
}
// 除了通过 getAttribute 获取属性外,
// 也可以通过 getter, setter 定义属性
get value(){
return this._value
}
set value(v){
this._value = v
console.log('value type: ',typeof v)
}
}
// 修改属性值
const card = document.querySelector('c-card')
card.value = 20
// value type: number
使用 getter , setter 后,可以通过直接赋值的方式设置属性值。 并且获取的值为原对象值类型,而非字符串
属性响应
上面修改非属性值时,我们无法响应属性的修改。要实现对属性的响应,需要依赖 attributeChangedCallback
observedAttributes
两个属性
{
// 属性响应函数, 属性修改后将触发该函数, 类似 vue 的watch函数
attributeChangedCallback(name, oldValue, newValue){
console.log(`属性名称: ${name}`)
console.log(`旧属性值: ${oldValue}`)
console.log(`新属性值: ${newValue}`)
}
// 并不是所有的属性都触发 attributeChangedCallback,只有在 observedAttributes 内注册的属性,才能触发回调
// 注册触发响应的属性名称
static observedAttributes = ['value']
// observedAttributes 也可以为 getter, setter
}
生命周期钩子
自定义元素内可以配置相应的周期钩子,以执行不同任务.
- connectedCallback 插入时
- disconnectedCallback 删除时
- adoptedCallback 移动时
- attributeChangedCallback 属性修改时
自定义事件
{
constructor(){
const card = document.createElement('div')
card.addEventListener('click', () => {
// 创建自定义事件
const event = new CustomEvent('cardClick', {detail: 'from card'})
// 抛出事件
this.dispatchEvent(event)
})
}
}
// 挂载事件监听
cont card = document.querySelector('c-card')
card.addEventListener('cardClick', ({detail}) => console.log(detail))
通信
在了解了自定义属性和自定事件后, 基本就满足了元素内与元素外通信的条件了。
模式类似vue的父子通信, 通过自定义属性获取外部值, 通过自定义事件向外抛出数据
{
constructor(){
this._value = 0
this._root = this.attachShadow({ mode: 'closed' })
this._rootElement = document.createElement('template')
this._rootElement.innerHTML = `
<div>
<button> add </button>
</div>
`
this._rootElement.content.querySelector('button')
.addEventListener('click', () => {
const event = new CustomEvent('add', {detail: this._value + 1})
this.dispatchEvent(event)
})
}
render(){
const ele = document.createElement('div')
ele.innerHTML = this._value
this._root.appendChild(ele)
}
get value(){ return this._value }
set value(n){ this._value = n; this.render() }
}
const ele = document.querySelect('element')
// 设置初始值
ele.value = 10
// 添加响应回调
ele.addEventListener('add', ({detail}) => {
ele.value = detail
})
插槽
使用<slot />
可以为标签添加嵌套功能, 与 vue slot 类似。
{
constructor(){
const tmp = `
<div>
<div>
默认插槽: <slot />
</div>
<div>
具名插槽: <slot name='sub' />
</div>
</div>
`
...
}
}
// html
<element>
<div name='sub'> 附属信息 </div>
<div> 主内容 </div>
</element>
隐藏 / 开放
shadow DOM 提供了隐藏元素实现的能力。外部将不能影响或获取到内部元素, 通过设置mode
类型开启 or 关闭。
{
constructor(){
// mode 的值可以为 'open' | 'closed'
this._root = this.attachShadow({ mode: 'closed' }) // 禁止外部访问内部节点
}
}
总结
web compoents 可以看作一种官方的组件化方案, 在不依赖其他MVVM框架或编译器的情况下,实现通用性组件。
在开发中,现有的API都比较简略,实际应用依然需要更上层的封装或工程化依赖做辅助。
网友评论