前言
至于为什么写一篇Web Components的文章,原因大致是因为,目前我在做的组件库项目,一直有一些小伙伴在群里说这个东西,包括文章下面的评论也会出现建议
于是我就去了解并学习了一下,然后发现此文的文章较少,所以花费一些时间写这篇文章供大家了解学习
我也是一个初学者,新手,如果文章有什么不对的地方欢迎大家进行指出以及建议!
然后我通过Web Components的方式已经写了一些组件,我对Web Components部分会单开专栏供大家学习
那么我们开始吧~
什么是Web Components
其实Web Components在MDN上已经有了定义Web Component - Web API 接口参考 | MDN (mozilla.org)
MDN给出的概念是Web Component 是一套不同的技术,允许你创建可重用的定制元素(它们的功能封装在你的代码之外)并且在你的 web 应用中使用它们。
是的,Web Components并不是第三方的库,而是浏览器原生支持的。
简单拿我们的组件库举例,我们知道常见的三大框架Vue、React、Angular,都有属于自己的组件库,那么我们知道,大部分的组件库是没法跨框架进行使用的(当然,你可以做一些框架支持,例如在Angular中去书写Vue,但是这毕竟不是我们讨论的这个点)。
那么,我们想要实现这种跨端,我们可以采用Web Components来实现,也就是用Web Components写的组件库,在哪里都可以用。
也可能不是组件库,我们只有几个组件想进行跨端使用,那么我们都可以去使用Web Components,其实重点就是,在这个组件化的时代,Web Components想进行统一的操作
Web Components的组成
三大组成部分:
- Custom Elements(自定义元素):允许开发者定义自定义 HTML 元素,并定义其行为和样式。
- Shadow DOM(影子 DOM):允许开发者将一个元素的样式和行为封装在一个隔离的作用域内,以防止与其他元素的样式或行为冲突。
- HTML Templates(HTML 模板):允许开发者定义一个带有占位符(slot,没错,就是类似于Vue中的插槽)的 HTML 模板,然后在需要时使用 JavaScript 动态地填充模板。
单看定义是很枯燥乏味的,我们还是通过例子来进行讲解
Web Components实战
我们就写一个最简单的Button,当然,最后我的这个Button组件,可能会是任何名字,也可能会有不同的功能,这也是我们组件化的意义
我们先看一下效果
关于怎么写呢,大家一定要多看文档,文档是很详细的,首先
我们定义出模板
其实我这里的写法顺序和Vue是很类似的,当然,大家写越多的Web Components就会发现,它和Vue是很相似的,这是因为Vue很多地方就是借鉴的Web Components
OK,我们模板大概是这种效果,这里我没用innerHTML的写法,大概是我还是Vue写习惯了哈哈哈
html复制代码 <template id="s-button-template">
<style>
/* 按钮样式 */
button {
display: inline-block;
padding: 10px 20px;
font-size: 16px;
font-weight: bold;
color: #fff;
background-color: #007bff;
border: none;
border-radius: 5px;
cursor: pointer;
}
/* 按钮悬停样式 */
button:hover {
background-color: #0062cc;
}
/* 按钮按下样式 */
button:active {
background-color: #005cbf;
}
</style>
<button>
<slot></slot>
</button>
</template>
这里的样式写法有很多哈,我们采用官网最开始给的这种简单的写法
然后slot我这里不过多解释了,跟Vue的一个道理
好的,我们的模板完事了,我们该通过Js的Class的方式来创建 s-button 这个自定义元素了
我们把代码放出来,然后逐行去解释
js复制代码 // 创建 s-button 自定义元素
class SButton extends HTMLElement {
constructor() {
super();
// 从模板中获取样式和内容
const template = document.getElementById('s-button-template');
const templateContent = template.content;
// 创建 Shadow DOM
const shadowRoot = this.attachShadow({ mode: 'open' });
// 将模板内容复制到 Shadow DOM 中
shadowRoot.appendChild(templateContent.cloneNode(true));
}
}
// 定义 s-button 元素
customElements.define('s-button', SButton);
然后我们去使用s-button即可
html复制代码 <s-button>Hello, World!</s-button>
总结
我们总结一下我们都做了什么
- 我们首先定义了一个模板 s-button-template,它包含了一个简单的样式和一个按钮元素。
- 然后,我们使用 JavaScript 创建了一个名为 SButton 的自定义元素,并在其中使用 Shadow DOM 将模板内容复制到自定义元素中。
- 最后,我们使用 customElements.define 方法将自定义元素注册为 s-button 元素
- 这样就可以在 HTML 中使用 <s-button> 标签来创建 s-button 组件了。
可能大家还是不太懂这个Shadow DOM是干什么的,我单独解释一下
Shadow DOM 起到了封装样式和行为的作用。具体来说,Shadow DOM 使得 s-button 组件的样式和行为可以被封装在一个独立的作用域内,不会影响到外部页面的样式和行为。
你如果不明白,那么如果你开发过Vue的话,你肯定知道scoped,是的,是类似的
只不过实现方式是不同的
Shadow DOM 使用了一种更加底层的方式来实现作用域限定,而 Vue 中的 scoped CSS 则是通过在样式选择器中添加特定的属性选择器来实现的。不过,它们都可以达到相同的效果
计数器
我们可以封装一个计数器组件,其实很简单,就是button的基础上,加上一块计数的区域
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</title>
</head>
<body>
<s-button>Add</s-button>
<template id="s-button-template">
<style>
/* 按钮样式 */
button {
display: inline-block;
padding: 10px 20px;
font-size: 16px;
font-weight: bold;
color: #fff;
background-color: #007bff;
border: none;
border-radius: 5px;
cursor: pointer;
}
/* 按钮悬停样式 */
button:hover {
background-color: #0062cc;
}
/* 按钮按下样式 */
button:active {
background-color: #005cbf;
}
/* 计数器样式 */
.counter {
margin-top: 10px;
font-size: 14px;
color: #999;
}
</style>
<button>
<slot></slot>
</button>
<div class="counter">Counter: 0</div>
</template>
<script>
// 创建 s-button 自定义元素
class SButton extends HTMLElement {
constructor() {
super();
// 从模板中获取样式和内容
const template = document.getElementById('s-button-template');
const templateContent = template.content;
// 创建 Shadow DOM
const shadowRoot = this.attachShadow({ mode: 'open' });
// 将模板内容复制到 Shadow DOM 中
shadowRoot.appendChild(templateContent.cloneNode(true));
// 获取计数器元素
this.counterElement = shadowRoot.querySelector('.counter');
// 初始化计数器值为0
this.counter = 0;
// 添加点击事件处理函数
this.shadowRoot.querySelector('button').addEventListener('click', () => {
this.incrementCounter();
});
}
// 增加计数器值并更新显示
incrementCounter() {
this.counter++;
this.updateCounter();
}
// 更新计数器显示
updateCounter() {
this.counterElement.textContent = `Counter: ${this.counter}`;
}
}
// 定义 s-button 元素
customElements.define('s-button', SButton);
</script>
</body>
</html>
这里就是意思说,我们可以通过正常的事件的方式来给组件加功能
然后我们就会得到对应的效果
Web Components生命周期
- connectedCallback():在元素被插入到文档中时调用,可以进行一些初始化操作,比如添加事件监听器或从外部资源加载数据。
- disconnectedCallback():在元素从文档中移除时调用,可以进行一些清理操作,比如移除事件监听器或取消正在进行的网络请求。
- attributeChangedCallback(attributeName, oldValue, newValue):当元素的属性被添加、移除、更新或替换时调用,可以对属性的变化作出响应。
- adoptedCallback():当元素从一个文档转移到另一个文档(例如通过document.importNode()方法)时被调用。
我们可以通过上面的Button的例子,来应用一下生命周期
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</title>
</head>
<body>
<s-button>Add</s-button>
<template id="s-button-template">
<style>
/* 按钮样式 */
button {
display: inline-block;
padding: 10px 20px;
font-size: 16px;
font-weight: bold;
color: #fff;
background-color: #007bff;
border: none;
border-radius: 5px;
cursor: pointer;
}
/* 按钮悬停样式 */
button:hover {
background-color: #0062cc;
}
/* 按钮按下样式 */
button:active {
background-color: #005cbf;
}
/* 计数器样式 */
.counter {
margin-top: 10px;
font-size: 14px;
color: #999;
}
</style>
<button>
<slot></slot>
</button>
<div class="counter">Counter: 0</div>
</template>
<script>
// 创建 s-button 自定义元素
class SButton extends HTMLElement {
constructor() {
super();
// 创建 Shadow DOM
const shadowRoot = this.attachShadow({ mode: 'open' });
// 从模板中获取样式和内容
const template = document.getElementById('s-button-template');
const templateContent = template.content;
// 将模板内容复制到 Shadow DOM 中
shadowRoot.appendChild(templateContent.cloneNode(true));
// 获取计数器元素
this.counterElement = shadowRoot.querySelector('.counter');
// 初始化计数器值为0
this.counter = 0;
// 添加点击事件处理函数
this.shadowRoot.querySelector('button').addEventListener('click', () => {
this.incrementCounter();
});
}
// 元素被插入到文档中时调用
connectedCallback() {
console.log('SButton connected to the DOM');
}
// 元素从文档中移除时调用
disconnectedCallback() {
console.log('SButton removed from the DOM');
}
// 元素的属性值发生变化时调用
attributeChangedCallback(name, oldValue, newValue) {
console.log(`SButton attribute "${name}" changed from "${oldValue}" to "${newValue}"`);
}
// 监听的属性列表
static get observedAttributes() {
return ['disabled'];
}
// 增加计数器值并更新显示
incrementCounter() {
this.counter++;
this.updateCounter();
}
// 更新计数器显示
updateCounter() {
this.counterElement.textContent = `Counter: ${this.counter}`;
}
}
// 定义 s-button 元素
customElements.define('s-button', SButton);
</script>
</body>
</html>
具体来说,我们在 SButton 类中实现了以下生命周期方法:
- connectedCallback(): 当组件被插入到文档中时调用。在这个例子中,我们在这个方法中使用 console.log() 打印了一条消息,以显示组件被添加到 DOM 中的事件。
- disconnectedCallback(): 当组件从文档中移除时调用。在这个例子中,我们在这个方法中使用 console.log() 打印了一条消息,以显示组件被从 DOM 中移除的事件。
- attributeChangedCallback(name, oldValue, newValue): 当组件所监听的属性值发生变化时调用。在这个例子中,我们监听了 disabled 属性,并在这个方法中使用 console.log() 打印了一条消息,以显示该属性值的变化。
此外,我们还定义了一个 observedAttributes 静态方法,它返回一个数组,用于指定组件需要监听的属性列表。在这个例子中,我们监听了 disabled 属性。
Web Components的样式
除了上面我们说过的Shadow DOM可以将组件的样式和行为可以被封装在一个独立的作用域内之外,书写Web Components的样式会使用var() 函数来引用 CSS 变量
还是拿我们的Button举例,把我们的样式通过css变量来进行一些改造
html复制代码<template id="s-button-template">
<style>
/* 定义 CSS 变量 */
:host {
--button-bg-color: #007bff;
--button-text-color: #fff;
--button-border: none;
}
/* 按钮样式 */
button {
display: inline-block;
padding: 10px 20px;
font-size: 16px;
font-weight: bold;
color: var(--button-text-color); /* 使用 CSS 变量 */
background-color: var(--button-bg-color); /* 使用 CSS 变量 */
border: var(--button-border); /* 使用 CSS 变量 */
border-radius: 5px;
cursor: pointer;
}
/* 按钮悬停样式 */
button:hover {
background-color: #0062cc;
}
/* 按钮按下样式 */
button:active {
background-color: #005cbf;
}
/* 计数器样式 */
.counter {
margin-top: 10px;
font-size: 14px;
color: #999;
}
</style>
<button>
<slot></slot>
</button>
<div class="counter">Counter: 0</div>
</template>
Lit
Lit 是一个基于 Web Components 标准的 JavaScript 库,它提供了一些工具和功能,用于简化 Web Components 的开发。
Lit官网
很简单,Web Components和Lit的关系,我觉得就像JavaScript和Jquery
我们可以通过Lit进行代码的简化
html复制代码 // 定义组件的属性和默认值
static properties = {
counter: { type: Number },
};
constructor() {
super();
this.counter = 0;
}
// 渲染组件的模板
render() {
return html`
<button @click=${this.incrementCounter}>
<slot></slot>
</button>
<div class="counter">Counter: ${this.counter}</div>
`;
}
// 增加计数器值并更新显示
incrementCounter() {
this.counter++;
this.requestUpdate();
}
这里
- 我们通过 @event 装饰器来监听 Web Components 的事件
- 使用了 Lit 提供的属性定义语法,定义了 counter 属性的类型为 Number
- 在组件的 render 方法中,我们使用了 Lit 的模板渲染语法,使用 html 模板函数来定义组件的模板。在模板中,我们使用了 ${} 语法来将 counter 属性的值绑定到计数器显示的文本中,从而实现了数据绑定
当然Lit还有很多别的功能,这里就是介绍一下这个库
尾语
这篇只是介绍一下Web Components,我们并没有进行实战
我觉得这部分知识点是蛮有趣的,所以我应该会单开专栏来讲,同时我们的组件库项目,我也在研究如何更好地应用Web Components在我们的组件库中
当然,现在已经有很多组件库开始应用Web Components了,也欢迎大家去了解
我觉得Web Components我们可以作为一个知识点去学习,而不是去研究,vue、react等框架是否被淘汰的问题,希望大家仁者见仁智者见智~
当然,欢迎大家提出意见,以及补充~