记住!插槽的内容是写在父组件的模板内,而插槽的出口是在子组件的模板内的。
Vue 实现了一套用于向子组件分发内容的 API,也就是 <slot>
机制。
它允许你像这样使用组件:
<todo-button> Add todo </todo-button>
然后在 <todo-button>
的模板中,你可能有:
<!-- todo-button 组件模板 --> <button class="btn-primary"> <slot></slot> </button>
当组件渲染的时候,<slot></slot>
将会被替换为Add Todo
<!-- 最终的渲染效果 HTML --> <button class="btn-primary"> Add todo </button>
不仅仅是字符串,插槽还可以包含任何模板代码,包括 HTML:
<todo-button> <!-- 添加一个Font Awesome 图标 --> <i class="fas fa-plus"></i> Add todo </todo-button>
或其他组件:
<todo-button> <!-- 添加一个图标的组件 --> <font-awesome-icon name="plus"></font-awesome-icon> Add todo </todo-button>
但是,如果 <todo-button>
的 template
中没有编写一个 <slot>
元素,则该组件被应用时的,起始标签和结束标签之间的,任何内容,都会被抛弃!
<!-- todo-button 组件模板 --> <button class="btn-primary"> Create a new item </button>
<todo-button> <!-- 以下文本不会渲染 --> Add todo </todo-button>
当你想在一个插槽中使用插值数据时,例如:
<todo-button> Delete a {{ item.name }} </todo-button>
该插槽可以访问与模板其余部分相同的实例 属性(即相同的“作用域”)。
插槽不能访问
<todo-button>
的作用域。例如,尝试访问 action
将不起作用:
<todo-button action="delete"> Clicking here will {{ action }} an item <!-- `action` 未被定义,因为它的内容是传递*到* <todo-button>,而不是*在* <todo-button>里定义的。 --> </todo-button>
请记住这条规则:
父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。
有时为插槽设置一个默认值是很有用的,它只会在没有提供具体内容的时候被启用。例如在一个 <submit-button>
组件中:
<button type="submit"> <slot></slot> </button>
我们希望这个 <button>
绝大多数情况下都渲染文本“Submit”。为了将“Submit”作为默认内容,我们可以将它放在 <slot>
标签内:
<button type="submit"> <slot>Submit</slot> </button>
现在当我们在一个父级组件中使用 <submit-button>
并且不提供任何插槽内容时:
<submit-button></submit-button>
备用内容“Submit”将会被渲染:
<button type="submit"> Submit </button>
但是如果我们提供内容:
<submit-button> Save </submit-button>
则这个提供的内容将会被渲染从而取代默认内容:
<button type="submit"> Save </button>
有时我们需要多个插槽。例如对于一个带有如下模板的 <base-layout>
组件:
<div class="container"> <header> <!-- 我们希望把页头放这里 --> </header> <main> <!-- 我们希望把主要内容放这里 --> </main> <footer> <!-- 我们希望把页脚放这里 --> </footer> </div>
对于这样的情况,<slot>
元素有一个特殊的属性:name
。这个属性就是用来区分各个插槽的:
<div class="container"> <header> <slot name="header"></slot> </header> <main> <slot></slot> </main> <footer> <slot name="footer"></slot> </footer> </div>
不带 name
的 <slot>
插槽的隐含名为default
。
在向命名插槽提供内容的时候,我们可以在外层包裹一个 <template>
元素,并在其上使用 v-slot
指令,然后以 v-slot
参数的形式提供插槽名称,如下所示:
<base-layout> <template v-slot:header> <h1>Here might be a page title</h1> </template> <template v-slot:default> <p>A paragraph for the main content.</p> <p>And another one.</p> </template> <template v-slot:footer> <p>Here's some contact info</p> </template> </base-layout>
<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>
注意,v-slot
只能添加在 <template>
上 (只有一种例外情况)
前面我们说了,插槽的内容有作用域,父子间不可以互相访问。
但是有时候,让插槽内容能够访问子组件中的数据是很有用的。
比如这个需求:当一个组件被用来渲染一个项目数组时,我们希望能够自定义每个项目的渲染方式。
假设我们有一个组件,包含 todo-items
列表:
app.component('todo-list', { data() { return { items: ['Feed a cat', 'Buy milk'] } }, template: ` <ul> <li v-for="(item, index) in items"> {{ item }} </li> </ul> ` })
我们可能会想把 {{ item }}
替换为 <slot>
,以便在父组件上如下自定义。
<todo-list> <i class="fas fa-check"></i> <span class="green">{{ item }}</span> </todo-list>
但是,这是行不通的,因为只有 <todo-list>
组件本身才可以访问 item
。
要使 item
可用于父级提供的插槽内容,我们可以为 <slot>
元素绑定一个item属性:
<ul> <li v-for="( item, index ) in items"> <slot :item="item"></slot> </li> </ul>
可以根据自己的需求,绑定任意数量的属性到 slot
上:
<ul> <li v-for="( item, index ) in items"> <slot :item="item" :index="index" :another-attribute="anotherAttribute"></slot> </li> </ul>
绑定在 <slot>
元素上的属性被称为插槽 prop。现在在父级作用域中,我们可以使用带值的 v-slot
来定义我们提供的插槽 prop 的名字:
<todo-list> <template v-slot:default="slotProps"> <i class="fas fa-check"></i> <span class="green">{{ slotProps.item }}</span> </template> </todo-list>
在上面的例子中,我们将包含所有插槽 prop 的对象命名为 slotProps
,你也可以使用任意你喜欢的名字。
说白了,作用域插槽就是Vue为父组件的内容,访问子组件数据开了个口子!
在上述情况下,当被提供的内容只有默认插槽时,组件的标签才可以被当作插槽的模板来使用。这样我们就可以把 v-slot
直接用在组件上:
<todo-list v-slot:default="slotProps"> <i class="fas fa-check"></i> <span class="green">{{ slotProps.item }}</span> </todo-list>
这种写法还可以更简单。就像假定未指明的内容对应默认插槽一样,不带参数的 v-slot
被假定对应默认插槽:
<todo-list v-slot="slotProps"> <i class="fas fa-check"></i> <span class="green">{{ slotProps.item }}</span> </todo-list>
注意默认插槽的缩写语法不能和命名插槽混用,因为它会导致作用域不明确:
<!-- 无效,会导致警告 --> <todo-list v-slot="slotProps"> <i class="fas fa-check"></i> <span class="green">{{ slotProps.item }}</span> <template v-slot:other="otherSlotProps"> slotProps is NOT available here </template> </todo-list>
只要同时有多个插槽,就需要始终为所有的插槽使用完整的基于 <template>
的语法:
<todo-list> <template v-slot:default="slotProps"> <i class="fas fa-check"></i> <span class="green">{{ slotProps.item }}</span> </template> <template v-slot:other="otherSlotProps"> ... </template> </todo-list>
作用域插槽的内部工作原理是将你的插槽内容包括在一个传入单个参数的函数里:
function (slotProps) { // ... 插槽内容 ... }
这意味着 v-slot
的值实际上可以是任何能够作为函数定义中的参数的 JavaScript 表达式。你也可以使用 ES2015 解构来传入具体的插槽 prop,如下:
<todo-list v-slot="{ item }"> <i class="fas fa-check"></i> <span class="green">{{ item }}</span> </todo-list>
这样可以使模板更简洁,尤其是在该插槽提供了多个 prop 的时候。它同样开启了 prop 重命名等其它可能,例如将 item
重命名为 todo
:
<todo-list v-slot="{ item: todo }"> <i class="fas fa-check"></i> <span class="green">{{ todo }}</span> </todo-list>
你甚至可以定义备用内容,用于插槽 prop 是 undefined 的情形:
<todo-list v-slot="{ item = 'Placeholder' }"> <i class="fas fa-check"></i> <span class="green">{{ item }}</span> </todo-list>
个人建议:不要玩这些ES的简写语法花样技巧,除了带来阅读障碍,没什么好处。尤其对于新手极其不友好!
动态指令参数也可以用在 v-slot
上,来定义动态的插槽名,这样一个插槽就可以化身千万了:
<base-layout> <template v-slot:[dynamicSlotName]> ... </template> </base-layout>
跟 v-on
和 v-bind
一样,v-slot
也有缩写,即把参数之前的所有内容 (v-slot:
) 替换为字符 #
。
例如 v-slot:header
可以被简写为 #header
:
<base-layout> <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> </base-layout>
然而,和其它指令一样,该缩写只在其有参数的时候才可用。这意味着以下语法是无效的:
<!-- This will trigger a warning --> <todo-list #="{ item }"> <i class="fas fa-check"></i> <span class="green">{{ item }}</span> </todo-list>
如果你希望使用缩写的话,必须始终以带有明确的插槽名:
<todo-list #default="{ item }"> <i class="fas fa-check"></i> <span class="green">{{ item }}</span> </todo-list>