文章目录
推荐先阅读一下 组件基础篇
v-slot指令:具名插槽和作用域插槽引入一个新的统一的语法。它取代了slot和slot-scope,目前这两个已被废弃但未被移除且仍在文档中的attribute。
插槽内容
Vue实现了一套内容分发的API,将<slot>
元素作为承载分发内容的出口,所以我们可以这样合成组件:
<navigation-link url="/API">
// 一顿操作...
</navigation-link>
然后再 的模板中可能写成:
<a
:href="url"
class="nav-link"
>
<slot></slot>
</a>
当组件在渲染的时候,就会将 <slot></slot>
替换成 ‘一顿操作…’ 。插槽内可以包含任意模板代码,包括HTML:
<navigation-link url="/API">
// 添加一个 Font Awesome 图标
<span class="fa fa-user"></span>
// 一顿操作...
</navigation-link>
甚至是它的组件:
<navigation-link url="/API">
// 添加一个图标组件
<font-awesome-icon name="user"></font-awesome-icon>
// 一顿操作...
</navigation-link>
如果<navigation-link>
的 template 中没有包含一个 <slot>
元素,则该组件起始标签和结束标签之间没有任何内容都会被抛弃。
编译作用域
当我们想要在插槽中使用数据时,例如:
<navigation-link url="/API">
Lo in as {{ user.name }}
</navigation-link>
该插槽跟模板的其他地方一样都可以访问相同的实例property(也就是相同的“作用域”),而不能访问<navigation-link>
的作用域。例如url是访问不到的:
<navigation-link url="/API">
this is a {{ url }}
// 这样写 url 会是 undefined ,因为其(只该插槽的)内容是传递给 <navigation-link>,而不是在 <navigation-link> 组件 内部 定义的
</navigation-link>
规则:父级模板里所有的内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。
后备(默认的)内容
有时需要给插槽设置具体的后备(也就是 默认的)内容,是很有用的,它只会在没有提供内容的时候被渲染,例如<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>
<button>Submit</button>
但是只要我们提供了内容,后备内容将不会被渲染出来,而是渲染我们提供的内容:
<submit-button>
Save
</submit-button>
<button>Save</button>
具名插槽
有时我们需要多个插槽时,例如对于一个带有如下模板的<base-layout>
组件:
<div class="container">
<header>
// 页头,随你操作...
</header>
<main>
// 页面主要内容,随你操作...
</main>
<footer>
// 页脚
</footer>
</div>
对于这种情况,<slot>
元素有一个特殊的 attribute: name 。这个 attribute 可以用来定义额外的插槽:
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main></main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
一个不带name的<slot>
出口会带有隐含的名字 <default>
在向具名插槽提供内容时,我们可以在一个 <template>
元素上使用 v-slot 指令,并以 v-slot 的参数的形式提供其名称:
<base-layout>
<template v-slot:header>
<h1>this is a title</h1>
</template>
<p>content</p>
<p>another</p>
<template v-slot:footer>
<p>info</p>
</template>
</base-layout>
现在<template>
元素中的所有内容都将会被传入相应的插槽。任何没有被包裹在带有 v-slot 的 <template>
中的内容都会被视为默认插槽的内容。
如果我们需要更明确一些,仍然可以在一个<template>
中包裹默认插槽的内容:
<base-layout>
<template v-slot:header>
<h1>this is a title</h1>
</template>
<template v-slot:default>
<p>content</p>
<p>another</p>
</template>
<template v-slot:footer>
<p>footer info</p>
</template>
</base-layout>
渲染结果便是(结果中标签内容分别对应 v-slot 的属性):
<div class="container">
<header>
<h1>this is a title</h1>
</header>
<main>
<p>content</p>
<p>another</p>
</main>
<footer>
<p>footer info<p>
</footer>
</div>
注意 v-slot 只能添加在 <template>
上(只有一种例外情况),这一点跟已经废弃的slot attribute不同。
具名插槽的缩写
v-slot 缩写为 #
<base-layout>
<template #header>
<h1>this is a title</h1>
</template>
<p>content</p>
<p>another</p>
<template #footer>
<p>footer info</p>
</template>
</base-layout>
和其他指令一样,该缩写只在其有参数的时候才可用,导致下面的写法是无效的:
// 一个警告
<current-user #="{ user }">
{{ user.firstName }}
</current-user>
防止报错,必须始终以明确的插槽名取而代之:
<current-user #default="{ user }">
{{ user.firstName }}
</current-user>
作用域插槽
让插槽能访问到子组件才有的数据是很有用的,例如:
// <current-user>组件
<span>
<solt>{{ user.lastName }}</slot>
</span>
// 换个字段显示
<current-user>
{{ user.firstName }}
</current-user>
但是上面的代码不会正常工作,因为只有 <current-user>
组件可以访问到 user ,而我们提供的内容是在父级插槽渲染的。
为了让 user 在父级的插槽内容中可以被使用,我们可以将 user 作为<slot>
元素的一个 attribute 绑定上去:
<span>
<slot :user="user">
{{ user.lastName }}
</slot>
</span>
上述绑定在<slot>
元素上的attribute被称为插槽prop,现在在父级作用域中,我们可以使用带值的v-slot来定义我们提供的插槽prop的名字:
// 将包含所有插槽prop的对象命名为slotProps,也可以使用其他名字
<current-user>
<template v-slot:default="slotProps">
{{ slotProps.user.firstName }}
</template>
</current-user>
独占默认插槽的缩写语法
当被提供的内容只有默认插槽时,组件的标签才可以被当作插槽的模板来使用,这样我们就可以把 v-slot 直接用在组件上:
<current-user v-slot:default="slotProps">
{{ slotProps.user.firstName }}
</current-user>
相比之前,这种写法更为简单,就像假定未指明的内容对应默认插槽一样,不带参数的 v-slot 被假定对应默认插槽:
<current-user v-slot="slotProps">
{{ slotProps.user.firstName }}
</current-user>
注意:默认插槽的缩写语法是不能与具名插槽混用,会导致作用域不明确:
// 会报错
<current-user v-slot="slotProps">
{{ slotProps.user.firstName }}
<template v-slot:other="otherSlotProps">
error
</template>
</current-user>
一旦使用多个插槽时,最好使用<template>
包裹一下,各自隔开:
<current-user>
<template v-slot:default="slotProps">
{{ slotProps.user.firstName }}
</template>
<template v-slot:other="otherSlotProps">
// ...
</template>
</current-user>
解构插槽 Prop
作用域插槽的内部工作原理:是将插槽内容包裹在一个拥有单个参数的函数里:
function (slotProps) {
// 插槽内容
}
这也就意味着,v-slot的值实际上可以是任何能够作为函数定义中的参数js表达式。所以在支持的环境下(单文件组件或现代浏览器),我们可以使用ES2015解构来传入具体的插槽prop:
<current-user v-slot="{ user }">
{{ user.firstName }}
</current-user>
这边使用的模板会更简洁,尤其是在插槽提供多个prop的时候。也能使用于prop重命名:
// 将 user 重命名为 person
<current-user v-slot="{ user: person }">
{{ person.firstName }}
</current-user>
也可是在重命名的时候,定义后面内容,用于防止插槽prop是undefined的情形:
<current-user v-slot="{ user = {firstName: 'Root'} }">
{{ person.firstName }}
</current-user>
动态插槽名
可以在v-slot上,来定义动态的插槽名:
<base-layout>
<template v-slot: [slotName]>
// ...
</template>
</base-layout>
其他示例
插槽prop允许我们将插槽装换为可复用的模板,这些模板可以基于输入的prop渲染出不同的内容。这在设计封装数据逻辑同时允许父级组件自定义布局部分布局的可复用组件是最有用的。
比如,我们要实现一个<todo-list>
组件,它是一个列表且包含布局和过滤逻辑:
<ul>
<li
v-for="todo in todoList"
:key="todo.id"
>
{{ todo.text }}
</li>
</ul>
我们可以将每个todo作为父级组件的插槽,以此通过父级组件对其进行控制,然后将todo作为一个插槽prop进行绑定:
<ul>
<li
v-for="todo in todoList"
:key="todo.id"
>
// 我们为每个todo准备一个插槽,将todo对象作为一个插槽的prop传入
<slot name="todo" :todo="todo">
{{ todo.text }}
</slot>
</li>
</ul>
现在当我们使用<todo-list>
组件的时候,我们可以选择为todo定义一个不一样的<template>
作为替代方案,并且可以从子组件获取数据:
<todo-list :todo="todos">
<template v-slot:todo="{ todo }">
<span v-if="todo.isComplete">
{{ todo.text }}
</span>
</template>
</todo-list>
当然,这些只是一点插槽的用法,还有很多作用域插槽的用法。。。
废弃的插槽语法
v-slot指令是 slot 和 slot-scope attribute的API替代方案
带有slot attribute的具名插槽
在<template>
上使用特殊的slot attribute,可以将内容从父级传给具名插槽(就比如上面<base-layout>
组件):
<base-layout>
<template slot="header">
<h1>this is a title</h1>
</template>
<p>content</p>
<p>another</p>
<template slot="footer">
<p>info</p>
</template>
</base-layout>
或者直接把slot attribute用在一个普通元素上:
<base-layout>
<h1 slot="header">this is a title</h1>
<p>content</p>
<p>another</p>
<p slot="footer">info</p>
</base-layout>
其实这里还有一个未命名插槽,也就是默认插槽,捕获所有未被匹配的内容,上面例子渲染结果都为:
<div class="container">
<header>
<h1>this is a title</h1>
</header>
<main>
<p>content</p>
<p>another</p>
</main>
<footer>
<p>info</p>
</footer>
</div>
带有slot-scope attribute的作用域插槽
elementUI 中会自定义表格内容时,会用到这个语法
在<template>
上使用特殊的slot-scope
attribute,可以接受传递给插槽的prop:
<slot-example>
// 这里的 slot="default" 可以被忽略为隐性写法
<template slot="default" slot-scope="slotProps">
{{ slotProps.msg }}
</template>
</slot-example>
这里的slot-scope
声明了被接受的prop对象会作为slotProps变量存在于<template>
作用域中。当然这个名字是随意命名的。
slot-scope
的值可以接收任何有效的可以出现在函数定义的参数位置上的js表达式,意味着在支持的环境下,可以在表达式中使用ES2015解构:
<slot-example>
<span slot-scope="{ msg }">
{{ msg }}
</span>
</slot-example>
在这里上面<todo-list>
等价于使用的slot-scope
的代码是:
<todo-list :todos="todos">
<template slot="todo" slot-scope="{ todo }">
<span v-if="todo.isComplete">√</span>
{{ todo-text }}
</template>
</todo-list>
而elementUI中使用的场景一般为:
// 自定义列模板
<template>
<el-table
:data="tableData"
>
<template slot-scope="scope">
// 加上各自相应的条件
<span v-if="type === 1">{{ scope.row.name }}</span>
<span v-else>{{ scope.row.name }}</span>
</template>
</el-table>
</template>