Web Components基础与应用
前言
Web Component 目前包含三项主要技术,这三项技术可以单独或一起使用来创建封装功能的定制元素,可以在任何地方重用,不必担心代码冲突。
1. 初识Web Components
Web Components是一组基于HTML、CSS和JavaScript构建的组件。它们是HTML5中新增的标准之一,可以用于创建可重用的组件,并将它们组合在一起构建Web应用程序。
2. Web Components的主要优势
可重用、可组合性
:Web Components 可以轻松地创建可重用的代码块,这有助于减少重复代码并提高代码的可维护性。灵活性
:Web Components 可以定制 HTML 标签,从而使 HTML 更加灵活和强大。也可以创建自定义的标签、组件和属性,以满足特定的需求。跨平台兼容性
:Web Components 是 HTML5 的一个重要组成部分,因此它们在所有主流浏览器中都是支持的。我们可以轻松地编写跨平台的代码,从而提高代码的通用性和可移植性。组件化和模块化程度高
:通过 Shadow DOM,可以将一个组件的 HTML、CSS 和 JavaScript 封装在一起,形成一个独立的模块。避免了样式冲突和命名空间污染,而不会影响到其他元素;将组件的实现细节隐藏起来,只暴露必要的接口,从而保护了组件的完整性和稳定性。互操作性
:组件可以超越框架并用于不同技术栈的项目中,不需要考虑技术栈版本升级带来的不兼容问题。
3. Web Components包含的三项主要技术
- Custom elements(自定义元素):一组 JavaScript API,可用于定义自定义元素及其行为,然后可以根据需要在用户界面中使用这些元素。
- Shadow DOM(影子 DOM):一组 JavaScript API,用于将封装的“影子” DOM 树附加到元素(与主文档 DOM 分开呈现)并控制其关联的功能。通过这种方式,可以保持元素的功能私有,这样它们就可以被脚本化和样式化,而不用担心与文档的其他部分发生冲突。
- HTML templates(HTML 模板): 使用
<template>
和<slot>
元素可以编写不在呈现页面中显示的标记模板。然后它们可以作为自定义元素结构的基础被多次重用。
实现web component的基本方法通常如下所示:
- 创建一个类或函数来指定web组件的功能,类使用 ECMAScript 2015 的类语法(参阅类获取更多信息)。
- 使用
CustomElementRegistry.define()
方法注册新的自定义元素 ,并向其传递要定义的元素名称、指定元素功能的类、以及可选的其所继承的元素。 - 如果需要的话,使用
Element.attachShadow()
方法将一个shadow DOM附加到自定义元素上。使用通常的DOM方法向shadow DOM中添加子元素、事件监听器等等。 - 如果需要的话,使用
<template>
和<slot>
定义一个HTML模板。再次使用常规DOM方法克隆模板并将其附加到shadow DOM中。 - 在页面任何位置使用自定义元素,就像使用常规HTML元素那样。
4. Web Components 组件的 4 个生命周期函数
准确来说应该称为:生命周期回调函数
connectedCallback:当组件第一次被添加到 DOM 文档后调用
disconnectedCallback:当组件从 DOM 文档移除后调用
adoptedCallback:当组件在 DOM 文档中被移动到其他 DOM 节点时调用
attributeChangedCallback:当组件的某个属性发生改变后调用。
这里的属性改变 包含:新增、移除、修改属性值 这 3 种情况
5. 如何使用Web Components?
要使用Web Components,需要在HTML中嵌入组件元素,并将它们用于构建Web应用程序。以下是一些使用Web Components的示例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Web Components Example</title>
</head>
<body>
<h1>自定义</h1>
<dialog-element></dialog-element>
<script>
// 创建一个类或函数来指定web组件的功能
class Dialog extends HTMLElement {
constructor() {
// 调用父类的属性和方法
super();
const p = document.createElement('p');
p.textContent = 'Web Components Example Custom elements(自定义元素)';
// this表示自定义元素实例。
this.appendChild(p);
}
}
// 用 customElements.define() 方法注册自定义的元素 ,并且指定component名称,以及创建的类。
customElements.define('dialog-element', Dialog);
</script>
</body>
</html>
注意点:
自定义元素的名称,一个 DOMString 标准的字符串,为了防止自定义元素的冲突,必须是一个带短横线连接的名称(e.g. custom-tag)。这个也是 Vue 自定义组件命名推荐的使用方式。
Class 类必须调用 super()。
constructor。自定义元素构造器,包含组件的生命周期的定义。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
<style>
div{
color: #f00;
font-size: 80px
}
</style>
</head>
<body>
<h1>影子 DOM </h1>
<div>test</div>
<host-element>
<span slot="data">哈哈哈</span>
</host-element>
<script>
// 创建一个类或函数来指定web组件的功能
class HostElement extends HTMLElement {
constructor() {
super()
// 将影子 DOM 树附加到指定元素,并返回对其 ShadowRoot 的引用。
// mode是必传的值,代表是否允许外部访问和修改ShadowRoot的属性,
// open表示开放(允许),closed表示关闭(不允许)。
this.attachShadow({
mode: "open"
})
// this.shadowRoot.textContent = "host-element"
this.shadowRoot.innerHTML = `<div>hello world <slot name="data"></slot></div>`
this.addStyle()
}
addStyle() {
const styleEle = document.createElement("style");
// :host伪类,指代自定义元素本身。
styleEle.textContent = `
:host {
font-size:20px;
color:lightblue;
}
div {
border:1px solid blue;
display:inline-block;
padding:20px;
border-radius:8px;
margin-top:20px;
}`
this.shadowRoot.appendChild(styleEle)
}
}
customElements.define("host-element", HostElement)
</script>
</body>
</html>
Shadow DOM 允许将隐藏的 DOM 树附加到常规的 DOM 树中——它以 Shadow root 节点为起始根节点,在这个根节点的下方,可以是任意元素,和普通的 DOM 元素一样:
影子 DOM 术语
影子宿主(Shadow host): 影子 DOM 附加到的常规 DOM 节点。
影子树(Shadow tree): 影子 DOM 内部的 DOM 树。
影子边界(Shadow boundary): 影子 DOM 终止,常规 DOM 开始的地方。
影子根(Shadow root): 影子树的根节点
Reveal box 是一个自定义的 Web 组件(内联块元素)。它用另一个盒子隐藏一个盒子的内容。当用户将鼠标悬停在盒子上,顶部盒子将移开,以便用户可以看到隐藏盒子的内容。
详细链接: Reveal box
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Web Components Example</title>
<style type="text/css">
img{
width: 300px;
margin-top:30px;
}
</style>
</head>
<body>
<h1>使用 Templates</h1>
<template>
<div>
<div>
这是 template 标签内的子节点内容
</div>
<img src="https://semantic-ui.com/images/avatar2/large/kristy.png"/>
</div>
</template>
<script>
// 获取 template 元素
const templateEle = document.querySelector("template");
// 获取 template 元素包含的文档片段
const content = templateEle.content;
// content 可以当做正常的 document 来使用
const node = content.querySelector("div");
// 追加节点到当前文档
// 由于将 Templates 代码片段内部的 div 追加到了当前文档结构,
// 所以 Templates 内部的 div 节点消失
document.body.appendChild(node);
// 避免修改内容模板内部的 DOM 结构,我们可以先克隆模板内部的元素节点,再将克隆的节点追到到当前文档
// 导入 node 到 当前文档
// 必须要有这一步
// const cloneNode = document.importNode(node, true);
// 也可以使用 cloneNode
// const cloneNode = node.cloneNode(true);
// document.body.appendChild(cloneNode);
</script>
</body>
</html>
html 模版是方便于编写自定义元素的html结构和css样式。它包括两个标签:template和slot。这里的slot与vue中的slot类似,用于指定一些占位的插槽,在外边可以用真实的元素替换掉。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Web Components Example</title>
</head>
<body>
<h1>使用 Templates</h1>
<!--在模板中使用 slot 进行占位-->
<template id="cardTmp">
<div class="header">MY CARD</div>
<div class="details">
My name is <slot name="userName">test</slot>。
</div>
</template>
<!--在使用上面模板的自定义元素中给 slot 传值-->
<my-card>
<span slot="userName">插槽传值</slot>
</my-card>
<my-card>
<span slot="userName">web Components</slot>
</my-card>
<my-card>
</my-card>
<script>
class MyCard extends HTMLElement {
constructor () {
super();
const template = document.getElementById('cardTmp');
const templateContent = template.content;
this.attachShadow({mode: 'open'}).appendChild(
templateContent.cloneNode(true)
);
}
}
customElements.define('my-card', MyCard);
</script>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Web Components Template 传参示例</title>
</head>
<body>
<!--相当于自定义一个属性message-->
<hello-world message="好好学习"></hello-world>
<br>
<hello-world message="天天向上"></hello-world>
<template id="hw-template">
<p>Hello World</p>
<style>
p {
padding: 10px;
background-color: #f40;
color: #fff
}
</style>
</template>
</body>
<script>
class HelloWorld extends HTMLElement {
constructor() {
super();
const templateContent = document.querySelector('#hw-template').content
const shadowRoot = this.attachShadow({
mode: 'open'
})shadowRoot.appendChild(templateContent.cloneNode(true))
//获取属性
const message = this.getAttribute('message')
shadowRoot.querySelector('p').innerText = message
}
}
customElements.define('hello-world',HelloWorld)
</script>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Web Components Click Event Example</title>
</head>
<body>
<my-button id="myButton">点击我</my-button>
<script>
class MyButton extends HTMLElement {
constructor() {
super();
this.addEventListener('click', this.handleClick);
}
connectedCallback() {
this.innerHTML = '点击我';
}
handleClick(event) {
console.log('按钮被点击了');
}
}
customElements.define('my-button', MyButton);
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<template id="mybutton">
<style>
button {
width: auto;
height: 30px;
cursor: pointer;
color: blue;
border: 0;
border-radius: 5px;
background-color: #F0F0F0;
}
</style>
<button id="btn">Add</button>
<slot name="my-text">My Default Text</slot>
</template>
<my-button text="hello">
<p slot="my-text">Another Text from outside</p>
</my-button>
<script>
// WebComponent 实例
class MyButton extends HTMLElement {
constructor() {
super();
const template = document.getElementById('mybutton'); // 获取dom节点
const content = template.content.cloneNode(true); // 深度克隆dom节点,使组件可以用在不同的地方并且相互之间不影响
const text = this.getAttribute('text');
const button = content.querySelector('#btn');
this.$button = button; // 把按钮对象挂载到this上
button.innerText = text;
const event = new CustomEvent('onClick', { // 生成新的自定义事件️
detail: 'Hello fom within the Custom Element', // 可选的默认值是 null 的任意类型数据,是一个与 event 相关的值
bubbles: false, // 表示该事件能否冒泡
cancelable: false // 表示该事件是否可以取消
})
button.addEventListener('click', e => {
this.dispatchEvent(event) // 在按钮被点击的时候,触发自定义事件
})
// this.appendChild(content); // 添加元素
this.attachShadow({
mode: 'open'
}).appendChild(content);
}
// 静态get函数,它的作用是定义那些属性需要被监听
static get observedAttributes() {
return ['text'];
}
// 监听text属性变化
get text() {
return this.getAttribute('text');
}
// 赋值text属性
set text(value) {
this.setAttribute('text', value);
}
/**
属性变化时的回调函数,也就是说,每一个被监听的属性,只要属性值发生变化,都会调用这个函数;
*/
attributeChangedCallback(name, oldVal, newVal) {
this.render();
}
// 渲染函数,属性更新后,如果要重新渲染组件,就要调用这个函数。
render() {
this.$button.innerText = this.text;
}
}
window.customElements.define('my-button', MyButton);
// 这俩需要带在调用方的组件里
const mybutton = document.querySelector('my-button')
mybutton.addEventListener('onClick', (value) => {
console.log(value.detail);
mybutton.text = value.detail
});
</script>
</body>
</html>
6. Vue 与 Web Components
默认情况下,Vue 会将任何非原生的 HTML 标签优先当作 Vue 组件处理,而将“渲染一个自定义元素”作为后备选项。这会在开发时导致 Vue 抛出一个“解析组件失败”的警告。要让 Vue 知晓特定元素应该被视为自定义元素并跳过组件解析,我们可以指定 compilerOptions.isCustomElement
// 将所有标签前缀为 `ion-` 的标签视为自定义元素
app.config.compilerOptions.isCustomElement = (tag) => {
return tag.startsWith('ion-')
}
详细链接:Vue 与 Web Components | Vue.js (vuejs.org)
7. 相关扩展
- MicroApp:是一款基于WebComponents的思想实现的高性能低成本微前端框架,提供了
js沙箱
、样式隔离
、元素隔离
、路由隔离
、预加载
、数据通信
等一系列完善的功能,做了诸多兼容,在任何前端框架中都可以正常运行。。 - Lit: 用于使用 lit-html 创建快速、轻量级的 Web Components。
- Omi:腾讯开源的前端跨框架跨平台的框架,是 Web Components + JSX/TSX 融合为一个框架,小巧的尺寸和高性能,融合和 React 和 Web Components 各自的优势。
- Stencil:由 Ionic 团队构建,是一个用于构建可重用、可扩展的设计系统的工具链。生成可在每个浏览器中运行的小型、极快且 100% 基于标准的 Web Components。
- Slim.js:开源的轻量级 Web Components 库,它为组件提供数据绑定和扩展能力,使用 es6 原生类继承。不依赖于其他框架,也提供了良好的拓展性,开发者可以自由拓展。
- Polymer:是 Google 推出的 Web Components 库,支持数据的单向和双向绑定,兼容性较好,跨浏览器性能也较好。
- X-Tag:是微软推出的开源库,支持 Web Components 规范,兼容Web Components。是一个开源 JavaScript 库,包装了 W3C 标准 Web Components API 系列,只需要自定义元素 API 支持即可运行。
转载自:https://juejin.cn/post/7351426862508294185