⭐ 推荐:社区规则条款 V1.0

template 和 slot 配合 Web Components

满月居于空
满月居于空 版主新人站长
发布于 2026-10-13 12:25 ·1 浏览 ·0 回复
内容摘要

讲解 template 与 slot 在 Web Components 中的分工与配合:template 定义内部结构,slot 接收外部内容,并通过最小 user-card 示例说明插槽分配、样式隔离及 ::slotted() 的适用条件

读完这篇,你能从零写出一个用 <template> 定义内部结构、用 <slot> 接收外部内容的 Web Component,并且搞清楚插槽内容为什么不受组件内部样式影响、什么时候该用 ::slotted()。

第一步:先分清 template 和 slot 各自负责什么

这一步要建立的心智模型是:<template> 负责"组件长什么样",<slot> 负责"外部内容往哪儿塞"。

<template> 元素是一块惰性 HTML。浏览器会解析它,但内容不会渲染、图片不会加载、脚本不会执行。它的内容挂在 template.content 属性上,是一个 DocumentFragment。

<slot> 元素只在 Shadow DOM 里有效,它是个占位符,作用是告诉浏览器:这个位置显示宿主元素(light DOM)里的哪个子节点。

两者配合起来的流程是:<template> 里写好结构和 <slot> 位置 → 挂到 shadow root → light DOM 里的子元素被"分配"到对应插槽显示。

注意:<slot> 写在普通文档里是没用的,它不会显示任何东西,也不会把兄弟节点吸进去。只有在 Shadow DOM 树里才具备分发能力。

第二步:搭一个最小可运行的自定义元素

这一步要做出一个能跑起来的骨架,做完你能在页面里看到一个带边框的卡片。

新建 index.html,完整内容如下:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8">
  <title>user-card demo</title>
</head>
<body>

<template id="user-card-tpl">
  <style>
    :host {
      display: block;
      border: 1px solid #ddd;
      border-radius: 8px;
      padding: 12px 16px;
      font-family: system-ui, sans-serif;
    }
  </style>
  <div class="name"><slot name="name">匿名用户</slot></div>
  <div class="body"><slot></slot></div>
</template>

<user-card>
  <span slot="name">张三</span>
  <p>前端工程师,主要写后台管理系统。</p>
  <p>业余时间折腾浏览器 API。</p>
</user-card>

<script>
  class UserCard extends HTMLElement {
    constructor() {
      super();
      const tpl = document.getElementById('user-card-tpl');
      const shadow = this.attachShadow({ mode: 'open' });
      shadow.appendChild(tpl.content.cloneNode(true));
    }
  }
  customElements.define('user-card', UserCard);
</script>

</body>
</html>

几个硬性规则:

  • 自定义元素名必须包含连字符(user-card、my-button),不能是 usercard 这种单词。
  • 同一个名字只能 customElements.define 一次,重复注册会抛 NotSupportedError。
  • attachShadow({ mode: 'open' }) 让 element.shadowRoot 可访问;用 'closed' 则外部拿不到,调试会变麻烦,新手建议先用 'open'。

注意:这里必须写 tpl.content.cloneNode(true),那个 true 是深拷贝。如果直接 shadow.appendChild(tpl.content),节点会被从 template 里搬走,页面上第二个 <user-card> 就会变成空白。

第三步:理解插槽的分配规则

这一步要弄明白"哪些元素会进哪个插槽",做完你能准确预测页面上每个节点的归属。

规则只有两条:

  1. 带 slot="xxx" 属性的子元素,进入 name="xxx" 的具名插槽。
  2. 没有 slot 属性的子元素,进入没有 name 的默认插槽,按文档顺序排列。

在刚才的例子中:<span slot="name">张三</span> 进 <slot name="name">,两个 <p> 进 <slot>。所以第一行显示"张三",下面两行显示两段介绍。

注意:只有宿主元素的直接子元素才会被分配。写成 <user-card><div><span slot="name">张三</span></div></user-card>,那个 span 不会被分配,因为它的父节点是 div 而不是宿主。

注意:同一个 name 可以对应多个元素,它们会按顺序全部进入那个插槽,不会报错也不会覆盖。

第四步:给插槽准备默认内容

这一步要解决"使用者什么都不传时页面不能开天窗"的问题,做完你能给每个插槽兜底。

写法就是在 <slot> 标签中间放内容:<slot name="name">匿名用户</slot>。

行为要点:只要有任何元素被分配进这个插槽,fallback 内容就完全消失,不是两个叠加显示。默认插槽同理,<slot>暂无内容</slot> 在有人塞东西进去时就只显示塞进去的东西。

第五步:控制组件内部样式和插槽样式

这一步要搞清楚样式的作用边界,做完你就知道为什么改了 CSS 却不好使。

三件事分清楚:

1. :host 选择器指向宿主元素本身,可以写 :host { display: block; },也可以用属性选择器写主题:

:host([theme="dark"]) {
  background: #1f1f1f;
  color: #eee;
}

2. ::slotted() 只能选中被分配的顶层元素:

::slotted(span[slot="name"]) {
  font-weight: 700;
  color: #1a73e8;
}
::slotted(p) {
  margin: 4px 0;
}

3. 外部文档的样式依然会作用于插槽里的节点。因为这些节点物理上还在 light DOM 树里,只是被"投影"到 shadow DOM 中渲染。所以使用方写 user-card p { color: red } 是生效的。

注意:::slotted(div p) 这种带后代的选择器是无效的,规范只允许 ::slotted() 里写复合选择器匹配顶层元素。想影响插槽内容的子孙节点,只能靠可继承属性(color、font-family、line-height 等)穿透,或者让使用方自己写样式。

第六步:用 slotchange 监听插槽内容变化

这一步要拿到"插槽里现在到底有什么",做完你能做数据统计、空状态切换之类的逻辑。

class UserCard extends HTMLElement {
  constructor() {
    super();
    const shadow = this.attachShadow({ mode: 'open' });
    shadow.appendChild(
      document.getElementById('user-card-tpl').content.cloneNode(true)
    );
    this._nameSlot = shadow.querySelector('slot[name="name"]');
  }

  connectedCallback() {
    this._onSlotChange = () => {
      const nodes = this._nameSlot.assignedNodes();
      const els = this._nameSlot.assignedElements();
      console.log('原始节点', nodes, '仅元素', els);
    };
    this._nameSlot.addEventListener('slotchange', this._onSlotChange);
  }

  disconnectedCallback() {
    this._nameSlot.removeEventListener('slotchange', this._onSlotChange);
  }
}

assignedNodes() 返回分配到该插槽的节点;传 { flatten: true } 会把插槽套插槽的情况展平。assignedElements() 是它的元素版本,过滤掉了文本节点。

注意:页面首次解析时,如果插槽里已经有内容,slotchange 也会触发一次。但在 constructor 里监听可能赶不上这次触发,所以监听写在 connectedCallback 里更稳。

注意:不要在 constructor 里读 this.getAttribute('xxx') 来决定渲染结果。元素被解析器创建时是同步升级的,那一刻属性可能还没解析完。要读属性请放到 connectedCallback,或者用 static get observedAttributes() + attributeChangedCallback。

第七步:用属性驱动内容,配合多个 template

这一步把组件从"静态壳子"变成"能跟数据联动",做完你可以用属性控制组件行为。

版权声明:本文来自 GJ站长论坛《template 和 slot 配合 Web Components》
原文链接:https://www.gj0.com/thread-1815.html
转载请注明出处并保留本声明;内容仅代表作者观点,与本站立场无关。若本文涉嫌侵权,请联系本站处理。
—/ 5暂无评分
还没有评分,来打第一分吧

全部回复 0

还没有回复,来抢沙发~