插槽
一.前言
在之前的章节中,我们已经了解到组件能够接收任意类型的 JavaScript 值作为 props,但组件要如何接收模板内容呢?在某些场景中,我们可能想要为子组件传递一些模板片段,让子组件在它们的组件中渲染这些片段。
举例来说,这里有一个 <FancyButton> 组件,可以像这样使用:
<!-- 这里是父组件 插入数据-->
<FancyButton>
Click me! <!-- 插槽内容 -->
</FancyButton>
而 <FancyButton> 的模板是这样的:
<!-- 这里是子组件 接收数据-->
<button class="fancy-btn">
<slot></slot> <!-- 插槽出口 -->
</button>
元素是一个插槽出口 (slot outlet),标示了父元素提供的插槽内容 (slot content) 将在哪里被渲染。
最终渲染出的 DOM 是这样:
<!-- 这里是父组件中最终渲染出来的DOM-->
<button class="fancy-btn">Click me!</button>
Tips: 在学习插槽的时候我们经常会把父子组件搞混,一定要明确,是
父组件向子组件插入内容。
二.基础
2.1 默认内容
在外部没有提供任何内容的情况下,可以为插槽指定默认内容。比如有这样一个 <SubmitButton> 组件:
<button type="submit">
<slot></slot>
</button>
如果我们想在父组件没有提供任何插槽内容时在 <button> 内渲染“Submit”,只需要将“Submit”写在 <slot> 标签之间来作为默认内容:
<button type="submit">
<slot>
Submit <!-- 默认内容 -->
</slot>
</button>
现在,当我们在父组件中使用 <SubmitButton> 且没有提供任何插槽内容时:
<SubmitButton></SubmitButton>
“Submit”将会被作为默认内容渲染:
<!-- 这里是父组件中最终渲染出来的DOM-->
<button type="submit">Submit</button>
但如果我们提供了插槽内容:
<SubmitButton>Save</SubmitButton>
那么被显式提供的内容会取代默认内容:
<!-- 这里是父组件中最终渲染出来的DOM-->
<button type="submit">Save</button>
2.2 具名插槽
有时在一个组件中包含多个插槽出口是很有用的。举例来说,在下面这个 <BaseLayout> 组件中,有三个地方需要插入内容。
对于这种场景,<slot> 元素可以有一个特殊的 attribute name,用来给各个插槽分配唯一的 ID,以确定每一处要渲染的内容:
<!-- 这里是子组件 接收数据-->
<div class="container">
<header>
<!-- 标题内容放这里 -->
<slot name="header"></slot>
</header>
<main>
<!-- 主要内容放这里 -->
<slot></slot>
</main>
<footer>
<!-- 底部内容放这里 -->
<slot name="footer"></slot>
</footer>
</div>
这类带 name 的插槽被称为具名插槽 (named slots)。细心的你发现了第二个插槽没给名字,是的,没有提供 name 的 <slot> 出口会隐式地命名为“default”。
在父组件中使用这个组件的时候,我们需要一种方式将多个插槽内容传入到各自目标插槽的出口。此时就需要用到具名插槽了:
要为具名插槽传入内容,我们需要使用一个含 v-slot 指令的 <template> 元素,并将目标插槽的名字传给该指令:
<!-- 这里是父组件 插入数据-->
<BaseLayout>
<template v-slot:header>
<!-- header 插槽的内容放这里 -->
</template>
</BaseLayout>
v-slot 有对应的简写 #,因此 <template v-slot:header> 可以简写为 <template #header>。其意思就是“将这部分模板片段传入子组件的 header 插槽中”。
下面我们给出完整的、向 <BaseLayout> 传递插槽内容的代码,指令均使用的是缩写形式:
<BaseLayout>
<template #header>
<h1>Here might be a page title</h1>
</template>
<template #default>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
</template>
<template #footer>
<p>Here's some contact info</p>
</template>
</BaseLayout>
当一个组件同时接收默认插槽和具名插槽时,所有位于顶级的非 <template> 节点都被隐式地视为默认插槽的内容。所以上面也可以写成:
<BaseLayout>
<template #header>
<h1>Here might be a page title</h1>
</template>
<!-- 隐式的默认插槽 -->
<p>A paragraph for the main content.</p>
<p>And another one.</p>
<template #footer>
<p>Here's some contact info</p>
</template>
</BaseLayout>
现在 <template> 元素中的所有内容都将被传递到相应的插槽。最终渲染出的 HTML 如下:
<div class="container">
<header>
<h1>Here might be a page title</h1>
</header>
<main>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
</main>
<footer>
<p>Here's some contact info</p>
</footer>
</div>
三.深入
3.1 渲染作用域
插槽内容可以访问到父组件的数据作用域,因为插槽内容本身是在父组件模板中定义的。举例来说:
<!-- 这里是父组件 插入数据-->
<span>{{ message }}</span>
<FancyButton>{{ message }}</FancyButton>
这里的两个 {{ message }} 插值表达式渲染的内容都是一样的。
插槽内容无法访问子组件的数据。Vue 模板中的表达式只能访问其定义时所处的作用域,这和 JavaScript 的词法作用域规则是一致的。换言之:
父组件模板中的表达式只能访问父组件的作用域;子组件模板中的表达式只能访问子组件的作用域。
3.2 动态插槽名
动态指令参数在 v-slot 上也是有效的,即可以定义下面这样的动态插槽名:
<base-layout>
<template v-slot:[dynamicSlotName]>
...
</template>
<!-- 缩写为 -->
<template #[dynamicSlotName]>
...
</template>
</base-layout>
注意这里的表达式和动态指令参数受相同的语法限制。
Tips:其实就是说中括号中的内容是个表达式。冒号后面的东西就是这个表达式的值
3.3 作用域插槽
在上面的渲染作用域中我们讨论到,插槽的内容无法访问到子组件的状态。
然而在某些场景下插槽的内容可能想要同时使用父组件域内和子组件域内的数据。要做到这一点,我们需要一种方法来让子组件在渲染时将一部分数据提供给插槽。我们也确实有办法这么做!
3.3.1 默认作用域插槽
可以像对组件传递 props 那样,向一个插槽的出口上传递 attributes:
<!-- 这里是子组件 -->
<div>
<slot :text="greetingMessage" :count="1"></slot>
</div>
当需要接收插槽 props 时,默认插槽和具名插槽的使用方式有一些小区别。下面我们将先展示默认插槽如何接受 props,通过子组件标签上的 v-slot 指令,直接接收到了一个插槽 props 对象:
<!-- 这里是父组件 -->
<MyComponent v-slot="slotProps">
{{ slotProps.text }} {{ slotProps.count }}
</MyComponent>
Tips:上面这个slotProps你想叫啥叫啥,叫Java都行,他就是取了个名给下面用而已
子组件传入插槽的 props 作为了 v-slot 指令的值,可以在插槽内的表达式中访问。
你可以将作用域插槽类比为一个传入子组件的函数。子组件会将相应的 props 作为参数传给它:
MyComponent({
// 类比默认插槽,将其想成一个函数
default: (slotProps) => {
return `${slotProps.text} ${slotProps.count}`
}
})
function MyComponent(slots) {
const greetingMessage = 'hello'
return `<div>${
// 在插槽函数调用时传入 props
slots.default({ text: greetingMessage, count: 1 })
}</div>`
}
v-slot="slotProps" 可以类比这里的函数签名,和函数的参数类似,我们也可以在 v-slot 中使用解构:
<MyComponent v-slot="{ text, count }">
{{ text }} {{ count }}
</MyComponent>
3.3.2 具名作用域插槽
具名作用域插槽的工作方式也是类似的,插槽 props 可以作为 v-slot 指令的值被访问到:v-slot:name="slotProps"。当使用缩写时是这样:
<MyComponent>
<template #header="headerProps">
{{ headerProps }}
</template>
<template #default="defaultProps">
{{ defaultProps }}
</template>
<template #footer="footerProps">
{{ footerProps }}
</template>
</MyComponent>
向具名插槽中传入 props:
<slot name="header" message="hello"></slot>
注意:插槽上的
name是一个 Vue 特别保留的属性,不会作为 props 传递给插槽。因此最终headerProps的结果是{ message: 'hello' }。
如果你同时使用了具名插槽与默认插槽,则需要为默认插槽使用显式的 <template> 标签。尝试直接为组件添加 v-slot 指令将导致编译错误。这是为了避免因默认插槽的 props 的作用域而困惑。为默认插槽使用显式的 <template> 标签有助于更清晰地指出 message 属性在其他插槽中不可用:
<template>
<MyComponent>
<!-- 使用显式的默认插槽 -->
<template #default="{ message }">
<p>{{ message }}</p>
</template>
<template #footer>
<p>Here's some contact info</p>
</template>
</MyComponent>
</template>