大家好,我是前端宝哥。
插槽,就像一个魔法口袋,可以让你轻松地把内容塞到组件里,不用再费劲地用 props
传递。
什么是插槽?
在 React 中,你可以用 children
属性把 JSX 代码塞进组件,但这样操作不太方便。
// 父组件
<blog-list>
<p>我的文字在这里</p>
</blog-list>
function BlogList({children}) {
return(
<div>
{children}
</div>
)
}
HTML 和其他 JavaScript 框架则用 <slot>
元素来创建占位符,你可以直接把内容塞进去,不用再用 props
传递。
而且,你还可以轻松地设置默认内容,不用额外写代码。
// FancyCard 组件
<div class=“fancy” />
<slot name="text"><p>默认文字</p></slot>
</div>
// 父组件
<FancyCard />
// 默认结果
<div class=“fancy” />
<p>默认文字</p>
</div>
你也可以直接把整个元素塞进去。
// FancyCard 组件
<div class=“fancy” />
<slot name="text"><p>默认文字</p></slot>
</div>
// 父组件
<FancyCard>
<ul slot="text">
<li><p>我的文字</p></li>
<li><p>我的文字</p></li>
</ul>
</FancyCard>
// 结果
<div class=“fancy” />
<ul>
<li><p>我的文字</p></li>
<li><p>我的文字</p></li>
</ul>
</div>
没有名字的插槽叫做默认插槽。如果你不给内容添加 slot 属性,它就会被塞到默认插槽里。用默认插槽和命名插槽,你就可以把一些内容放到指定位置,把剩下的内容放到另一个位置。
// FancyCard 组件
<div class=“fancy” />
<slot name="text"><p>默认文字</p></slot>
<slot></slot>
</div>
// 父组件
<FancyCard>
<p slot="text">这是其他文字</p>
<p>这是更多文字</p>
<button>点击我!</button>
</FancyCard>
// 结果
<div class=“fancy” />
<p>这是其他文字</p>
<p>这是更多文字</p>
<button>点击我!</button>
</div>
如果 <slot>
没有默认内容,你也没塞任何东西进去,它就什么也不会显示。
// FancyCard 组件
<div class=“fancy” />
<slot name="text"><p>默认文字</p></slot>
<slot></slot>
</div>
// 父组件 A
<FancyCard />
// 结果 A
<div class=“fancy” />
<p>默认文字</p>
</div>
Web Components 中的插槽
HTML 有 Web Components 规范,它允许开发者使用 HTML 创建组件。
创建一个使用 <slot>
的自定义元素需要三个部分。第一个是 HTML 模板。
<template id="preview">
<style>
@import "../styles.css";
</style>
<div class="blog-list-container">
<h2><slot name="title">默认标题</slot></h2>
<slot></slot>
</div>
</template>
第二个部分是使用 自定义元素 创建 JavaScript 类。在自定义元素的构造函数中,我们需要创建一个 影子 DOM 来存放我们的 <slot>
。影子 DOM API 在 DOM 中创建了一个作用域、封装的、独立的 DOM,这个 DOM 由你的 HTML 页面中的其他部分创建。为了创建一个影子 DOM 并将我们的 <slot>
放入其中,构造函数执行三个操作:
使用
getElementById
和.content
属性访问第一部分中 HTML 模板的内容,并将其分配给一个变量。在开放模式下实例化一个影子根节点,以便浏览器和页面中的其他部分可以访问其中的内容。
克隆 HTML 模板的内容,并将其作为子节点附加到影子 DOM。
class BlogPreview extends HTMLElement {
constructor() {
super();
const templateContent = document.getElementById("preview").content;
this.attachShadow({mode: "open"});
this.shadowRoot.appendChild(templateContent.cloneNode(true));
}
}
customElements.define('preview-component', Preview);
使用 <slot>
的第三部分是在 HTML 文件中调用自定义元素。
<preview-component><span slot="title">我的标题</span><p>我的描述</p></preview-component>
结果将显示我的 <span>
,其中包含我的标题文本,放在 <h2>
中。
<div class="blog-list-container">
<h2><span>我的标题</span></h2>
<p>我的描述</p>
</div>
当你检查这个自定义元素时,DevTools 将显示带有 <slot>
和你传递给组件的内容的影子根节点。
<preview-component>
#shadow-root (open)
<div class="blog-list-container">
<h2><slot name="title">default</slot></h2>
<slot></slot>
</div>
<span slot="title">我的标题</span>
<p>我的描述</p>
</preview-component>
Chrome DevTools 和 FireFox DevTools 都有内置工具,可以帮助你通过点击来查看 内容的来源。
Vue 中的插槽
Vue 插槽功能更加强大,但你也能看到它们是如何从 Web Components 规范中汲取灵感的。
默认插槽
如果你把内容放到默认的 <slot>
中,你只需要把它包裹在你的组件中。
// blog-list 组件
<template>
<div class="blog-list-container">
<h2><slot>默认</slot></h2>
</div>
</template>
// 父组件
<blog-list>
<span>我的标题</span>
</blog-list>
// 结果
<div class="blog-list-container">
<h2><span>我的标题</span></h2>
</div>
命名插槽
如果你使用命名插槽,你需要使用一个带有插槽名称的 <template>
,以 # 开头。
// blog-list 组件
<template>
<div class="blog-list-container">
<h2><slot name="title">默认</slot></h2>
</div>
</template>
// 父组件
<blog-list>
<template #title>
<span>我的标题</span>
</template>
</blog-list>
// 结果
<div class="blog-list-container">
<h2><span>我的标题</span></h2>
</div>
你也可以在默认插槽中使用这种语法。
// blog-list 组件
<template>
<div class="blog-list-container">
<h2><slot name="title">默认</slot></h2>
<slot></slot>
</div>
</template>
// 父组件
<blog-list>
<template #title>
<span>我的标题</span>
</template>
<template #default>
<p>把我放进去,教练!</p>
</template>
</blog-list>
// 结果
<div class="blog-list-container">
<h2><span>我的标题</span></h2>
<p>把我放进去,教练!</p>
</div>
作用域插槽
这里就变得有趣了。你可以从子组件传递数据到插槽。当你在父组件中使用子组件时,你就可以访问这些数据。
// counter 组件
<script setup>
const count = ref(0);
const counterMessage = count < 10 ? "继续计数!" : "干得好!"
</script>
<template>
<div class="counter-container">
<slot :number="count" :message="counterMessage"></slot>
</div>
</template>
// 父组件
<counter v-slot="slotProps">
<h2>我的计数器</h2>
<p>{{slotProps.number}}</p>
<p>{{slotProps.message}}</p>
</counter>
// 结果
<div class="counter-container">
<h2>我的计数器</h2>
<p>0</p>
<p>继续计数!</p>
</div>
你可以使用解构,这样你就不必使用 slotProps.property
,只需使用 property
。
// 父组件
<counter v-slot="{number, message}">
<span>我的计数器</span>
<p>{{number}}</p>
<p>{{message}}</p>
</counter>
这个功能在命名插槽中使用了略微不同的语法。此外,如果你在传递插槽属性时混合使用默认插槽和命名插槽,则必须使用 <template #default>
。
// counter 组件
<template>
<div class="counter-container">
<h2><slot>默认</slot></h2>
<slot name="display" :number="count" :message="counterMessage"></slot>
</div>
</template>
// 父组件
<counter>
<template #default>
<span>我的计数器</span>
</template>
<template #display="{number, message}">
<p>{{number}}</p>
<p>{{message}}</p>
</template>
</counter>
// 结果
<div class="counter-container">
<h2><span>我的计数器</span></h2>
<p>0</p>
<p>继续计数!</p>
</div>
如果你曾经使用过 Array.prototype.map() 来创建 JSX 列表,那么你就可以理解 Vue 的设计思路。你可以创建一个可扩展的列表组件,并以你想要的方式显示它。
// 列表组件
<script setup>
defineProps({
listItems: Array as PropType<String[]>
})
</script>
<template>
<ul>
<li v-for="listItem in listItems" :key="listItem.id">
<slot name="item" :item="listItem"></slot>
</li>
</ul>
</template>
// 父组件
<script setup>
const products = [
{
name: "袜子",
description: "你的双脚的奢华享受",
count: 12,
},
{
name: "帽子",
description: "戴在头上的",
count: 8
},
];
</script>
<template>
<list-component :listItems="products">
<template #item="{ item }">
<div class="product">
<h3>{{ item.name }}</h3>
<p>{{ item.description }}</p>
<span class="count">{{ item.count }}</span>
</div>
</template>
</list-component>
</template>
// 结果
<ul>
<li>
<div class="product">
<h3>袜子</h3>
<p>你的双脚的奢华享受</p>
<span class="count">12</span>
</div>
</li>
<li>
<div class="product">
<h3>帽子</h3>
<p>戴在头上的</p>
<span class="count">8</span>
</div>
</li>
</ul>
你甚至可以更进一步,创建 高阶组件 或智能组件,它们不渲染元素,只包含逻辑。Vue 将它们称为 无渲染组件。
Angular 中的插槽
Angular 借鉴了 Web Components 规范,也拥有类似的功能。你可以使用 影子 DOM API 在 Angular 中实现 视图封装模式。
Angular 也使用插槽,但它将功能称为 内容投影。它不使用 <slot>
元素,而是使用 <ng-content>
元素。
创建组件时,可以使用两种方法创建模板。
在 HTML 文件中:
// blog-list.component.html
<ng-content select="[text]"></ng-content>
或者在 @component 装饰器中:
@Component({
selector: 'blog-list'
template: `
<h2>标题</h2>
默认:
<ng-content></ng-content>
文字:
<ng-content select="[blog-text]"></ng-content>
`
})
然后,在父组件中,你就可以使用你在想要传递给它的元素上定义的 <ng-content>
选择器。
// 父组件
<blog-list>
<p blog-text>我的文字在这里</p>
</blog-list>
// 结果
<h2>标题</h2>
<p>我的文字在这里</p>
就像之前的例子一样,未命名的插槽将接收所有不使用定义的选择器传递的内容。
Angular 还提供 条件内容投影,使用 <ng-template>
。
总结
过去我在 React(以及 2013 年的 AngularJS)中工作过,我花了一些时间才开始习惯在编写 Vue 组件时使用 <slot>
而不是 props。 用 Web Components 规范手动创建一些 <slot>
并使用 Chrome DevTools 检查它们非常有帮助!
往期推荐
38个Vue、Nuxt 和 Vite 技巧、窍门和实践的合集
最后,如果你觉得宝哥的分享还算实在,就给我点个赞,关注一波。分享出去,也许你的转发能给别人带来一点启发。
关注我,明天见!
觉得好看,请关注我,点“在看”