碎片化学习前端之HTML(webComponent)
前言
webComponent 是 HTML5 推出的新特性,为组件化推广奠定基础。
webComponent 基本使用
原生组件,性能较好,但存在兼容性问题。其核心技术有:Custom elements, Shadow DOM, HTML Templates。
Custom elements
JavaScript API,用于定义 custom elements 及其行为。
<m-button type="primary">webComponent</m-button> // 调用
<script type="text/javascript">
class MButton extends HTMLElement {
constructor() {
super();
}
}
window.customElements.define('m-button', MButton) // 关联 HTML 和 JS,自定义组件
</script>
HTML Templates
<template>
, <slot>
元素标记元素结构,并重用元素。
<m-button type="primary">webComponent</m-button>
<template id="m-btn">
<button class="m-button">
<slot>Default</slot>
</button>
</template>
<script type="text/javascript">
class MButton extends HTMLElement {
constructor() {
super();
let btnTmpl = document.getElementById('m-btn') // 定义模板并获取模板
}
}
</script>
Shadow DOM
JavaScript API,用于封装 Shadow DOM 附加到元素上,并关联其功能,以此保证元素的功能私有,不会与其他元素冲突。
<m-button type="primary">webComponent</m-button>
<template id="m-btn">
<button class="m-button">
<slot>Default</slot>
</button>
</template>
<script type="text/javascript">
class MButton extends HTMLElement {
constructor() {
super();
let btnTmpl = document.getElementById('m-btn') // 定义模板并获取模板
let shadow = this.attachShadow({ mode: 'open' }) // 配置 devtools 是否可查看 DOM 结构,open / close
let cBtnTmpl = btnTmpl.content.cloneNode(true) // copy 模板便于重用
shadow.appendChild(cBtnTmpl) // 模板挂载 Shadow DOM
}
}
</script>
webComponent 高阶使用
webComponent 高阶使用主要包括属性设置,样式设置,事件绑定等
webComponent 属性设置
webComponent 在调用组件时进行属性传值,在声明组件时通过 DOM API 获取属性值进行操作。
<m-button type="primary">webComponent</m-button>
<script type="text/javascript">
class MButton extends HTMLElement {
constructor() {
super();
let btnTmpl = document.getElementById('m-btn') // 定义模板并获取模板
let shadow = this.attachShadow({ mode: 'open' })
let cBtnTmpl = btnTmpl.content.cloneNode(true) // copy 模板便于重用
let type = this.getAttribute('type') // 读取属性值,进行操作
shadow.appendChild(cBtnTmpl) // 模板挂载 Shadow DOM
}
}
</script>
webComponent 样式设置
-
template 设置样式
<m-button type="primary">webComponent</m-button> <template id="m-btn"> <style type="text/css"> // 定义模板样式 .m-button { border: none; outline: none; border-radius: 4px; padding: 5px 20px; } </style> <button class="m-button"> <slot>Default</slot> </button> </template> class MButton extends HTMLElement { constructor() { super(); let btnTmpl = document.getElementById('m-btn') // 定义模板并获取模板 let shadow = this.attachShadow({ mode: 'open' }) let cBtnTmpl = btnTmpl.content.cloneNode(true) // copy 模板便于重用 let type = this.getAttribute('type') // 读取属性值,进行操作 shadow.appendChild(cBtnTmpl) // 模板挂载 Shadow DOM } }
-
Shadow DOM 设置样式
<m-button type="primary">webComponent</m-button> <template id="m-btn"> <style type="text/css"> // 定义模板样式 .m-button { border: none; outline: none; border-radius: 4px; padding: 5px 20px; } </style> <button class="m-button"> // 具名插槽使用:模板内<slot name="btn-text"></slot>,模板外调用时<span slot="btn-text"></span> <slot>Default</slot> </button> </template> class MButton extends HTMLElement { constructor() { super(); let btnTmpl = document.getElementById('m-btn') // 定义模板并获取模板 let shadow = this.attachShadow({ mode: 'open' }) let cBtnTmpl = btnTmpl.content.cloneNode(true) // copy 模板便于重用 let type = this.getAttribute('type') // 读取属性值,进行操作 const sheets = { 'primary': { background: '#409EFF', color: '#FFF' }, 'success': { background: '#67C23A', color: '#FFF' }, 'warning': { background: '#E6A23C', color: '#FFF' }, 'danger': { background: '#F56C6C', color: '#FFF' }, 'default': { background: '#909399', color: '#FFF' }, } const style = document.createElement('style') style.textContent = ` .m-button { background: ${sheets[type].background}; // 属性 配置css 样式 color: ${sheets[type].color}; } ` shadow.appendChild(style) shadow.appendChild(cBtnTmpl) // 模板挂载 Shadow DOM } }
注意:webComponent 通过 Shadow DOM 隔离以后,外界无法修改样式,只能通过属性或者设置 CSS 变量的方式修改。
-
css 变量设置样式
<style type="text/css"> :root { --text-color: #fff; } </style> <m-button type="primary">webComponent</m-button> <template id="m-btn"> <style type="text/css"> // 定义模板样式 .m-button { border: none; outline: none; border-radius: 4px; padding: 5px 20px; } </style> <button class="m-button"> <slot>Default</slot> </button> </template> class MButton extends HTMLElement { constructor() { super(); let btnTmpl = document.getElementById('m-btn') // 定义模板并获取模板 let shadow = this.attachShadow({ mode: 'open' }) let cBtnTmpl = btnTmpl.content.cloneNode(true) // copy 模板便于重用 let type = this.getAttribute('type') // 读取属性值,进行操作 const sheets = { 'primary': { background: '#409EFF', color: '#FFF' }, 'success': { background: '#67C23A', color: '#FFF' }, 'warning': { background: '#E6A23C', color: '#FFF' }, danger': { background: '#F56C6C', color: '#FFF' }, 'default': { background: '#909399', color: '#FFF' }, } const style = document.createElement('style') style.textContent = ` .m-button { background: ${sheets[type].background}; // 属性 配置 css 样式 color: var(--text-color, ${sheets[type].color}); // css 变量配置 css 样式 } ` shadow.appendChild(style) // 样式挂载至 Shadow DOM shadow.appendChild(cBtnTmpl) // 模板挂载至 Shadow DOM } }
注意:通过 templates 设置样式的优先级是高于 css 变量和 Shadow DOM 的。
webComponent 事件绑定
<m-button type="primary">webComponent</m-button>
<template id="m-btn">
<button class="m-button">
<slot>Default</slot>
</button>
</template>
<script type="text/javascript">
class MButton extends HTMLElement {
constructor() {
super();
let btnTmpl = document.getElementById('m-btn') // 定义模板并获取模板
let shadow = this.attachShadow({ mode: 'open' }) // 配置 devtools 是否可查看 DOM 结构,open / close
let cBtnTmpl = btnTmpl.content.cloneNode(true) // copy 模板便于重用
cBtnTmpl.querySelector('.m-button').addEventListener('click', this.onClick)
shadow.appendChild(cBtnTmpl) // 模板挂载 Shadow DOM
}
onClick() {
alert('click')
}
}
</script>
webComponent 生命周期
<m-button type="primary">webComponent</m-button>
<template id="m-btn">
<button class="m-button">
<slot>Default</slot>
</button>
</template>
<script type="text/javascript">
class MButton extends HTMLElement {
constructor() {
super();
let btnTmpl = document.getElementById('m-btn') // 定义模板并获取模板
let shadow = this.attachShadow({ mode: 'open' }) // 配置 devtools 是否可查看 DOM 结构,open / close
let cBtnTmpl = btnTmpl.content.cloneNode(true) // copy 模板便于重用
cBtnTmpl.querySelector('.m-button').addEventListener('click', this.onClick)
shadow.appendChild(cBtnTmpl) // 模板挂载 Shadow DOM
}
static get observedAttributes() {
return [ 'type' ] // 监控 type 属性是否改变
}
connectedCallback() {
// 组件首次挂载时调用
}
attributeChangedCallback(key, oldValue, newValue) {
// 组件更新时调用,key 为属性名,oldValue, newValue 为属性值
}
disconnectedCallback() {
// 组件移除时调用
}
}
</script>
Demo 案例
利用 webComponent 简单实现折叠组件,包括父子组件间通信。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>折叠组件 m-collapse</title>
</head>
<body>
<m-collapse>
<m-collapse-item title="Node" name="1">
<div>nodejs</div>
</m-collapse-item>
<m-collapse-item title="React" name="2">
<div>React</div>
</m-collapse-item>
<m-collapse-item title="Vue" name="3">
<div>Vue</div>
</m-collapse-item>
</m-collapse>
<template id="collapse">
<style type="text/css">
/* 为 m-collapse(Shadow DOM 根元素) 设置样式 */
:host {
border: 1px solid #ebebeb;
border-radius: 5px;
}
.m-collapse {
width: 100%;
}
</style>
<div class="m-collapse">
<slot></slot>
</div>
</template>
<template id="collapse-item">
<style type="text/css">
.m-collapse-item {
width: 100%;
}
.m-collapse-item .title {
background: #f1f1f1;
line-height: 35px;
height: 35px;
}
.m-collapse-item .content {
font-size: 14px;
}
</style>
<div class="m-collapse-item">
<div class="title"></div>
<div class="content">
<slot></slot>
</div>
</div>
</template>
<script type="text/javascript">
class MCollapse extends HTMLElement {
constructor() {
super();
let shadow = this.attachShadow({ mode: 'open' })
let tmpl = document.getElementById('collapse')
let cTmpl = tmpl.content.cloneNode(true)
let style = document.createElement('style')
style.textContent = `
.m-collapse {
color: red;
}
`
shadow.appendChild(style)
shadow.appendChild(cTmpl)
// 监控 slot
let slot = shadow.querySelector('slot')
slot.addEventListener('slotchange', event => {
this.slots = event.target.assignedElements(); // 获取所有 slot
this.render()
})
}
static get observedAttributes() {
return [ 'active' ]
}
connectedCallback() {
console.log('组件挂载')
}
attributeChangedCallback(key, oldValue, newValue) {
console.log('组件更新调用')
if(key === 'active') {
this.actives = newValue
this.render()
}
}
disconnectedCallback() {
console.log('组件卸载')
}
render() {
// 父组件给子组件通信
if(this.actives && this.slots) {
([...this.slots]).forEach(slot => {
slot.setAttribute('active', JSON.stringify(this.actives))
})
}
}
}
window.customElements.define('m-collapse', MCollapse)
class MCollapseItem extends HTMLElement {
constructor() {
super();
this.isShow = true // 默认显示 collapse-item
let shadow = this.attachShadow({ mode: 'open' })
let tmpl = document.getElementById('collapse-item')
let cTmpl = tmpl.content.cloneNode(true)
let style = document.createElement('style')
style.textContent = `
.m-collapse-item {
border-bottom: 1px solid #000;
}
`
shadow.appendChild(style)
shadow.appendChild(cTmpl)
this.titleEle = shadow.querySelector('.title')
// 绑定事件
this.titleEle.addEventListener('click', () => {
// 子组件给父组件通信(自定义事件)
document.querySelector('m-collapse').dispatchEvent(new CustomEvent(
'collapseEvent',
{
detail: {
name: this.getAttribute('name'),
isShow: this.isShow
}
}
))
})
}
static get observedAttributes() {
return [ 'active', 'title', 'name' ]
}
attributeChangedCallback(key, oldValue, newValue) {
console.log('item 组件更新调用', key)
switch(key) {
case 'active':
this.actives = JSON.parse(newValue)
break
case 'title':
this.titleEle.innerHTML = newValue
break
case 'name':
this.name = newValue
break
default:
break;
}
if(this.actives && this.name) {
this.isShow = this.actives.includes(this.name)
this.shadowRoot.querySelector('.content').style.display = this.isShow ? 'block' : 'none'
}
}
}
window.customElements.define('m-collapse-item', MCollapseItem)
let actives = [ '1', '2' ] // 默认 1,2 展开
document.querySelector('m-collapse').setAttribute('active', JSON.stringify(actives))
document.querySelector('m-collapse').addEventListener('collapseEvent', (event) => {
if(event.detail.isShow) {
let index = actives.indexOf(event.detail.name)
actives.splice(index, 1)
} else {
actives.push(event.detail.name)
}
document.querySelector('m-collapse').setAttribute('active', JSON.stringify(actives))
})
</script>
</body>
</html>
后记
webComponent 提供了一种组件定义的方式,但其根本仍是借助 DOM API 进行操作,对于原生开发能力有较高的要求。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?