Vue3手册译稿 - 深入组件 - 插槽
插槽
本章节需要掌握组件基础
插槽内容
Vue实现一套优秀的内容分发接口,基于WEB组件草案,使用<slot>
标签提供内容分发服务。
这允许你像这样构成一个组件:
<todo-button>
Add todo
</todo-button>
todo-button
模板应该这样写:
<!-- todo-button 组件模板 -->
<button class="btn-primary">
<slot></slot>
</button>
组件被渲染时,<slot></solt>
将会被替换为: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 组件模板-->
<button class="btn-primary">
Create a new item
</button>
<todo-button>
<!-- 下面文字不会被渲染 -->
Add todo
</todo-button>
渲染域
如果你想在组件内使用data
,例如:
<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></slot>
中间:
<button type="submit">
<slot>Submit</slot>
</button>
当使用这个组件且不为插槽提供内容时:
<submit-button></submit-button>
会被渲染为:
<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>
这种情形,插槽有一个特殊的属性name
,它可以为每个插槽定义一个唯一的ID,这样你就可以决定哪些内容渲染到哪个地方了:
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
没有指定name
的插槽的名字是default
。
模板中使用具名插槽,我们需要使用v-slot
指令,v-slot
参数就是插槽的名字:
<base-layout>
<template v-slot:header>
<h1> 这里可能是标题</h1>
</template>
<template v-slot:default>
<p> 主要内容段落</p>
<p>另一段内容</p>
</template>
<template v-slot:footer>
<p>一些联系方式</p>
</template>
</base-layout>
现在所有模板内的元素都被传递到相应的插槽里,最终渲染结果可能如下:
<div class="container">
<header>
<h1>这里可能是标题</h1>
</header>
<main>
<p> 主要内容段落</p>
<p>另一段内容</p>
</main>
<footer>
<p>一些联系方式</p>
</footer>
</div>
注意:v-slot
只能放在template
内(有一种例外)
作用域插槽
有时,只有在子组件内部可以访问data内容是有用的。一个常用的例子就是使用组件渲染一个数组项,我们想定制数组项的渲染方式。
例如我们有一个组件,里面包含一个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 }}`` 用一个插槽替换,以在父组件来定制:
<todo-list>
<i class="fas fa-check"></i>
<span class="green">{{ item }}</span>
</todo-list>
但这不会工作,因为只有<todo-list>
组件才能够访问item
和父组件提供的插槽内容。
要想让父组件提供的插槽内容能够使用item
,我们可以添加<slot>
标签并绑定成属性:
<ul>
<li v-for="( item, index ) in items">
<slot :item="item"></slot>
</li>
</ul>
一个插槽可以绑定多个属性:
<ul>
<li v-for="( item, index ) in items">
<slot :item="item" :index="index" :another-attribute="anotherAttribute"></slot>
</li>
</ul>
属性绑定到<slot>
叫做slot props
。现在在父组件域,我们可以使用v-slot
绑定值来定义一个slot props名字:
<todo-list>
<template v-slot:default="slotProps">
<i class="fas fa-check"></i>
<span class="green">{{ slotProps.item }}</span>
</template>
</todo-list>
在这个例子中,我们选择命名一个包含全部slot props的对象slotProps
,你可以使用任何名称随你喜欢。
单默认插槽缩写语法
如上例,单个默认插槽,组件模板可以使用组件标签。这允许我们直接在组件上使用v-slot
:
<todo-list v-slot:default="slotProps">
<i class="fas fa-check"></i>
<span class="green">{{ slotProps.item }}</span>
</todo-list>
可以更简短,就像没有指定内容分配给默认插槽,无参数的v-solt
指向默认插槽:
<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>
无论何时有多个插槽时,需要为所有插槽使用完整的模板语法:
<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>
解构插槽props
内部原理是,作用域插槽是插槽内含有一个参数的函数实现:
function (slotProps) {
// ... slot content ...
}
这意味着v-slot
的值可以接受任何合法的JavaScript表达式,函数定义可以出现在参数位置。所以你可以使用ES2005结构脱离特定的插槽props,像这样:
<todo-list v-slot="{ item }">
<i class="fas fa-check"></i>
<span class="green">{{ item }}</span>
</todo-list>
这可以使用模板看起来更清爽,特别是插槽有多个props时。这也开启了其它可能性,比如重命名props,e.g. item
转成todo
:
<todo-list v-slot="{ item: todo }">
<i class="fas fa-check"></i>
<span class="green">{{ todo }}</span>
</todo-list>
你甚至可以定义一个备用,在有些场景下插槽prop未定义时。
<todo-list v-slot="{ item = 'Placeholder' }">
<i class="fas fa-check"></i>
<span class="green">{{ item }}</span>
</todo-list>
动态具名插槽
动态指令参数同样也适用于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>
但是,和其他指令一样,缩写仅适用于提供了参数的情形。这意味着下面的例子是不合法的:
<!-- 这会触发一个警告 -->
<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>