10.2Web Component
本文参考MDN
https://developer.mozilla.org/zh-CN/docs/Web/API/Web_components
Web Component 的提出
Web Component 的概念最早可以追溯到 2011 年,由 Google 的工程师提出。其核心规范在随后的几年中逐步发展和完善。
截至当前日期(2025年10月24日),Web Component 的核心 API 已经得到了所有现代主流浏览器的广泛支持。
-
核心 API 支持:
- Custom Elements (自定义元素):V1 版本已被 Chrome、Firefox、Safari、Edge 等现代浏览器完全支持。
- Shadow DOM (影子 DOM):V1 版本同样被所有现代浏览器支持,提供了强大的样式和 DOM 封装能力。
- HTML Templates (
<template>和<slot>):这些标签也被所有现代浏览器支持,是构建可复用组件模板的基础。
Web Component 的四大核心技术
Web Component 并非单一技术,而是由四个关键的 Web API 组成:
1. 自定义元素 (Custom Elements)
这是 Web Component 的基础,它让你可以定义和注册全新的 HTML 标签。
customElements.define():用于注册一个自定义元素。- 必须包含一个连字符
-(例如my-button,user-card),这是为了避免与未来的 HTML 标准元素冲突。 - 基于 ES6 的
class语法继承HTMLElement或其子类。
示例:
// 定义一个类
class MyButton extends HTMLElement {constructor() {super();// 创建 Shadow DOMthis.attachShadow({ mode: 'open' });// 设置内部结构和样式this.shadowRoot.innerHTML = `<style>button {background-color: #007bff;color: white;border: none;padding: 10px 20px;border-radius: 5px;cursor: pointer;}button:hover {background-color: #0056b3;}</style><button><slot></slot></button>`;}// 生命周期钩子:元素被添加到文档时调用connectedCallback() {console.log('MyButton 已连接到页面');}// 生命周期钩子:元素从文档中移除时调用disconnectedCallback() {console.log('MyButton 已从页面移除');}// 监听属性变化static get observedAttributes() {return ['disabled'];}attributeChangedCallback(name, oldValue, newValue) {if (name === 'disabled') {const button = this.shadowRoot.querySelector('button');button.disabled = newValue !== null;}}
}// 注册自定义元素
customElements.define('my-button', MyButton);
使用方式:
<my-button disabled>点击我</my-button>
2. 影子 DOM (Shadow DOM)
这是实现封装性的核心技术。影子 DOM 允许你将一个隐藏的、独立的 DOM 树附加到一个元素上,这个树与主文档的 DOM 是隔离的。
- 作用域隔离:影子 DOM 内部的 CSS 样式不会影响外部页面,外部的样式也不会穿透进来(除非使用
::part或::slotted())。 - DOM 隔离:影子 DOM 内部的元素无法被外部的 JavaScript 直接访问(除非通过
shadowRoot)。 - 模式 (mode):
open:可以通过element.shadowRoot访问。closed:无法通过 JavaScript 访问(实际应用中很少用,因为有绕过的方法)。
3. HTML 模板 (<template> 和 <slot>)
<template>:用于声明一段在页面加载时不会渲染的 HTML 模板。你可以克隆这段模板并插入到 DOM 中。非常适合存放组件的结构。<slot>:插槽机制,允许你在自定义元素的内部预留位置,让用户传入内容进行填充。实现了内容分发。
示例模板:
<template id="user-card-template"><style>.card { border: 1px solid #ccc; padding: 16px; }.name { font-weight: bold; }</style><div class="card"><div class="name"><slot name="name">默认姓名</slot></div><div class="email"><slot name="email">默认邮箱</slot></div></div>
</template>
在 JS 中克隆模板并插入影子 DOM 即可。
4. 生命周期回调 (Lifecycle Callbacks)
自定义元素类中可以定义一些特殊的回调函数,在元素生命周期的不同阶段自动执行:
connectedCallback():元素被插入到 DOM 时调用。disconnectedCallback():元素从 DOM 中移除时调用。adoptedCallback():元素被移动到新文档时调用(如 iframe)。attributeChangedCallback(attrName, oldVal, newVal):当观察的属性发生变化时调用。需要配合observedAttributes静态 getter 使用。
完整例子
准备工作
你只需要一个文本编辑器(如 VS Code)和一个现代浏览器(Chrome/Firefox/Safari/Edge)。
创建以下文件结构:
web-component-demo/
├── index.html
└── fancy-button.js
📚 案例:创建一个“炫酷按钮”组件 <fancy-button>
我们来创建一个带有渐变背景、圆角、悬停动画的按钮,它支持自定义颜色,并且可以像普通按钮一样插入文字。
第一步:创建 JavaScript 文件 (fancy-button.js)
这是组件的核心逻辑。
// fancy-button.js// 1. 定义一个类,继承自 HTMLElement
class FancyButton extends HTMLElement {// 构造函数:组件创建时调用constructor() {super(); // 必须调用 super()// 2. 创建 Shadow DOM 并附加到当前元素// mode: 'open' 表示可以通过 JS 访问 shadowRootthis.attachShadow({ mode: 'open' });// 3. 定义组件的内部结构(HTML)和样式(CSS)// 我们使用模板字符串来写this.shadowRoot.innerHTML = `<style>/* 这里的样式只作用于组件内部,不会影响页面其他部分 */.button {background: linear-gradient(45deg, #667eea 0%, #764ba2 100%);color: white;border: none;padding: 12px 24px;font-size: 16px;border-radius: 25px;cursor: pointer;box-shadow: 0 4px 15px rgba(0,0,0,0.2);transition: all 0.3s ease;outline: none;}.button:hover {transform: translateY(-2px);box-shadow: 0 7px 20px rgba(0,0,0,0.3);}.button:active {transform: translateY(0);}/* 如果组件有 primary 属性,应用主色调 */:host([primary]) .button {background: linear-gradient(45deg, #f093fb 0%, #f5576c 100%);}/* 如果组件有 small 属性,应用小尺寸 */:host([small]) .button {padding: 8px 16px;font-size: 14px;border-radius: 20px;}</style><!-- 注意:这里使用了 <slot> --><button class="button"><slot></slot> <!-- 这里会显示用户写在 <fancy-button> 标签之间的内容 --></button>`;}// 生命周期钩子:当元素被添加到页面时调用connectedCallback() {console.log('FancyButton 已加载到页面!');// 这里可以添加事件监听等逻辑// 例如:this.shadowRoot.querySelector('button').addEventListener('click', ...)}
}// 4. 注册自定义元素
// 第一个参数是标签名,必须包含连字符
// 第二个参数是类
customElements.define('fancy-button', FancyButton);
代码详解:
class FancyButton extends HTMLElement:创建一个类,继承原生 HTML 元素的能力。super():必须调用父类构造函数。this.attachShadow({ mode: 'open' }):创建影子 DOM,实现样式和 DOM 的封装。this.shadowRoot.innerHTML = ...:在影子 DOM 中写入 HTML 结构和 CSS 样式。样式不会“泄漏”出去,也不会被外部样式影响。<slot></slot>:这是一个“插槽”,它会自动显示你在<fancy-button>内容</fancy-button>标签之间写的内容。:host([primary])::host选择器指向自定义元素本身。[primary]表示当元素有primary属性时,应用后面的样式。这是通过属性控制样式的技巧。customElements.define():将类注册为一个可以在 HTML 中使用的标签。
第二步:创建 HTML 页面 (index.html)
现在我们来使用刚刚创建的组件。
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0"/><title>Web Component 入门案例</title><!-- 5. 引入组件的 JavaScript 文件 --><script type="module" src="./fancy-button.js"></script><style>body {font-family: Arial, sans-serif;padding: 40px;background-color: #f0f2f5;}h1 {color: #333;}/* 测试外部样式是否会干扰组件 */button {background: red !important; /* 不会生效!因为组件内部是隔离的 */}</style>
</head>
<body><h1>Web Component 入门案例</h1><p>下面是一些使用 <code><fancy-button></code> 的例子:</p><!-- 6. 直接使用自定义标签 --><fancy-button>普通按钮</fancy-button><br /><br /><!-- 使用 primary 属性 --><fancy-button primary>主色调按钮</fancy-button><br /><br /><!-- 使用 small 属性 --><fancy-button small>小按钮</fancy-button><br /><br /><!-- 组合使用属性 --><fancy-button primary small>小的主色调按钮</fancy-button><br /><br /><!-- 插槽可以插入任何内容,不仅仅是文字 --><fancy-button><strong>加粗的</strong> 按钮内容</fancy-button><br /><br /><!-- 对比:原生按钮 --><button>原生按钮 (样式被隔离,不受影响)</button>
</body>
</html>
代码详解:
<script type="module" src="./fancy-button.js"></script>:使用type="module"加载 JS 文件。这是现代浏览器加载 ES6 模块的方式,确保脚本在 DOM 解析完成后执行,避免注册顺序问题。<fancy-button>内容</fancy-button>:直接像使用普通 HTML 标签一样使用你的自定义组件!primary和small:通过添加属性来改变按钮的外观,这得益于:host()选择器。<strong>加粗的</strong>:展示了<slot>的强大,它可以分发任何 HTML 内容。
Web Component 的魅力在于“原生”和“可复用”。一旦你创建了一个组件,它就可以在任何现代浏览器的任何项目中使用,无论项目是用 React、Vue 还是纯 HTML 构建的!
