Web Components

概念

Web Components提供了基于原生支持的、对视图层的封装能力。

它可以让单个组件相关的javaScript、css、html模板运行在以html标签为界限的局部环境中,不会影响到全局,组件间也不会相互影响。

就是提供了原生JS实现自定义标签的能力,并且提供了标签内完整的生命周期 。

  • Custom elements(自定义元素):JavaScript API,允许定义custom elements及其行为,然后按需使用。
  • Shadow DOM(影子DOM):JavaScript API,用于将封装的”影子“DOM树附加到元素(与主文档DOM分开呈现)并控制其关联的功能。通过这种方式,保持元素的功能私有,这样它们就可以被脚本化和样式化,而不用担心与文档的其他部分发生冲突。
  • HTML templates(HTML模板):和元素使开发者可以编写与HTML结构类似的组件和样式。然后它们可以作为自定义元素结构的基础被多次重用。

使用

btn.js:

1
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
// 自定义元素,继承HTMLElement
class Btn extends HTMLElement {
constructor() {
super()

// 创建shadowDOM
const shadowDom = this.attachShadow({
mode: "open"
})

// 给shadowDOM加东西(传统方式)
// this.p = this.h("p")
// this.p.innerText = "oh my god"
// this.p.setAttribute("style","width:200px;height:200px;border:1px solid black")
// shadowDom.appendChild(this.p)

// 给shadowDOM加东西(template方式)
this.template = this.h("template")
// 这里面写的style都会被隔离,不会影响外部
this.template.innerHTML = `
<style>
div {
width:200px;
height:200px;
border:1px solid black;
}
</style>
<div>oh my god</div>
`
shadowDom.appendChild(this.template.content.cloneNode(true))
}

h(el) {
return document.createElement(el)
}

// 生命周期

// 当自定义元素第一次被连接到文档DOM时被调用
connectedCallback() {
console.log('connectedCallback')
}
//当自定义元素与文档 DOM 断开连接时被调用。
disconnectedCallback() {
console.log('disconnectedCallback')
}
//当自定义元素被移动到新文档时被调用
adoptedCallback() {
console.log('adoptedCallback')
}
//当自定义元素的一个属性被增加、移除或更改时被调用
attributeChangedCallback() {
console.log('attributeChangedCallback')
}
}

// 注册自定义元素
window.customElements.define("my-btn", Btn)

index.html:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="./btn.js"></script>
</head>

<body>
<my-btn></my-btn>
<my-btn></my-btn>
<!-- 这个原生div不会受影响 -->
<div></div>
</body>

</html>

页面效果:

Vue+Web Components

在vite.config.ts中配置:

1
2
3
4
5
6
7
8
9
10
11
12
export default defineConfig({
plugins: [
vue({
template: {
compilerOptions: {
// 在这里注册的组件都会跳过Vue组件检测,被识别为Custom elements
isCustomElement:(tag)=>tag.includes("my-btn")
}
}
}),
],
})

custom-vue.ce.vue:

后缀名为.ce.vue的文件才能被识别为Web Components。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<div>
<button>OH MY GOD</button>
</div>
</template>

<script setup lang="ts">
defineProps<{
obj:any
}>()
</script>

<style scoped lang="scss">
button {
width: 100px;
height: 100px;
}
</style>

App.vue:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<div>
<my-btn :obj="JSON.stringify(obj)"></my-btn>
</div>
</template>

<script setup lang="ts">
import { defineCustomElement } from "vue"
import customVueCeVue from "./components/custom-vue.ce.vue"

const Btn = defineCustomElement(customVueCeVue)
window.customElements.define("my-btn",Btn)

// 不能直接传参,因为参数内容会被映射到标签上
// 于是变成了<my-btn :obj="[object Object]"></my-btn>
// 如果要传引用类型,只能使用stringify
const obj = {name:"yajue"}
</script>