Vue3 与 Web Components 集成
3 与 Web Components 的集成非常方便。Web Components 是一组浏览器原生技术,允许开发者创建可重用的自定义 HTML 元素。Vue 3 提供了对 Web Components 的良好支持,可以将 Vue 组件封装为 Web Components,或者在 Vue 项目中使用外部的 Web Components。
在 Vue 3 中使用 Web Components
Vue 3 可以直接使用外部的 Web Components,只需确保浏览器支持 Web Components 技术(如 Custom Elements 和 Shadow DOM)。
示例:使用外部 Web Components
假设有一个自定义的 Web Components <m-hero-logo>,可以在 Vue 3 中直接使用:
注意事项:
属性传递:Vue 会将属性(如
:title="title")和事件(如@click="handleClick")传递给 Web Components。兼容性:确保浏览器支持 Web Components,或者使用 polyfill。
将 Vue 组件封装为 Web Components
Vue 3 提供了 defineCustomElement 方法,可以将 Vue 组件封装为 Web Components。
示例:将 Vue 组件封装为 Web Components
<template>
<button @click="handleClick">
<slot></slot>
</button>
</template>
<script setup>
function handleClick() {
console.log('按钮被点击了');
}
</script>2
3
4
5
6
7
8
9
10
11
import { defineCustomElement } from 'vue';
import MyButton from './MyButton.ce.vue';
// 将 Vue 组件转换为 Web Components
const MyButtonElement = defineCustomElement(MyButton);
// 注册自定义元素
customElements.define('my-button', MyButtonElement);2
3
4
5
6
7
8
使用封装后的 Web Components
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Vue Web Components</title>
</head>
<body>
<my-button>点击我</my-button>
<script src="./main.js"></script>
</body>
</html>2
3
4
5
6
7
8
9
10
11
12
Vue 组件与 Web Components 的区别
| 特性 | Vue 组件 | Web Components |
|---|---|---|
| 技术栈 | Vue 生态系统 | 浏览器原生技术 |
| 封装性 | 支持作用域样式和逻辑封装 | 使用 Shadow DOM 实现样式和逻辑封装 |
| 跨框架使用 | 仅限于 Vue 项目 | 可以在任何框架或原生 HTML 中使用 |
| 性能 | 依赖 Vue 运行时 | 原生支持,性能更高 |
| 兼容性 | 需要 Vue 运行时 | 需要浏览器支持或 polyfill |
在 Vue 3 中使用 Shadow DOM
Web Components 通常与 Shadow DOM 结合使用,以实现样式和逻辑的封装。
Vue 3 支持在 Web Components 中使用 Shadow DOM。
示例:使用 Shadow DOM
<template>
<button @click="handleClick">
<slot></slot>
</button>
</template>
<script lang="ts" setup>
function handleClick() {
console.log('按钮被点击了');
}
</script>
<style>
button {
background-color: blue;
color: white;
}
</style>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { defineCustomElement } from 'vue';
import MyButton from './MyButton.ce.vue';
// 将 Vue 组件转换为 Web Components,并启用 Shadow DOM
const MyButtonElement = defineCustomElement({
...MyButton,
styles: [MyButton.styles], // 将样式注入 Shadow DOM
});
// 注册自定义元素
customElements.define('my-button', MyButtonElement);2
3
4
5
6
7
8
9
10
11
Vue 3 与 Web Components 的事件通信
Vue 组件和 Web Components 之间可以通过自定义事件进行通信。
示例:Vue 组件监听 Web Components 事件
示例:Web Components 触发自定义事件
<template>
<button @click="handleClick">
<slot></slot>
</button>
</template>
<script lang="ts" setup>
function handleClick() {
const event = new CustomEvent('custom-click', {
detail: { message: 'Hello from Web Components' },
});
dispatchEvent(event);
}
</script>2
3
4
5
6
7
8
9
10
11
12
13
14
Vue 3 与 Web Components 的样式隔离
Web Components 使用 Shadow DOM 实现样式隔离,Vue 3 的样式默认不会影响 Web Components,反之亦然。
示例:样式隔离
<template>
<div class="vue-container">
<my-button>点击我</my-button>
</div>
</template>
<style>
.vue-container {
background-color: lightgray;
}
</style>2
3
4
5
6
7
8
9
10
11
Vue 组件的样式不会影响 Web Components 的内部样式。
Web Components 的样式也不会影响 Vue 组件。
工具支持
:Vite 对 Web Components 提供了开箱即用的支持。
Vue CLI:Vue CLI 项目可以通过配置
vue.config.js支持 Web Components。
总结
Vue 3 可以轻松集成 Web Components,既可以使用外部的 Web Components,也可以将 Vue 组件封装为 Web Components。
使用
defineCustomElement方法可以将 Vue 组件转换为 Web Components。Web Components 的 Shadow DOM 提供了样式和逻辑的封装,适合跨框架使用。
浏览器原生支持的组件化方案?Web Components深度解毒指南
Web Component 深度解析:构建原生可复用组件
Web Components 是一套由浏览器原生支持的 Web API,它允许开发者创建可重用、封装良好的定制 HTML 元素,从而实现组件化的前端开发模式。本文将深入探讨 Web Components 的核心 API 及其使用方式,并通过丰富的代码示例展示如何构建强大的自定义组件。
什么是 Web Component?
Web Components 旨在解决代码复用和组件化管理的问题,它由三项主要技术组成:
- *Custom Elements (自定义元素)*:允许开发者扩展 HTML 元素集合,通过定义新的标签来创建自定义组件 - *Shadow DOM (影子 DOM)*:提供封装样式和结构的能力,使组件内部的 CSS 样式不会影响到外部环境,反之亦然 - *HTML Templates (HTML 模板)*:使用
<template>和<slot>元素定义组件的内容和可替换区域
这些技术可以一起使用来创建封装功能的定制元素,可以在任何地方重用,不必担心代码冲突。
自定义元素 (Custom Elements)
基本概念
自定义元素分为两种类型:
- 独立自定义元素 (Autonomous custom element):继承自 HTML 元素基类 HTMLElement,必须从头开始实现它们的行为 - 自定义内置元素 (Customized built-in element):继承自标准的 HTML 元素,如 HTMLParagraphElement 或 HTMLImageElement,扩展标准元素的行为
创建自定义元素
自定义元素作为一个类来实现,该类可以扩展 HTMLElement(在独立元素的情况下)或者你想要定制的接口(在自定义内置元素的情况下)。
// 独立自定义元素的最小实现
class PopUpInfo extends HTMLElement {
constructor() {
super();
// 此处编写元素功能
}
}
// 自定义内置元素的最小实现,该元素定制了<p>元素
class WordCount extends HTMLParagraphElement {
constructor() {
super();
// 此处编写元素功能
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
注册自定义元素
要使自定义元素在页面中可用,需要调用 CustomElementRegistry.define() 方法:
// 注册独立自定义元素
customElements.define('popup-info', PopUpInfo);
// 注册自定义内置元素
customElements.define('word-count', WordCount, { extends: 'p' });2
3
4
5
使用方式也有所不同:
<!-- 使用独立自定义元素 -->
<popup-info></popup-info>
<!-- 使用自定义内置元素 -->
<p is="word-count"></p>2
3
4
5
生命周期回调
自定义元素生命周期回调包括:
connectedCallback():每当元素添加到文档中时调用disconnectedCallback():每当元素从文档中移除时调用adoptedCallback():每当元素被移动到新文档时调用attributeChangedCallback():在属性更改、添加、移除或替换时调用
class MyCustomElement extends HTMLElement {
static observedAttributes = ["color", "size"];
constructor() {
super();
}
connectedCallback() {
console.log("自定义元素添加至页面。");
}
disconnectedCallback() {
console.log("自定义元素从页面中移除。");
}
adoptedCallback() {
console.log("自定义元素移动至新页面。");
}
attributeChangedCallback(name, oldValue, newValue) {
console.log(`属性 ${name} 已变更。`);
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
响应属性变化
为了有效地使用属性,元素必须能够响应属性值的变化。为此,自定义元素需要:
一个名为
observedAttributes的静态属性,包含需要监听的属性名称数组实现
attributeChangedCallback()生命周期回调
class MyCustomElement extends HTMLElement {
static observedAttributes = ["size"];
constructor() {
super();
}
attributeChangedCallback(name, oldValue, newValue) {
console.log(`属性 ${name} 已由 ${oldValue} 变更为 ${newValue}。`);
// 根据属性变化更新组件
}
}
customElements.define('my-custom-element', MyCustomElement);2
3
4
5
6
7
8
9
10
11
12
13
14
使用示例:
<my-custom-element size="100"></my-custom-element>Shadow DOM (影子 DOM)
基本概念
Shadow DOM 允许你将一个 DOM 树附加到一个元素上,并且使该树的内部对于在页面中运行的 和 CSS 是隐藏的。关键术语:
- 影子宿主 (Shadow host):影子 DOM 附加到的常规 DOM 节点 - 影子树 (Shadow tree):影子 DOM 内部的 DOM 树 - 影子边界 (Shadow boundary):影子 DOM 终止,常规 DOM 开始的地方 - 影子根 (Shadow root):影子树的根节点
创建 Shadow DOM
const host = document.querySelector('#host');
const shadow = host.attachShadow({ mode: 'open' });
const span = document.createElement('span');
span.textContent = "I'm in the shadow DOM";
shadow.appendChild(span);2
3
4
5
attachShadow() 方法接受一个配置对象,其中 mode 属性可以是:
open:可以通过host.shadowRoot获取影子 DOMclosed:无法通过host.shadowRoot获取影子 DOM(返回 null)
Shadow DOM 的样式封装
Shadow DOM 的一个重要特性是样式封装,组件内部的样式不会影响外部,外部的样式也不会影响组件内部。
class PopUpInfo extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
// 创建一些 CSS 应用于影子 DOM
const style = document.createElement('style');
style.textContent = `
.wrapper {
position: relative;
}
.info {
font-size: 0.8rem;
width: 200px;
display: inline-block;
border: 1px solid black;
padding: 10px;
background: white;
border-radius: 10px;
opacity: 0;
transition: 0.6s all;
position: absolute;
bottom: 20px;
left: 10px;
z-index: 3;
}
img {
width: 1.2rem;
}
.icon:hover + .info, .icon:focus + .info {
opacity: 1;
}
`;
shadow.appendChild(style);
// 添加其他元素...
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
HTML 模板 (HTML Templates)
<template> 元素
<template> 元素使你可以编写不在呈现页面中显示的标记模板,然后它们可以作为自定义元素结构的基础被多次重用。
<template id="my-template">
<style>
/* 组件样式 */
</style>
<div class="container">
<slot></slot> <!-- 这里可以插入其他元素 -->
</div>
</template>2
3
4
5
6
7
8
<slot> 元素
<slot> 元素作为插槽,允许你在使用自定义元素时插入自定义内容。
const template = document.createElement('template');
template.innerHTML = `
<style>
label { display: block; }
.description { color: #a9a9a9; font-size: .8em; }
</style>
<label>
<input type="checkbox" />
<slot></slot>
<span class="description"><slot name="description"></slot></span>
</label>
`;2
3
4
5
6
7
8
9
10
11
12
使用示例:
<todo-item>
todo1
<span slot="description">其他描述</span>
</todo-item>2
3
4
完整示例:实现一个下拉选择组件
让我们实现一个包含 select 和 option 的基础下拉选择组件。
Select 组件
class Select extends HTMLElement {
constructor() {
super();
const template = document.createElement("template");
template.innerHTML = `
<style>
:host {
position: relative;
display: inline-block;
}
.select-inner {
height: 34px;
border: 1px solid #cdcdcd;
box-sizing: border-box;
font-size: 13px;
outline: none;
padding: 0 10px;
border-radius: 4px;
}
.drop {
position: absolute;
top: 36px;
left: 0;
width: 100%;
padding: 4px 0;
border-radius: 2px;
overflow: auto;
max-height: 256px;
box-shadow: 0 2px 4px rgba(0, 0, 0, .12), 0 0 6px rgba(0, 0, 0, .04);
display: none;
}
</style>
<input class="select-inner" readonly>
<div class="drop">
<slot></slot>
</div>
`;
const shadowEle = this.attachShadow({ mode: "open" });
const content = template.content.cloneNode(true);
shadowEle.appendChild(content);
this.input = shadowEle.querySelector(".select-inner");
this.dropEle = shadowEle.querySelector(".drop");
this.value = null;
this.input.addEventListener("click", () => {
this.dropEle.style.display = "block";
});
this.BodyClick = (ev) => {
if (ev.target !== this.input) {
this.dropEle.style.display = "none";
}
};
this.dropEle.addEventListener("click", (ev) => {
const target = ev.target;
const nodeName = target.nodeName.toLowerCase();
if (nodeName === "ivy-option") {
this.value = target.getAttribute("value");
this.input.setAttribute("value", target.innerHTML);
this.dispatchEvent(new CustomEvent("change", {
detail: { value: this.value }
}));
this.dropEle.style.display = "none";
}
});
}
connectedCallback() {
document.addEventListener("click", this.BodyClick, true);
}
disconnectedCallback() {
document.removeEventListener("click", this.BodyClick);
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
Option 组件
class Option extends HTMLElement {
constructor() {
super();
const template = document.createElement("template");
template.innerHTML = `
<style>
:host {
position: relative;
}
.option {
height: 32px;
line-height: 32px;
box-sizing: border-box;
font-size: 13px;
color: #333333;
padding: 0 10px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.option:hover {
background-color: #f4f4f4;
}
</style>
<div class="option">
<slot></slot>
</div>
`;
const shadowELe = this.attachShadow({ mode: "open" });
const content = template.content.cloneNode(true);
shadowELe.appendChild(content);
}
static get observedAttributes() {
return ["value"];
}
attributeChangedCallback(name, oldValue, newValue) {
if (name === "value" && oldValue !== newValue) {
// 处理value属性变化
}
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
注册和使用
customElements.define("ivy-select", Select);
customElements.define("ivy-option", Option);2
<ivy-select>
<ivy-option value="1">Apple</ivy-option>
<ivy-option value="2">Banana</ivy-option>
<ivy-option value="3">Orange</ivy-option>
</ivy-select>2
3
4
5
扩展内置元素
Web Components 还允许你扩展内置 HTML 元素的功能。
class ExpandableList extends HTMLUListElement {
constructor() {
super();
this.style.position = 'relative';
// 创建切换按钮
this.toggleBtn = document.createElement('button');
this.toggleBtn.style.position = 'absolute';
this.toggleBtn.style.border = 'none';
this.toggleBtn.style.background = 'none';
this.toggleBtn.style.padding = '0';
this.toggleBtn.style.top = '0';
this.toggleBtn.style.left = '5px';
this.toggleBtn.style.cursor = 'pointer';
this.toggleBtn.innerText = '>';
this.appendChild(this.toggleBtn);
// 定义点击事件
this.toggleBtn.addEventListener('click', () => {
this.dataset.expanded = !this.isExpanded;
});
}
get isExpanded() {
return this.dataset.expanded !== 'false' && this.dataset.expanded !== null;
}
static get observedAttributes() {
return ['data-expanded'];
}
attributeChangedCallback(name, oldValue, newValue) {
this.updateStyles();
}
updateStyles() {
const transform = this.isExpanded ? 'rotate(90deg)' : '';
this.toggleBtn.style.transform = transform;
[...this.children].forEach((child) => {
if (child !== this.toggleBtn) {
child.style.display = this.isExpanded ? '' : 'none';
}
});
}
connectedCallback() {
this.updateStyles();
}
}
customElements.define('expandable-list', ExpandableList, { extends: 'ul' });2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
使用示例:
<ul is="expandable-list" data-expanded name="myul">
<li>apple</li>
<li>banana</li>
</ul>2
3
4
预告:Polymer 和 Lit 库
虽然 Web Components 提供了强大的原生能力,但在实际开发中,我们可能会使用一些库来简化开发流程。下面简单介绍两个流行的 Web Components 库:
Polymer
Polymer 是一个开源的 JavaScript 库,由 Google 开发,旨在简化 Web 组件的开发过程。它提供了一系列语法糖和工具,使得创建和使用 Web Components 更加便捷。
Polymer 的核心特性包括:
- 声明式数据绑定(单向绑定使用
[[ ]],双向绑定使用) - 便捷的属性系统
- 简化的事件处理
<dom-module id="hello-world">
<template>
<style>
:host { display: block; padding: 10px; }
</style>
<h1>Hello, [[name]]!</h1>
</template>
<script>
Polymer({
is: 'hello-world',
properties: {
name: { type: String, value: 'World' }
}
});
</script>
</dom-module>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Lit
Lit 是一个轻量级的库,基于 Polymer 项目发展而来,旨在简化 Web 组件的开发。它提供了更简洁的 API 和更好的性能,是当前 Web Components 生态中的重要组成部分。
Lit 的核心特点:
- 简单的组件定义方式
- 高效的渲染
- 模板字面量支持
import { LitElement, html } from 'lit';
class MyElement extends LitElement {
static properties = {
name: { type: String }
};
constructor() {
super();
this.name = 'World';
}
render() {
return html`<h1>Hello, ${this.name}!</h1>`;
}
}
customElements.define('my-element', MyElement);2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
结语
Web Components 为 Web 开发带来了一种强大的组件化方式,让开发者能够更好地组织代码,提升代码复用性和维护性。通过深入学习和实践,你会发现 Web Components 在现代前端项目中的巨大价值。
虽然 Web Components 已经得到了所有现代浏览器的支持,但在实际项目中,你可能还需要考虑一些额外的因素,如浏览器兼容性、性能优化和与现有框架的集成等。Polymer 和 Lit 这样的库可以帮助你解决这些问题,让你更专注于业务逻辑的实现。
希望本文能够帮助你理解 Web Components 的核心概念和 API,并激发你尝试在自己的项目中使用这项强大的技术。
