影子根是附加到宿主元素的封装 DOM 子树:其内部样式不会泄露,页面样式也不会泄露(继承的属性除外)。模板可高效构建树;插槽可将 Light DOM 内容投影到树中。
附加 shadow root
this.attachShadow({ mode: 'open', delegatesFocus: true });
delegatesFocus: true 会使聚焦宿主将焦点移至 Shadow Tree 内的第一个可聚焦元素,将 :focus 样式路由到宿主,并在与表单关联的自定义元素 (static formAssociated = true) 上将 <label> 点击转发到内部控件。每当组件封装可聚焦控件时,请设置此属性。在几乎所有情况下,都应使用 mode: 'open',以便开发者工具、页面脚本和服务器水合可以访问 element.shadowRoot(请参阅 Web 组件方向)。
模板:高效构建树
一次性定义 <template>(模块范围或静态字段),并为每个实例克隆它。克隆已解析的 fragment 比在每次构造时重新解析 innerHTML 字符串要快得多。
const template = document.createElement('template');
template.innerHTML = `
<div class="card">
<slot name="header"></slot>
<div class="body"><slot></slot></div>
<slot name="footer"></slot>
</div>
`;
class MyCard extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
// MANDATORY: deep-clone the template content so each instance gets its own nodes.
this.shadowRoot.append(template.content.cloneNode(true));
}
}
customElements.define('my-card', MyCard);
保持模板精简。深度嵌套的“div 汤”和多层嵌套的 shadow root 都会增加可测量的样式解析和布局成本,从而对大规模性能造成重大影响。优先选择扁平结构和语义元素。
槽位:令人惊喜的部分
默认槽和具名槽的工作方式与文档中所述一致,但以下行为可能会带来困扰:
- 空格也算作字符。元素之间的文本节点(包括纯空白字符)会被分配到默认槽,因此默认槽很少真正“空置”。
- 系统会保留 DOM 顺序,而不是槽顺序。无论填充哪个槽,投影节点都会出于
:first-child/:last-child目的保留其原始 Light DOM 顺序。::slotted(*:first-child)规则定位到第一个 DOM 子级,而不是该槽位的第一个子级,因此通常不会与您预期的内容匹配。 slotchange是升级时间信号。当分配的节点发生变化(包括初始分配)时,它会触发,并且是可靠的方法,可用于在升级后(当它们在connectedCallback期间可能不存在时)了解 Light DOM 子节点已到达。
this.shadowRoot.querySelector('slot[name="header"]')
.addEventListener('slotchange', (e) => {
const nodes = e.target.assignedElements();
this.toggleAttribute('has-header', nodes.length > 0);
});
将样式表加载到 shadow root 中
| 方法 | 优点 | 缺点 |
|---|---|---|
模板中的 <style> |
简单、完全封装、无需 JS 即可运行。 | 每个实例都会重复,大规模使用时会使 DOM 膨胀。 |
<link rel="stylesheet"> |
可缓存的外部文件。 | 额外请求;存在无样式内容闪烁的风险。 |
adoptedStyleSheets |
所有实例共享一个 CSSStyleSheet;大规模运行时速度最快。 |
需要 JS;构造的样式表不支持 @import。 |
对于一个组件的多个实例,最好使用共享的构造样式表:
const sheet = new CSSStyleSheet();
sheet.replaceSync(`:host { display: block; }`);
class MyCard extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
// The same sheet object is reused by every instance; no per-instance parse.
this.shadowRoot.adoptedStyleSheets = [sheet];
}
}