1. Slot API核心概念:插槽定义、命名与默认内容、作用域

Slot(插槽)是组件化框架中实现内容分发与组合的核心机制。在 Material 3 生态中,无论是基于 Lit 的 @material/web 库,还是 Vue / React 的二次封装,插槽都承担着“定义组件可变内容区域”的角色。本章从底层原理出发,依次解析插槽的三种基本形态:默认插槽、命名插槽和作用域插槽,并给出对应代码实现与常见调试技巧。

1.1 插槽的三要素定义

概念含义使用场景
默认插槽组件模板中未指定 name 的 <slot> 元素,接收所有未匹配的投影内容按钮文本、卡片正文、弹窗主体
命名插槽通过 name 属性标记的 <slot name="xxx">,只接收同名的 slot="xxx" 投影内容弹窗标题/脚注、列表项前缀/后缀
作用域插槽允许子组件将内部数据通过 slot 属性暴露给父级模板,父级可自定义渲染逻辑表格行自定义、下拉选项格式化、动态列表

1.2 插槽的实现流程

以 Lit 组件(对应 Material 3 Web Components 官方实现)为例,定义与使用插槽分为四步:

  1. 声明插槽:在组件的 render 模板中使用 <slot></slot>(默认)或 <slot name="title"></slot>(命名)。
  2. 提供默认内容:在 <slot> 标签内部写入回退元素,当父级未投影时显示。
  3. 投影内容:父级使用组件时,将子元素或模板放置在组件标签内,并通过 slot="xxx" 属性匹配命名的插槽。
  4. 作用域传递:在子组件中通过 @slotchange 事件捕获插槽变化,或使用 Lit 的 map + html 动态渲染具名插槽数据;在 Vue 中使用 v-slot 指令接收作用域对象。

1.3 代码示例:三种插槽的定义与使用

以下基于 Lit 3.x 实现一个 Material 3 风格的 Card 组件,完整演示默认、命名与作用域插槽。

// card.ts - 使用 Lit 定义的 Material 3 Card 组件
import { LitElement, html, css } from 'lit';
import { customElement } from 'lit/decorators.js';

@customElement('m3-card')
export class M3Card extends LitElement {
  static styles = css`
    :host { display: block; padding: 16px; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
    .header { font-size: 1.2em; margin-bottom: 8px; }
    .footer { margin-top: 16px; border-top: 1px solid #eee; padding-top: 8px; }
  `;

  // 默认插槽 + 命名插槽 + 作用域数据
  render() {
    return html`
      <div class="header"><slot name="title">默认标题</slot></div>
      <div class="body"><slot>默认正文内容</slot></div>
      <div class="footer">
        <slot name="actions">
          <!-- 提供默认操作按钮 -->
          <button>确定</button>
        </slot>
      </div>
      <!-- 作用域插槽通过属性暴露内部数据 -->
      <slot name="item" .data=${{ id: 1, label: '示例' }}></slot>
    `;
  }
}

父级使用组件并投影内容:

<m3-card>
  <span slot="title">产品说明</span>
  <p>这是一张基于 Material 3 的卡片,内容由父级提供。</p>
  <button slot="actions">取消</button>

  <!-- 作用域插槽:父级可通过 slot 获取子组件暴露的数据 -->
  <template slot="item" slot-scope="data">  <!-- 注:Lit 原生不支持 slot-scope,此为示意;实际需配合 @slotchange 或 Lit 的 repeat 指令 -->
    <span>${data.id} - ${data.label}</span>
  </template>
</m3-card>
提示: Lit 的原生 slot 不支持作用域插槽语法。实际 Material 3 组件库(如 @material/web)通常通过属性绑定或事件回调来实现类似作用域的效果。在 Vue 中则直接使用 v-slot 指令,写法更简洁。

1.4 作用域插槽的常见实现方式

在不同框架中,作用域插槽的实现路径有所差异:

框架/库实现方式示例
Vue 3v-slot:item="{ data }"<template v-slot:item="{ id }">{{ id }}</template>
Lit通过 @slotchange 事件获取插槽元素,或使用 map+html 动态渲染this.shadowRoot.querySelector('slot[name="item"]')?.assignedNodes()
React使用 render props 模式:组件的 props 接收一个函数,由子组件调用并传入数据<Card renderItem={(id) => <span>{id}</span>} />
实现关键: 无论框架如何变化,作用域插槽的本质都在于“子组件将内部数据暴露给父级的模板作用域”,父级获得数据后可以决定如何渲染。理解这一点后,在任何框架中都能快速切换。

1.5 调试与常见错误

  • 错误:插槽未被渲染——检查父级投影元素是否包含匹配的 slot 属性,注意属性名大小写(推荐全小写)。
  • 错误:默认内容不显示——确认 <slot> 标签内确实写入了子元素,并且父级没有提供相同名称的投影内容(空内容也算)。
  • 错误:作用域数据未传递——在 Lit 中,直接写 <slot .data=${{...}}> 并不会自动暴露到父级作用域,需要配合 @slotchange 或使用 Lit 的 repeat 指令手动遍历。
  • 调试技巧:在浏览器开发者工具中查看 Shadow DOM 的 slot 节点,观察 assignedNodes 列表,可以快速定位投影内容是否匹配。

1.6 Slot 结构关系图

下图直观展示了 Material 3 组件中默认插槽、命名插槽和作用域插槽的协作关系:

Material 3 组件插槽结构 父级模板 <m3-card> (组件 Shadow DOM) slot name="title" 默认标题 默认 slot (接收未命名投影内容) 默认正文 slot name="actions" 默认按钮 slot name="item" (带数据) 投影内容 具名+默认+作用域
注意: 上图作用域插槽的“带数据”仅为概念示意。在实际 Lit 组件中,作用域数据通常通过 @slotchange 事件或组件属性向父级传递,而非直接在 <slot> 标签上挂载。请结合具体框架的 API 文档验证。

1.7 默认内容的最佳实践

为插槽提供合理的默认内容能提升组件的即用性。原则:

  • 默认文本应简洁,避免占位符过长影响布局。
  • 当默认内容包含交互元素(如按钮)时,务必绑定正确的事件处理函数。
  • 在 Material 3 中,默认内容应遵循组件的语义角色:例如按钮默认显示“确定”,弹窗默认无标题则显示空字符串。