Slot 内容分发
一、Slot 概述
1.1 什么是 Slot
Slot(插槽)是 Web Components 中的一种内容分发机制,允许在外部分别定义组件的各个部分,同时保持组件内部的封装性。Slot 可以理解为 Shadow DOM 内部的"占位符",外部内容会通过这个占位符被分发到 Shadow DOM 中渲染。
<!-- 定义组件 -->
<my-card>
<h2 slot="title">Card Title</h2>
<p slot="content">Card content goes here</p>
</my-card>
// 组件内部
class MyCard extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<div class="card">
<slot name="title"></slot>
<slot name="content"></slot>
</div>
`;
}
}
1.2 Slot 的类型
| 类型 | 描述 | 示例 |
|---|---|---|
| 命名插槽 | 有 name 属性,接收指定 slot 的内容 |
<slot name="header"> |
| 默认插槽 | 没有 name 属性的插槽,接收所有未匹配的内容 |
<slot> |
二、命名插槽
2.1 基本用法
<!-- 组件使用 -->
<user-profile>
<img slot="avatar" src="/avatar.jpg" alt="User avatar" />
<span slot="username">john_doe</span>
<span slot="bio">Software developer</span>
</user-profile>
// 组件定义
class UserProfile extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<style>
.profile { display: flex; gap: 16px; padding: 16px; }
.info { display: flex; flex-direction: column; }
</style>
<div class="profile">
<slot name="avatar"></slot>
<div class="info">
<slot name="username"></slot>
<slot name="bio"></slot>
</div>
</div>
`;
}
}
customElements.define('user-profile', UserProfile);
2.2 多个元素到同一插槽
<!-- 多个元素可以匹配到同一个命名插槽 -->
<my-list>
<li slot="item">Item 1</li>
<li slot="item">Item 2</li>
<li slot="item">Item 3</li>
</my-list>
// 组件内部
class MyList extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<ul>
<slot name="item"></slot>
</ul>
`;
}
}
三、默认插槽与备用内容
3.1 默认插槽
没有 name 属性的 <slot> 会接收所有没有指定 slot 属性的外部内容:
<my-component>
<p>Goes to default slot</p>
<p>Also default slot</p>
<p slot="named">Goes to named slot</p>
</my-component>
class MyComponent extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<slot></slot> <!-- 接收前两个 p -->
<slot name="named"></slot> <!-- 接收第三个 p -->
`;
}
}
3.2 备用内容
插槽内部的内容作为"备用内容",当没有外部内容分发时显示:
<slot name="title">
<h3>Default Title</h3>
<p>Default description</p>
</slot>
<!-- 没有提供 title 内容时,显示默认标题 -->
<my-card></my-card>
<!-- 提供了 title 内容时,默认内容被替换 -->
<my-card>
<h2 slot="title">Custom Title</h2>
</my-card>
四、slotchange 事件
4.1 基本用法
当插槽内的节点发生变化时,会触发 slotchange 事件:
class MyComponent extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `<slot></slot>`;
// 监听 slotchange
this.shadowRoot.querySelector('slot').addEventListener('slotchange', (e) => {
console.log('Slot content changed');
console.log('Assigned nodes:', e.target.assignedNodes());
});
}
}
4.2 实际应用
// 动态更新组件状态
class TagList extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<style>
:host { display: flex; flex-wrap: wrap; gap: 8px; }
.tag { padding: 4px 8px; background: #e0e0e0; border-radius: 4px; font-size: 14px; }
</style>
<slot id="tags"></slot>
`;
this.shadowRoot.getElementById('tags').addEventListener('slotchange', () => {
this.updateCount();
});
}
updateCount() {
const count = this.shadowRoot.querySelector('slot')
.assignedNodes()
.filter(n => n.nodeType === Node.ELEMENT_NODE)
.length;
console.log(`Tag count: ${count}`);
}
}
customElements.define('tag-list', TagList);
五、::slotted 伪元素
5.1 基本用法
::slotted() 伪元素允许从 Shadow DOM 内部样式化被分发到插槽的元素:
<style>
/* 匹配 slot 传入的 div 元素 */
::slotted(div) {
color: blue;
border: 1px solid blue;
}
</style>
<slot></slot>
<my-component>
<div>This will be blue and have blue border</div>
</my-component>
5.2 限制
/* ❌ 只能选中直接分配给该插槽的元素 */
/* 不能选中插槽内部 slot 的后代 */
::slotted(div p) { } /* 无效 */
/* ❌ 不能使用组合器 */
::slotted(div + span) { } /* 无效 */
/* ✅ 只能设置部分 CSS 属性 */
::slotted(div) {
color: blue; /* ✅ 有效 */
background: red; /* ✅ 有效 */
font-size: 14px; /* ✅ 有效 */
display: block; /* ❌ 无效 - 布局属性可能无效 */
}
六、assignedNodes 方法
6.1 获取分配的节点
const slot = this.shadowRoot.querySelector('slot[name="item"]');
// 获取分配给该插槽的所有节点
const nodes = slot.assignedNodes();
// 获取分配给该插槽的元素(不包括文本节点)
const elements = slot.assignedElements();
// 获取带选项的分配
const options = { flatten: true };
const flattened = slot.assignedNodes(options);
6.2 实际应用
// 访问分发内容进行操作
class Accordion extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<slot name="header"></slot>
<slot name="content"></slot>
`;
this.headerSlot = this.shadowRoot.querySelector('slot[name="header"]');
this.headerSlot.addEventListener('slotchange', () => {
this.attachHeaderListeners();
});
}
attachHeaderListeners() {
const headers = this.headerSlot.assignedElements();
headers.forEach((header, index) => {
header.addEventListener('click', () => {
this.toggle(index);
});
});
}
toggle(index) {
const contentSlot = this.shadowRoot.querySelector('slot[name="content"]');
const contents = contentSlot.assignedElements();
// 切换第 index 个内容面板
contents[index].classList.toggle('expanded');
}
}
七、面试高频问题
Q: slot 和 ::slotted 有什么区别?
回答要点:<slot> 是实际元素,作为内容分发的占位符;::slotted() 是 CSS 伪元素,用于样式化被分发到插槽的内容。Slot 定义在哪里显示外部内容,::slotted 定义如何样式化这些外部内容。
Q: 如何监听插槽内容的变化?
回答要点:通过监听 slot 元素的 slotchange 事件。但需要注意 slotchange 不会在属性变化时触发,只在节点增删时触发。如果需要监听属性变化,可能需要 MutationObserver。
Q: 备用内容和分发内容的关系是什么?
回答要点:备用内容是插槽标签内的默认内容,当没有外部内容分发到该插槽时显示。一旦有外部内容分发,备用内容被完全替换,不存在"合并"关系。