vue3 组合式API:插槽
一、内容与出口
1、<slot>
元素是一个插槽出口 (slot outlet),标示了父元素提供的插槽内容 (slot content) 将在哪里被渲染。插槽内容可以是任意合法的模板内容,不局限于文本,可以是多个元素,甚至是组件
// 插槽内容可以是多个元素,也可以是组件
<FancyButton><span style="color:red">Click me!</span><AwesomeIcon name="plus" />
</FancyButton>
<FancyButton>Click me! <!-- 插槽内容 -->
</FancyButton>// <FancyButton> 模板
<button class="fancy-btn"><slot></slot> <!-- 插槽出口 -->
</button>// 最终渲染出的 DOM 是这样:
<button class="fancy-btn">Click me!</button>
二、渲染作用域
1、插槽内容可以访问到父组件的数据作用域,因为插槽内容本身就是在父组件模板中定义的
//两个 {{ message }} 插值表达式渲染的内容都是一样的
<span>{{ message }}</span>
<FancyButton>{{ message }}</FancyButton>
2、插槽内容无法访问子组件的数据。Vue 模板中的表达式只能访问其定义时所处的作用域,换言之:父组件模板中的表达式只能访问父组件的作用域;子组件模板中的表达式只能访问子组件的作用域。
三、默认内容:在外部没有提供内容的情况下,可以为插槽指定默认内容。
//例如:<SubmitButton> 组件<button type="submit"><slot></slot>
</button>//如果想在父组件没有提供插槽内容时,在<button>内渲染 “Submit” ,只需将 “Submit”放在<solt>
//标签之间,来作为默认内容<button type="submit"><slot>Submit <!-- 默认内容 --></slot>
</button>上述案例就可以在父组件中没有提供内容时 <SubmitButton /> 被渲染成
<button type="submit">Submit</button>//但是如果提供了内容Save,那么显示提供的内容就会取代默认内容
<SubmitButton>Save</SubmitButton>
//渲染成:
<button type="submit">Submit</button>
四、具名插槽
场景:在父组件中使用的子组件中有多个插槽出口时,为了将内容精准的注入子组件的插槽出口时,此时就需要用到具名插槽了
1、如果一个组件中有多插槽,那么给各个插槽配唯一的ID就成了必要(内容精准的传入到各自目标插槽),<slot>元素有个特殊的attribute name,这类带 name
的插槽被称为具名插槽,没有提供name的 <slot> 出口会隐式的命名为default。
例如:<BaseLayout>组件
<div class="container"><header><!-- 标题内容放这里 --></header><main><!-- 主要内容放这里 --></main><footer><!-- 底部内容放这里 --></footer>
</div>//改为具名插槽后
<div class="container"><header><slot name="header"></slot></header><main><slot></slot></main><footer><slot name="header"></slot></footer>
</div>
2、为具名插槽传入内容,需要使用一个含有v-slot指令的<template>的元素,并将目标插槽的名字传给v-slot指令 ( v-slot 可简写为 # )

//例如2
<BaseLayout><template v-slot:header></template>
</BaseLayout>//简写
<BaseLayout><template #header><h1>Here might be a page title</h1></template>//带 name 叫具名插槽,没有提供name的 <slot> 出口会隐式的命名为default。<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>
3、如果一个组件中同时接收默认、具名插槽时,所有位于顶级的非 <template> 节点都被 “隐式” 视为默认插槽的内容(所以就可以省略<template #default>这个标签)
<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><BaseLayout><template #header><h1>Here might be a page title</h1></template><!-- 隐式的默认插槽:位于顶级的非 <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>
上述被渲染为
<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>
五、条件插槽:有时候根据内容是否被传入了插槽来渲染某些内容,可以使用 $slots 属性结合 v-if 来实现
<template><div class="card"><div v-if="$slots.header" class="card-header"><slot name="header" /></div><div v-if="$slots.default" class="card-content"><slot /></div><div v-if="$slots.footer" class="card-footer"><slot name="footer" /></div></div>
</template>
六、动态插槽名:动态指令参数在 v-slot
上也是有效的,即可以定义下面这样的动态插槽名
<base-layout><template v-slot:[dynamicSlotName]>...</template><!-- 缩写为 --><template #[dynamicSlotName]>...</template>
</base-layout>
七、作用域插槽名:插槽的内容无法访问到子组件的状态,某些场景下插槽的内容可能想要同时使用父组件、子组件域内的数据,这是需要让子组件在渲染时将一部分数据提供给插槽(可以像对组件传递 props 那样,向一个插槽的出口上传递 attributes)
1、默认插槽:子组件传入插槽的 props 作为了 v-slot
指令的值,可以在插槽内的表达式中访问。
//<MyComponent> 模板<div><slot :text="greetingMessage" :count="1"></slot>
</div>//默认插槽如何接受 props
<MyComponent v-slot="soltProps">{{soltProps.text}} {{soltProps.count}}
</MyComponent>// v-slot="slotProps" 可以类比这里的函数(具体函数参考插槽-作用域插槽)签名,和函数的参数类似
//,我们也可以在 v-slot 中使用解构<MyComponent v-slot="{ text, count }">{{ text }} {{ count }}
</MyComponent>
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 特别保留的 attribute,不会作为 props 传递给插槽。因此最终 //headerProps 的结果是 { message: 'hello' }。
3、同时使用了具名插槽与默认插槽,则需要为默认插槽使用显式的 <template>
标签。尝试直接为组件添加 v-slot
指令将导致编译错误。这是为了避免因默认插槽的 props 的作用域而困惑
<!-- <MyComponent> template -->
<div><slot :message="hello"></slot><slot name="footer" />
</div>//无法使用的错误示范
<!-- 该模板无法编译 -->
<MyComponent v-slot="{ message }"><p>{{ message }}</p><template #footer><!-- message 属于默认插槽,此处不可用 --><p>{{ message }}</p></template>
</MyComponent>//正确的
<MyComponent><!-- 使用显式的默认插槽 --><template #default="{ message }"><p>{{ message }}</p></template><template #footer><p>Here's some contact info</p></template>
</MyComponent>
4、高级列表组件示例
场景:渲染一个列表,并同时会封装一些加载远端数据的逻辑、使用数据进行列表渲染、或者是像分页或无限滚动这样更进阶的功能。然而我们希望它能够保留足够的灵活性,将对单个列表元素内容和样式的控制权留给使用它的父组件
<FancyList :api-url="url" :per-page="10"><template #item="{ body, username, likes }"><div class="item"><p>{{ body }}</p><p>by {{ username }} | {{ likes }} likes</p></div></template>
</FancyList>//在 <FancyList> 之中,我们可以多次渲染 <slot> 并每次都提供不同的数据 (注意我们这里使用了 v-bind 来传递插槽的 props):
<ul><li v-for="item in items"><slot name="item" v-bind="item"></slot></li>
</ul>