Kodokon kodokon.com

深入 Web Components:自定义元素、shadow DOM 与插槽

掌握自定义元素的生命周期、shadow DOM 的封装,以及通过插槽(slot)进行的内容投影。

12 分钟 · 3 题

在 Kodokon 中打开本课

自定义元素(custom elements)扩展了 HTML 的词汇表:任何继承自 HTMLElement 的类都可以注册到 CustomElementRegistry 中,只要它的名字包含一个连字符(user-badge,绝不能是 userbadge)。有个微妙之处:如果该标签在 customElements.define 被调用之前就出现在文档中,它会先被创建为一个未知元素,然后就地被升级(upgraded) —— 这时构造函数会在一个已经存在于树中的节点上被事后调用。

JAVASCRIPT
class UserBadge extends HTMLElement {
  static observedAttributes = ["name"];
  connectedCallback() {
    this.render();
  }
  attributeChangedCallback() {
    this.render();
  }
  render() {
    this.textContent = this.getAttribute("name");
  }
}
customElements.define("user-badge", UserBadge);
一个具备完整生命周期的最简自定义元素。

生命周期藏着一些陷阱:attributeChangedCallback 只会为 observedAttributes 中列出的属性触发,而在升级期间,它会在 connectedCallback 之前为每一个已存在的属性被调用。规范禁止构造函数检查属性和子节点,也禁止添加任何内容:在它运行的那一刻(通过 createElement 或克隆),元素可能完全是空的。

HTML
<template id="card-tpl">
  <style>
    :host { display: block; padding: 1rem; }
    ::slotted(h2) { margin: 0; }
  </style>
  <slot name="title"></slot>
  <slot></slot>
</template>
一个惰性的模板:会被解析,但既不执行也不渲染。

attachShadow 会创建一棵封装的子树:样式不会泄漏出去,页面的选择器也无法伸入其中 —— 只有可继承的 CSS 属性(color、font)和自定义属性能跨越这条边界。<template> 的内容存放在一个通过 .content 访问的 DocumentFragment 中:在你把它克隆进文档之前,其中的脚本不会运行、图片也不会加载。放在 light DOM 中的节点会被投影(projected)<slot> 元素里:它们在物理上仍留在 light DOM 中,而 slotchange 事件会在分配发生任何变化时通知你。

JAVASCRIPT
class InfoCard extends HTMLElement {
  constructor() {
    super();
    const tpl = document.getElementById("card-tpl");
    this.attachShadow({ mode: "open" })
      .appendChild(tpl.content.cloneNode(true));
  }
}
customElements.define("info-card", InfoCard);
把模板克隆进一个 open 模式的 shadow root。

有两个扩展值得你留意。表单关联自定义元素(form-associated custom elements):借助 static formAssociated = trueattachInternals(),你的组件能完整地参与表单(提交值、原生校验、:invalid 状态)。以及声明式 shadow DOM<template shadowrootmode=open> 会在解析时附加一个 shadow root,无需 JavaScript,这让 web 组件与服务端渲染兼容。

知识检测

确认你已牢记本课的重点内容。

  1. 为什么自定义元素的构造函数既不能读取它的属性,也不能创建子节点?
    • 因为属性在 connectedCallback 之前被冻结为只读
    • 因为构造期间 shadow DOM 会隐藏 light DOM
    • 因为在那一刻(createElement、克隆),元素可能是空的;规范要求构造函数对内容不产生任何影响
  2. 使用 attachShadow({ mode: 'closed' }) 时,从外部访问 element.shadowRoot 会返回什么?
    • null:只有 attachShadow 返回的引用才能访问 shadow root
    • shadow root,但是只读的
    • 一个空的 DocumentFragment
    • 抛出一个 DOMException
  3. 被投影进 <slot> 的节点在物理上存放在哪里?
    • 它们在渲染时被移动到 shadow DOM 中
    • 它们留在 light DOM 中;插槽只是在扁平化树中它所在的位置上显示它们
    • 它们被克隆:原件留在 light DOM 中,副本存放在 shadow DOM 中