学完这篇,你能用浏览器自带的 API 定义自己的 HTML 标签,不装任何框架就把组件封装起来。
第一步:先认清这是三样东西拼起来的
Web Components 不是单个 API,而是三块积木:
- Custom Elements:注册自己的标签,比如
<my-badge>
- Shadow DOM:给组件一套独立的样式作用域
- Template / Slot:写结构模板,留插槽给外部塞内容
它们都是浏览器原生能力,不需要构建工具,写完刷新页面就能跑。
第二步:注册一个最小的自定义元素
新建 badge.js:
class MyBadge extends HTMLElement {
connectedCallback() {
this.textContent = 'Hello';
}
}
customElements.define('my-badge', MyBadge);
页面里引入脚本后直接写:
<my-badge></my-badge>
浏览器会把它当普通标签解析,然后调用你的 connectedCallback。
注意:标签名必须包含连字符(my-badge 合法,mybadge 会直接报错),也不能叫 font-face 这类保留名。另外自定义元素不能写成自闭合的 <my-badge />,必须写完整闭合标签。
第三步:用 Shadow DOM 隔离样式
attachShadow 之后,组件内部的 CSS 不会漏出去,外面的样式也进不来:
class MyBadge extends HTMLElement {
constructor() {
super();
const root = this.attachShadow({ mode: 'open' });
root.innerHTML = `
<style>
.badge { background:#eef; padding:2px 8px; border-radius:10px; }
</style>
<span class="badge"><slot></slot></span>
`;
}
}
用法变成 <my-badge>新手上路</my-badge>,文字会通过 <slot> 显示。
注意:mode: 'open' 时外部能用 el.shadowRoot 访问内部;mode: 'closed' 只是让 shadowRoot 返回 null,它不是安全边界,别拿它藏敏感逻辑。
第四步:用 template 复用结构
结构长了就抽到 <template> 里,避免每次实例化都拼字符串:
<template id="badge-tpl">
<style>.badge{background:#eef;padding:2px 8px;border-radius:10px}</style>
<span class="badge"><slot name="text">默认文案</slot></span>
</template>
const tpl = document.getElementById('badge-tpl');
class MyBadge extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' })
.appendChild(tpl.content.cloneNode(true));
}
}
注意:必须 cloneNode(true)。直接 append tpl.content 会把模板本身搬走,第二个实例就空了。
第五步:让属性驱动渲染
想让 <my-badge type="warn"> 能改样式,需要声明监听哪些属性:
static get observedAttributes() { return ['type']; }
attributeChangedCallback(name, oldVal, newVal) {
if (name === 'type' && this.shadowRoot) {
this.shadowRoot.querySelector('.badge').dataset.type = newVal;
}
}
注意:attributeChangedCallback 会在元素升级时,对每个已存在的属性各触发一次,而这时 constructor 可能还没跑完。所以回调里先判断 this.shadowRoot 存不存在,别直接 querySelector 报 null 错。
第六步:记住生命周期顺序
constructor → attributeChangedCallback → connectedCallback(插入文档)→ disconnectedCallback(移出文档)→ adoptedCallback(跨文档移动)。
注意:constructor 里不要添加子节点、不要设属性,规范会直接抛错。DOM 相关的初始化统一放 connectedCallback,并且要能容忍被多次调用(元素被移出再插回就会再来一次)。
第七步:清楚它的短板再决定用不用
- 服务端渲染(SSR)不友好,首屏是空标签,得等 JS 执行
- Shadow DOM 内的内容对部分老爬虫不友好,但 Slot 分发的内容可以正常抓取
- 要做表单控件得实现
formAssociated,比普通元素麻烦不少
- 优点也很实在:无依赖、样式天然隔离、跨框架通用,微前端和内部组件库场景特别好用
小结
- 三块积木:Custom Elements 管标签、Shadow DOM 管隔离、Template/Slot 管结构
- 标签名必须带连字符,不能自闭合
attachShadow 后样式双向隔离,closed 不等于安全
- 模板要
cloneNode(true) 再插入
- 属性监听靠
observedAttributes + attributeChangedCallback,回调里先判空
- 初始化放
connectedCallback,别塞进 constructor
- 它不解决 SSR 和表单集成,适合内部组件库、嵌入式挂件这类场景