一、内容与出口
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>