Web Component 是一套由 W3C 标准化的原生 Web 技术,旨在通过封装实现可复用、高内聚的定制化 HTML 元素。Web Component 的核心目标是提供一种标准化的方式来创建和使用自定义的 HTML 标签,同时确保这些标签的行为、样式和结构能够被完全封装,不会受到外部代码的影响。
1、Web Component 的简介
Web Component 其核心技术包括以下三部分:
- Custom Elements(自定义元素
允许开发者创建新的 HTML 标签,通过继承HTMLElement类定义组件逻辑。例如,可注册<my-button>标签并绑定行为。 - Shadow DOM(影子 DOM
提供封装机制,将组件的 DOM 和样式与主文档隔离,避免全局污染。通过attachShadow({ mode: 'open' })创建独立作用域。 - HTML Templates(HTML 模板
使用<template>和<slot>标签定义可复用的 HTML 结构,支持动态内容插入。模板内容不会直接渲染,需通过 JavaScript 激活。
1.1 优点
- 高度封装性
Shadow DOM 实现样式和 DOM 的隔离,确保组件内部逻辑不受外部影响,适合跨团队协作。 - 跨框架复用
原生支持特性使其可在 Vue、React 等框架中直接使用,避免重复开发不同技术栈的组件库。 - 原生兼容性
不依赖第三方框架,浏览器原生支持,减少项目维护成本和版本升级风险。 - 生命周期管理
提供connectedCallback(挂载)、disconnectedCallback(卸载)等钩子函数,支持组件状态管理。
1.2 兼容性
兼容性
- Chrome、Firefox、Safari 及 Edge 的最新版本均原生支持 Web Component 的三大核心规范(Custom Elements、Shadow DOM、HTML Templates)
- IE 与旧版浏览器:需通过 Polyfill 实现兼容(如
@webcomponents/webcomponentsjs)
使用 Polyfill 提高兼容性
对于不完全支持 Web Component 的浏览器,可以使用 Polyfill 库来提高兼容性。
- webcomponents.js:这是一个由 Google 维护的库,提供了对 Web Components 标准的支持。
npm install @webcomponents/webcomponentsjs
// index.js 或 main.js
import '@webcomponents/webcomponentsjs/webcomponents-loader.js';
- 通过 CDN 引入:
<script src="https://unpkg.com/@webcomponents/webcomponentsjs/webcomponents-loader.js"></script>
2、Custom Elements (自定义元素)
Custom Elements 允许你定义和使用新的自定义 HTML 标签。你可以通过 JavaScript 创建这些自定义元素,并为它们添加特定的行为和功能。
2.1 自定义元素的类型
- 自定义内置元素(Customized built-in element)继承自标准的 HTML 元素,例如 HTMLImageElement 或 HTMLParagraphElement。它们的实现定义了标准元素的行为。
<script>
// 重写内置元素
// 自定义img标签
class MyImg extends HTMLImageElement {
constructor() {
super();
}
}
// 自定义p标签
class MyP extends HTMLParagraphElement {
constructor() {
super();
}
}
</script>
- 独立自定义元素(Autonomous custom element)继承自 HTML 元素基类 HTMLElement。你必须从头开始实现它们的行为。
<script>
// 重新定义一个新元素
class MyDom extends HTMLElement {
constructor() {
super();
}
}
</script>
2.2 注册自定义元素及使用
要使自定义元素在页面中可用,请调用 Window.customElements 的 define() 方法。
- 自定义内置元素
<body>
<img src="https://www.baidu.com/img/flexible/logo/pc/result.png" alt="" />
<!-- 第三步:使用内置元素,但将自定义名称作为 is 属性的值 -->
<img is="my-img" src="https://www.baidu.com/img/flexible/logo/pc/result.png" alt="" />
<script>
// 第一步:自定义内置元素
class MyImg extends HTMLImageElement {
constructor() {
super();
this.style.backgroundColor = "red";
}
}
// 第二步:注册,要传对应的内置元素名 { extends: "img" }
customElements.define("my-img", MyImg, { extends: "img" });
</script>
</body>

- 独立自定义元素(Autonomous custom element)继承自 HTML 元素基类 HTMLElement。你必须从头开始实现它们的行为。
<body>
<!-- 第三步:直接使用自定义元素作为标签 -->
<my-dom>121212121</my-dom>
<script>
// 第一步:自定义元素
class MyDom extends HTMLElement {
constructor() {
super();
this.style.backgroundColor = "red";
this.style.color = "white";
}
}
// 第二步:注册
customElements.define("my-dom", MyDom);
</script>
</body>

注意:
define()方法接受以下参数:
- name
元素的名称。必须以小写字母开头,包含一个连字符,并符合规范中有效名称的定义中列出的一些其他规则。- constructor
自定义元素的构造函数。- options
仅对于自定义内置元素,这是一个包含单个属性extends的对象,该属性是一个字符串,命名了要扩展的内置元素。
2.3 自定义元素生命周期
constructor()
- 描述: 构造函数是自定义元素的初始化方法。
- 调用时机: 当自定义元素实例被创建时调用。
- 注意事项:
- 必须调用
super()来确保父类的构造函数被调用。 - 不应该在这个方法中操作 DOM,因为此时元素还没有被插入到文档中。
- 必须调用
connectedCallback()
- 描述: 每当元素被插入到文档中时调用。
- 用途: 通常用于设置初始状态、监听事件或进行其他需要在元素插入文档后执行的操作。
disconnectedCallback()
- 描述: 每当元素从文档中移除时调用。
- 用途: 通常用于清理资源、移除事件监听器或进行其他需要在元素移除文档前执行的操作。
attributeChangedCallback(name, oldValue, newValue)
- 描述: 每当自定义元素监听的属性
observedAttributes发生变化时调用。 - 参数:
name: 发生变化的属性名称。oldValue: 属性的旧值。newValue: 属性的新值。
- 用途: 通常用于响应属性变化并更新元素的状态或行为。
// 监听的属性
static observedAttributes = ["color", "size"];
// 或者写成get函数
static get observedAttributes() {
return ["color", "size"];
}
// 回调函数
attributeChangedCallback(name, oldValue, newValue) {
console.log("自定义元素属性变化",name,oldValue,newValue);
}
- 使用:
<body>
<my-dom color="red" size="16">121212121</my-dom>
<button onclick="document.querySelector('my-dom').setAttribute('color','gary')">按钮</button>
<script>
class MyDom extends HTMLElement {
static get observedAttributes() {
return ["color", "size"];
}
constructor() {
super();
console.log("自定义元素创建",this.getAttribute('color'),this.getAttribute('size'));
}
connectedCallback() {
console.log("自定义元素添加至页面",this.getAttribute('color'),this.getAttribute('size'));
}
attributeChangedCallback(name, oldValue, newValue) {
console.log("自定义元素属性变化",name,oldValue,newValue);
}
}
customElements.define("my-dom", MyDom);
</script>
</body>

- 在元素创建时,初始化属性也会触发
attributeChangedCallback - 当重新设置属性值时,即使新旧值一样,也会触发
attributeChangedCallback
adoptedCallback(oldDocument, newDocument)
- 描述: 每当元素被移动到一个新的文档时调用。
- 参数:
oldDocument: 元素原来所在的文档。newDocument: 元素现在所在的文档。
- 用途: 通常用于处理跨文档移动的情况,例如移动到
iframe中。
<body>
<my-dom id="mydom">121212121</my-dom>
<button id="btn">按钮</button>
<script>
class MyDom extends HTMLElement {
constructor() {
super();
console.log("自定义元素创建");
}
connectedCallback() {
console.log("自定义元素添加至页面");
}
adoptedCallback(oldDocument, newDocument) {
console.log("自定义元素被移动到新文档",oldDocument, newDocument);
}
}
customElements.define("my-dom", MyDom);
document.querySelector("#btn").addEventListener("click", ()=> {
let mydom = document.querySelector("#mydom");
// 创建一个新的文档
let newDoc = document.implementation.createHTMLDocument("newDoc");
// 移动到新文档
newDoc.adoptNode(mydom);
})
</script>
</body>

3、Shadow DOM (影子DOM)
影子 DOM(Shadow DOM)允许你将一个 DOM 树附加到一个元素上,并且使该树的内部对于在页面中运行的 JavaScript 和 CSS 是隐藏的。这个子树与主文档的 DOM 树是隔离的,这意味着样式和脚本在 Shadow DOM 内部是独立的,不会影响到主文档,反之亦然。这种隔离性使得组件可以更好地封装其内部结构和样式,从而提高代码的可维护性和复用性。
- 在宿主元素上调用
attachShadow(),创建一个Shadow DOM并返回Shadow DOM根对象(类似文档对象document)
3.1 attachShadow()
mode决定了外部代码是否可以访问 Shadow DOM 树对象- 当创建了Shadow DOM,宿主元素的内容就无效了,(ps:可以使用插槽渲染这些内容)
attachShadow({ mode: 'open' })可以访问 Shadow DOM 树
<body>
<div id="host">host</div>
<script>
let host = document.getElementById('host');
let shadowRoot = host.attachShadow({ mode: 'open' });
console.log(shadowRoot, host.shadowRoot);
</script>
</body>

attachShadow({ mode: 'closed' })不可以访问 Shadow DOM 树
创建时返回的Shadow DOM根对象还可以使用,但是无法重新从宿主元素上获取。
<body>
<div id="host">host</div>
<script>
let host = document.getElementById('host');
let shadowRoot = host.attachShadow({ mode: 'closed' });
console.log(shadowRoot, host.shadowRoot);
</script>
</body>

3.2 Shadow DOM的内容
插入元素
<body>
<div id="host">host</div>
<script>
let host = document.getElementById('host');
let shadowRoot = host.attachShadow({ mode: 'open' });
let h1 = document.createElement('h1');
h1.innerText = 'Web Component';
shadowRoot.appendChild(h1);
</script>
</body>
innerHTML插入一段html内容
<body>
<div id="host">host</div>
<script>
let host = document.getElementById('host');
let shadowRoot = host.attachShadow({ mode: 'open' });
shadowRoot.innerHTML = `
<h1>title</h1>
<div>content</div>
`;
</script>
</body>
插入样式
<body>
<div id="host">host</div>
<script>
let host = document.getElementById('host');
let shadowRoot = host.attachShadow({ mode: 'open' });
shadowRoot.innerHTML = `
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css" />
<style>
h1 {
color: red;
}
</style>
<h1>title</h1>
<div>content</div>
`;
</script>
</body>
插入html文档
<body>
<div id="host">host</div>
<script>
let host = document.getElementById('host');
let shadowRoot = host.attachShadow({ mode: 'open' });
// 这里创建了一个文档来添加,或者你可以加载一个html文件来添加
shadowRoot.appendChild(document.implementation.createHTMLDocument('shadow').documentElement);
</script>
</body>

3.3 JavaScript隔离
- 无法通过主文档
document直接获取Shadow DOM的元素 - 可以通过Shadow DOM根对象获取
Shadow DOM的元素
<body>
<span>主文档</span>
<div id="host">host</div>
<script>
let host = document.getElementById('host');
let shadowRoot = host.attachShadow({ mode: 'open' });
shadowRoot.innerHTML = `<span>Shadow Dom</span>`;
console.log('document',...document.querySelectorAll('span'));
console.log('shadowRoot',...shadowRoot.querySelectorAll('span'));
</script>
</body>

3.4 CSS隔离
主文档样式无法影响Shadow Dom的样式,反之亦然;
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Web Component</title>
<style>
span {
color: red;
}
</style>
</head>
<body>
<span>主文档</span>
<div id="host">host</div>
<script>
let host = document.getElementById('host');
let shadowRoot = host.attachShadow({ mode: 'open' });
shadowRoot.innerHTML = `
<style>
span{
color: blue;
}
</style>
<span>Shadow Dom</span>
`;
</script>
</body>
</html>

主文档样式和Shadow Dom的样式都可以影响宿主元素
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Web Component</title>
<style>
#host{
width: 200px;
border: 2px solid red;
}
</style>
</head>
<body>
<span>主文档</span>
<div id="host">host</div>
<script>
let host = document.getElementById('host');
let shadowRoot = host.attachShadow({ mode: 'open' });
// :host 选择器代表宿主元素
shadowRoot.innerHTML = `
<style>
:host{
width: 100px;
height: 100px;
background-color: green;
}
</style>
<span>Shadow Dom</span>
`;
</script>
</body>
</html>

:host选中当前的宿主元素
<script>
let host = document.getElementById('host');
let shadowRoot = host.attachShadow({ mode: 'open' });
shadowRoot.innerHTML = `
<style>
:host{
width: 100px;
height: 100px;
background-color: green;
}
</style>
<span>Shadow Dom</span>
`;
</script>
:host()当宿主元素符合特定选择器时的样式
<body>
<span>主文档</span>
<div id="host" class="a">host</div>
<script>
let host = document.getElementById('host');
let shadowRoot = host.attachShadow({ mode: 'open' });
shadowRoot.innerHTML = `
<style>
// 当宿主元素id为host时的样式
:host(#host){
width: 100px;
}
// 当宿主元素class为a时的样式
:host(.a){
height: 100px;
}
// 当宿主元素class为b时的样式
:host(.b){
background: red;
}
// 当宿主元素d为host并且class为a时的样式
:host(#host.a){
background: greenyellow;
}
</style>
<span>Shadow Dom</span>
`;
</script>
</body>

:host-context()当宿主元素的父元素符合特定选择器时宿主元素的样式
<body>
<span>主文档</span>
<div class="par1">
<div class="par2">
<div id="host">host</div>
</div>
</div>
<script>
let host = document.getElementById('host');
let shadowRoot = host.attachShadow({ mode: 'open' });
shadowRoot.innerHTML = `
<style>
// 当宿主元素的父元素class为par1时,宿主元素的样式生效
:host-context(.par1) {
color: red;
}
// 当宿主元素的父元素class为par2时,宿主元素的样式生效
:host-context(.par2) {
font-size: 20px;
}
</style>
<span>Shadow Dom</span>
`;
</script>
</body>

4、HTML Templates (HTML模板)
4.1 <template>HTML模板
核心:通过 <template> 标签定义可复用的 HTML 结构片段,其内容在页面初始加载时不会直接渲染,需通过 JavaScript 动态激活后插入到 DOM 中。
惰性加载与内容封装
<template> 标签内的 HTML 和 CSS 会被浏览器解析但不渲染,避免资源浪费,同时保持内部结构的独立性。
<body>
<span>主文档</span>
<template id="my-template">
<p>template</p>
</template>
</body>

动态内容填充
通过 document.querySelector 获取模板,结合 cloneNode(true) 方法克隆内容,支持动态插入数据(如修改属性、绑定事件)。
<body>
<span>主文档</span>
<div id="box"></div>
<template id="my-template">
<p>template 1</p>
<p>template 2</p>
</template>
<script>
let template = document.getElementById("my-template");
let content = template.content.cloneNode(true);
content.querySelector("p").innerHTML = "template cloneNode";
document.getElementById("box").appendChild(content);
</script>
</body>

作用域隔离
模板内部的脚本(<script>)和样式(<style>)在激活前不会执行,确保逻辑与全局环境隔离。
- 未激活前,不影响全局样式,不执行脚本
<body>
<span>主文档</span>
<p class="text">主文档 p</p>
<div id="box"></div>
<template id="my-template">
<style>.text { color: red; }</style>
<p class="text">template 1</p>
<p>template 2</p>
<script>
document.querySelectorAll('p.text').forEach(p => p.textContent += '动态激活');
</script>
</template>
</body>

- 插入其他元素,激活后,影响全局样式,执行脚本
<body>
<span>主文档</span>
<p class="text">主文档 p</p>
<div id="box"></div>
<template id="my-template">
<style>.text { color: red; }</style>
<p class="text">template 1</p>
<p>template 2</p>
<script>
document.querySelectorAll('p.text').forEach(p => p.textContent += '动态激活');
</script>
</template>
<script>
let template = document.getElementById("my-template");
let content = template.content.cloneNode(true);
document.getElementById("box").appendChild(content);
</script>
</body>

4.2 <slot>插槽
- 占位符机制
Slot 是 Web Components 中用于在组件内部预留内容区域的占位符,允许外部在使用组件时插入自定义内容,实现动态内容分发。12 - Shadow DOM 依赖
单独使用Slot没有意义,Slot 必须与 Shadow DOM 结合使用,通过封装性隔离组件内部结构,同时借助插槽建立与外部 DOM 的内容连接通道。
<body>
<span>主文档</span>
<div id="box">
<span>span1</span>
<span slot="title">title</span>
<span>span2</span>
</div>
<template id="my-template">
<p>默认插槽: <slot>默认插槽</slot></p>
<p>具名插槽: <slot name="title">具名插槽</slot></p>
</template>
<script>
let template = document.getElementById("my-template");
let content = template.content.cloneNode(true);
document.getElementById("box").attachShadow({ mode: "open" }).appendChild(content);
</script>
</body>

这里是复制模版内容到shadow dom,也可以直接设置shadow dom的内容innerHTML。
5、使用示例-自定义输入框
5.1 创建自定义元素类CustomInput
- 有两个自定义属性,
valueplaceholder
<script>
class CustomInput extends HTMLElement {
static get observedAttributes() {
return ["value", "placeholder"];
}
constructor() {
super();
}
}
customElements.define("custom-input", CustomInput);
</script>
5.2 创建Shadow Dom并添加输入框样式内容
contenteditable属性可以让元素内容可编辑
<script>
class CustomInput extends HTMLElement {
static get observedAttributes() {
return ["value", "placeholder"];
}
constructor() {
super();
this.attachShadow({ mode: "open" });
this.shadowRoot.innerHTML = this.createElementContent(this.getAttribute("value"), this.getAttribute("placeholder"));
}
// 生成元素内容
createElementContent(value,placeholder) {
return `
<style>
#box {
width: 400px;
height: 40px;
line-height: 40px;
border: 1px solid #d9d9d9;
border-radius: 4px;
padding: 0 10px;
box-sizing: border-box;
position: relative;
}
.placeholder {
color: #999;
font-size: 14px;
position: absolute;
left: 10px;
z-index: 0;
}
.value {
color: #333;
height: 100%;
outline: none;
position: relative;
z-index: 1;
}
</style>
<div id="box">
<div class="placeholder">${placeholder}</div>
<div class="value" contenteditable>${value}</div>
</div>
`;
}
}
customElements.define("custom-input", CustomInput);
</script>
5.3 监听输入内容变化
<script>
class CustomInput extends HTMLElement {
static get observedAttributes() {
return ["value", "placeholder"];
}
constructor() {
super();
this.attachShadow({ mode: "open" });
this.shadowRoot.innerHTML = this.createElementContent(this.getAttribute("value"), this.getAttribute("placeholder"));
}
// ....
// 自定义元素添加至页面后触发回调
connectedCallback() {
// 监听输入框内容变化修改属性值
this.shadowRoot.querySelector(".value").addEventListener("keyup", (e)=>{
let input = e.target.innerText;
if(input!==this.getAttribute("value")){
this.setAttribute("value",input.trim());
}
});
}
}
customElements.define("custom-input", CustomInput);
</script>
5.4 value变化时处理样式和事件
- 自定义事件 CustomEvent
<script>
class CustomInput extends HTMLElement {
static get observedAttributes() {
return ["value", "placeholder"];
}
constructor() {
super();
this.attachShadow({ mode: "open" });
this.shadowRoot.innerHTML = this.createElementContent(this.getAttribute("value"), this.getAttribute("placeholder"));
}
// ....
// 自定义元素属性变化时触发回调
attributeChangedCallback(name, oldValue, newValue) {
if(name==="value"&&newValue!==oldValue){
// 有值时,背景色为白色,遮挡placeholder
this.shadowRoot.querySelector(".value").style.background = newValue?"#fff":"transparent";
// 触发自定义change事件
this.dispatchEvent(new CustomEvent("change",{detail:{value:newValue}}));
}
}
}
customElements.define("custom-input", CustomInput);
</script>
5.5 使用自定义输入框并监听change事件
<custom-input id="input" value="" placeholder="请输入"></custom-input>
<script>
document.getElementById("input").addEventListener("change", (e)=>{
console.log('change',e.detail.value);
});
</script>

5.6 完整代码
<body>
<h1>主文档</h1>
<custom-input id="input" value="" placeholder="请输入"></custom-input>
<script>
class CustomInput extends HTMLElement {
static get observedAttributes() {
return ["value", "placeholder"];
}
constructor() {
super();
this.attachShadow({ mode: "open" });
this.shadowRoot.innerHTML = this.createElementContent(this.getAttribute("value"), this.getAttribute("placeholder"));
}
// 生成元素内容
createElementContent(value,placeholder) {
return `
<style>
#box {
width: 400px;
height: 40px;
line-height: 40px;
border: 1px solid #d9d9d9;
border-radius: 4px;
padding: 0 10px;
box-sizing: border-box;
position: relative;
}
.placeholder {
color: #999;
font-size: 14px;
position: absolute;
left: 10px;
z-index: 0;
}
.value {
color: #333;
height: 100%;
outline: none;
position: relative;
z-index: 1;
}
</style>
<div id="box">
<div class="placeholder">${placeholder}</div>
<div class="value" contenteditable>${value}</div>
</div>
`;
}
// 自定义元素添加至页面后触发回调
connectedCallback() {
// 监听输入框内容变化修改属性值
this.shadowRoot.querySelector(".value").addEventListener("keyup", (e)=>{
let input = e.target.innerText;
if(input!==this.getAttribute("value")){
this.setAttribute("value",input.trim());
}
});
}
// 自定义元素属性变化时触发回调
attributeChangedCallback(name, oldValue, newValue) {
if(name==="value"&&newValue!==oldValue){
// 有值时,背景色为白色,遮挡placeholder
this.shadowRoot.querySelector(".value").style.background = newValue?"#fff":"transparent";
// 触发自定义change事件
this.dispatchEvent(new CustomEvent("change",{detail:{value:newValue}}));
}
}
}
customElements.define("custom-input", CustomInput);
</script>
<script>
document.getElementById("input").addEventListener("change", (e)=>{
console.log('change',e.detail.value);
});
</script>
</body>
6、穿透隔离机制
6.1 CSS 样式穿透
CSS变量注入
利用 CSS 自定义属性(CSS Variables)传递值到 Shadow DOM,实现动态样式控制。
/* 外部定义变量 */
:root { --primary-color: #42b983; }
/* 组件内部使用变量 */
:host { color: var(--primary-color); }
::part伪元素样式
- 在shadow dom 中声明可穿透的元素
- 在外部使用
::part()可设置对应元素的样式
<body>
<style>
#box::part(content) {
color: red;
}
/* 无效,无法设置子元素 */
#box::part(content) p{
font-weight: bold;
}
</style>
<custom-dom id="box"></custom-dom>
<script>
class CustomDom extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
this.shadowRoot.innerHTML = `
<p>我是自定义标签</p>
<div part="content">
可穿透样式的元素
<p>子级元素</p>
</div>
<p part="content">可穿透样式的元素</p>
`;
}
}
customElements.define("custom-dom", CustomDom);
</script>
</body>

6.2 JavaScript 操作穿透
Shadow Root 访问
当 Shadow DOM 模式为 open 时,可通过 element.shadowRoot 直接访问内部 DOM 树:
const component = document.querySelector('my-element');
const innerDiv = component.shadowRoot.querySelector('.inner'); // 操作内部元素
Closed 模式破解
若组件使用 { mode: 'closed' },可通过在构造函数中保留引用实现间接访问:
class ClosedComponent extends HTMLElement {
constructor() {
super();
this._shadowRoot = this.attachShadow({ mode: 'closed' });
this._shadowRoot.innerHTML = `<p>我是自定义标签</p>`;
}
}
//外部获取保留的引用,操作内部元素
document.querySelector("my-element")._shadowRoot.querySelectorAll("p");


9405

被折叠的 条评论
为什么被折叠?



