前言
自从各大框架百花齐放以来,各大框架的各种组件也是让大家应接不暇。比如element-ui,你会发现针对不同框架又不同版本,再比如公司内部开发一个vue组件库,有一天突然说大家要开始把技术栈调整为react,相应的组件库也要调整,这必然会导致耗费人力时间,开发大量重复的代码
Web Components诞生
Web Components 是一套不同的技术,允许您创建可重用的定制元素(它们的功能封装在您的代码之外)并且在您的web 应用中使用它们。
相比第三方框架,原生组件简单直接,符合直觉,不用加载任何外部模块,代码量小。目前,它还在不断发展,但已经可用于生产环境。
我们熟知html元素,div,p,ul,复杂的比如video,audio等等,那么是不是可以允许用户自己定义元素呢
比如商品页卡,自定义一个sku-card元素 <sku-card img="xxx" name="xxx" price="xxx"></sku-card>,就可以直接渲染成下面这样,是不是很神奇
Web Components 的组成
Web Components不是单一的规范,而是一系列的技术组成,包括
- Custom Element 自定义元素
- Shadow DOM 影子DOM
- HTML templates HTML模板
接下来我们就挨个看看不同技术的实现
Custom Element
Custom Elements 是网页组件化的基础,也是其核心。主要分为两类
- 独立元素,它不继承其他内建的 HTML 元素,直接以HTML标签形式在页面使用,比如<sku-card></sku-card>
- 继承元素,在创建时继承自其他HTML元素,比如<div is="sku-card"></div>
在开发时选择哪种创建方式自己选择
CustomElementRegistry对象
创建自定义元素需要用到CustomElementRegistry对象,要获取它的实例,使用 window.customElements
属性,主要作用就是
- 给页面注册自定义标签
- 获取已注册自定义标签元素的相关信息
其中我们只需要着重看一下CustomElementRegistry.define()这个方法就好了,具体使用如下
js
customElements.define(name, constructor, options);
参数解析:
- name自定义标签名。注意:
它不能是单个单词,且其中必须要是lisp-case连接符式命名法
,比如'sku-card',否则会抛出异常的 - constructor 自定义元素构造器,它可以控制元素的表现形式、行为和生命周期等
- options 一个包含
extends
属性的配置对象,是可选参数。它指定了所创建的元素继承自哪个内置元素,可以继承任何内置元素。
下面看一下两种元素的创建方式
独立元素
js
class SkuCard extends HTMLElement {
constructor() {
super()
console.log(this)
this.addEventListener('click', () => {
console.log('我被点击了')
})
// 都会报错
// this.setAttribute('aaaa', 3333)
// this.innerHTML = '<div slot="aaa">我是slot</div>'
}
}
window.customElements.define('sku-card', SkuCard)
// 页面中使用就是<sku-card></sku-card>
其实你测试会发现,即便你不在js中定义这个自定义元素,仍然可以直接在dom中使用,并且使用document.getElementsByTagName('xxx')
也会返回相应的自定义元素相应信息,这是因为浏览器在解析的时候自动做了处理
这里需要需要注意以下几点
- 创建独立元素必须继承自HTMLElement,否则会报错
- 使用了继承super函数不能丢
- 打印一下this,你会得到自定义组件的dom元素,但是这个元素不能被填充子元素,甚至不能设置属性,否则就会报错,但是可以创建shadow dom这个后面会讲
可以添加事件绑定
继承元素
使用方式会有区别
js
class SkuCard1 extends HTMLDivElement {
constructor() {
super()
}
}
window.customElements.define('sku-card1', SkuCard1, {extends: 'div'})
// 页面中使用<div is="sku-card1"></div>
生命周期
我们在使用前端框架页面或者组件都是有生命周期的,同样的Custom Elements也有自己的生命周期,在生命周期中执行相应的代码逻辑,可以使我们的组件变的异常强大
- ConnectedCallback:当 Custom Elements 首次被插入文档 DOM 时,这个回调函数会被调用
- DisconnectedCallback:当 Custom Elements 从文档 DOM 中删除时,这个回调函数会被调用。
- AdoptedCallback:当 Custom Elements 被移动到新的文档时,这个回调函数会被调用。
- AttributeChangedCallback:当 Custom Elements 增加、删除、修改自身属性时,这个回调函数会被调用。
我们来看一个例子,模拟一下组件的props,改变Custom Element的value属性,在Custom Element内部监听,然后做出一些响应
这里一定要注意监听的值要使用observedAttributes显示的声明出来
html
<input class="test-input" id="testVal" oninput="testValChange()"/>
<sku-card value="1">
</sku-card>
js
function testValChange () {
let testVal = document.getElementById('testVal')
// console.log(testVal.value)
let el = document.getElementsByTagName('sku-card')
el[0].setAttribute('value', testVal.value)
}
class SkuCard extends HTMLElement {
constructor() {
super()
this.shadow = this.attachShadow({
mode: 'open'
// mode: 'closed'
})
let child = document.createElement('div')
child.innerHTML = `<div>我是原始dom</div>`
this.shadow.appendChild(child)
}
static get observedAttributes() {
return ['value'];
}
connectedCallback() {
console.log("挂载到页面");
}
disconnectedCallback() {
console.log("custom-square 从页面被移除");
}
attributeChangedCallback(name, oldValue, newValue) {
if (newValue!= oldValue) {
console.log("属性值被改变");
let newChild = document.createElement('div')
newChild.innerHTML = `<div>我是新增的dom,我的值是${newValue}</div>`
this.shadow.appendChild(newChild)
}
}
}
window.customElements.define('sku-card', SkuCard)
shadow DOM
来看一件比较神奇的事情,在我们的认识里,html元素好像都是一些内置的基础元素
按以下步骤打开设置
这时我们再来看看我们的原生html元素,这里其实就是shadom-dom,一个video竟然可以这么复杂
Shadow DOM 能在 Web Components 体系中占据重要的地位
,是因为其具有良好的密封性,主要表现在:
- 隐藏标记、样式和行为
- 保持代码隔离,保证页面的干净整洁,各组件内部代码互不影响
- 隐藏实现细节,便于使用更强大的语法或功能
这就意味着,使用了Shadow DOM,内部不管写了什么,都不会影响页面其他部分。这也是实现css沙箱的一种方案
Shadow DOM 结构
Shadow DOM 允许将隐藏的 DOM 树附加到常规的 DOM 树中,它以 Shadow root 节点为起始根节点,在这个根节点的下方,可以是任意元素,和普通的 DOM 元素一样
具体使用
js
class SkuCard extends HTMLElement {
constructor() {
super()
let shadow = this.attachShadow({
mode: 'open'
// mode: 'closed'
})
// 像操作其他普通dom一样操作shadow元素
let child = document.createElement('div')
child.innerHTML = '我是shadow DOM 中的子元素'
shadow.appendChild(child)
}
}
window.customElements.define('sku-card', SkuCard)
语法
js
let shadow = this.attachShadow({
mode: 'open'
// mode: 'closed'
})
mode参数需要注意一下
- open 这是默认值。当模式为 "open" 时,任何人都可以访问到这个 Shadow DOM,并对其进行读取和修改。
- closed 当模式为 "closed" 时,只有创建了这个 Shadow DOM 的代码可以访问它。其他代码无法读取或修改这个 Shadow DOM 的内容。
使用 mode: "closed"
可以提供额外的封装性,确保组件的内部结构不被外部代码意外或恶意地修改。但是,这也意味着当你试图调试或检查元素的 Shadow DOM 时,可能会遇到一些限制,因为开发工具可能无法访问到这些封闭的 Shadow DOM。
HTML templates
这个其实是template模板+slot共同作用
template
template元素是一种容器,用于包含一组 HTML 内容,这些内容将作为模板供以后使用,元素在页面加载时不会显示
,它主要用于在 JavaScript 中动态创建和插入 HTML 内容。
slot
学过vue的同学肯定都了解slot插槽的用法,通过插槽可以让组件更灵活,Web Components技术套件中的slot其实和vue中的作用差不多,我们直接来看看相关用法吧,这里就不细讲slot的语法了,也有默认插槽和具名插槽的区别
html
<template id="my-component">
<div>
<!-- 具名插槽 -->
<slot name="header"></slot>
<p>This is the main content.</p>
<slot name="footer"></slot>
<!-- 默认插槽 -->
<slot></slot>
</div>
</template>
<my-component>
<h1 slot="header">My Component</h1>
<!-- 会放在 -->
<p>This is the content for the main slot.</p>
<p slot="footer">This is the footer content.</p>
<p>This is another content for the main slot.</p>
</my-component>
js
class MyComponent extends HTMLElement {
constructor() {
super()
const template = document.getElementById('my-component')
const content = template.content
let shadow = this.attachShadow({
mode: 'open'
// mode: 'closed'
})
shadow.appendChild(content.cloneNode(true))
}
}
window.customElements.define('my-component', MyComponent)
实战
完成sku-card页卡
html
<template id="sku">
<style>
.sku {
width: 160px;
overflow: hidden;
}
.img-wraper {
background-color: #fff;
width: 160px;
height: 160px;
position: relative;
}
.img-wraper .img {
width: 100%;
height: 100%;
}
.sku-name {
margin-bottom: 12px;
color: #000000;
text-align: left;
max-height: 38px;
font-size: 14px;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.sku-price {
display: flex;
align-items: baseline;
line-height: 24px;
color: #FF450C;
font-weight: bold;
font-size: 16px;
margin-top: 12px;
}
</style>
<div class="sku">
<div class="img-wraper">
<slot class="img" name="skuImg"></slot>
</div>
<p class="sku-name">
<slot name="skuName"></slot>
</p>
<p class="sku-price">¥<slot name="skuPrice"></slot></p>
</div>
</template>
<sku-card>
<img class="img" slot="skuImg" src="https://wanshunfu-1301582899.cos.ap-guangzhou.myqcloud.com/null/9b496c274eacdd0606e79714d08d8c1d.png"/>
<span slot="skuName">国产新疆羊肉有机乳羔羊排酸羊肉礼盒(只发广东) 有机羔羊 1kg /(羊排500g/羊脊椎500g/羊腱子500g/羊脖子500g/羊后腿500g 任意选两种 </span>
<span slot="skuPrice">1111</span>
</sku-card>
js
class SkuCard extends HTMLElement {
constructor() {
super()
const template = document.getElementById('sku')
const content = template.content
let shadow = this.attachShadow({
mode: 'open'
// mode: 'closed'
})
shadow.appendChild(content.cloneNode(true))
// 组件内定义样式
// let styleEle = document.createElement("style");
// styleEle.textContent = `
// :host .img-wraper {
// width: 100%;
// height: 100%;
// }
// `;
// shadow.appendChild(styleEle);
}
}
window.customElements.define('sku-card', SkuCard)