vue2.x学习笔记(十六)
接着前面的内容:https://www.cnblogs.com/yanggb/p/12616543.html。
组件中的插槽
在2.6.0的版本中,vue为具名插槽和作用域插槽引入了一个新的统一的语法(即【v-slot】指令),它取代了【slot】和【slot-scope】这两个目前已经被废弃但是还没被移除且仍在文档中的attribute。
插槽内容
vue实现了一套内容分发的api,这套api的设计灵感来源于web components的规范草案,将<slot>元素作为承载分发内容的出口。
它允许你像这样合成组件:
<navigation-link url="/profile"> Your Profile </navigation-link>
然后你在<navigation-link>的模板中可能会写为:
<a v-bind:href="url" class="nav-link"> <slot></slot> </a>
当这个组件被渲染的时候,<slot></slot>就会被替换为Your Profile。这里的<slot></slot>就是插槽,插槽内可以包含任何模板代码,包括html:
<navigation-link url="/profile"> <!-- 添加一个 Font Awesome 图标 --> <span class="fa fa-user"></span> Your Profile </navigation-link>
甚至其他组件:
<navigation-link url="/profile"> <!-- 添加一个图标的组件 --> <font-awesome-icon name="user"></font-awesome-icon> Your Profile </navigation-link>
如果<navigation-link>组件没有包含一个<slot>元素的话,则该组件起始标签和结束标签之间的任何内容都会被抛弃,因为它们没有能够找到对应的插槽进行内容替换(分发)。
插槽的编译作用域
当你想要在一个插槽中使用数据的时候,比如:
<navigation-link url="/profile"> Logged in as {{ user.name }} </navigation-link>
该插槽跟模板的其他地方一样,都可以访问相同的实例属性(也就是和父级模板相同的作用域),而不能访问<navigation-link>的作用域。例如下例中的url是访问不到的:
<navigation-link url="/profile"> Clicking here will send you to: {{ url }} <!-- 这里的url会是undefined,因为"/profile"是 传递给<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>
而当我们提供内容的时候,这个提供的内容就会被渲染,从而代替后备的内容。
具名插槽
关于具名插槽的内容自2.6.0起有所更新,这里是官方文档日前的最新语法,对于旧的语法不做理会。
有的时候我们可能会需要多个插槽,例如对于一个带有如下模板的<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> <p>A paragraph for the main content.</p> <p>And another one.</p> <template v-slot:footer> <p>Here's some contact info</p> </template> </base-layout>
现在<template>元素中的所有内容都会被传入到相应的插槽中。任何没有被包裹在带有【v-slot】指令的<template>中的内容,都会被视为默认插槽的内容。
虽然机制是这样的,但是为了让页面逻辑更明确一些,依然可以在一个<template>中包裹默认插槽的内容。
<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>
任何一种写法都会渲染出:
<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>上(只有一种例外情况,在后面的作用域插槽会说明),这一点和已经废弃的【slot】属性不同。
作用域插槽
关于作用域插槽的内容自2.6.0起有所更新,这里是官方文档日前的最新语法,对于旧的语法不做理会。
有的时候让插槽内容能够访问子组件中才有的数据是很有用的。例如,设想一个带有如下模板的<current-user>组件:
<span> <slot>{{ user.lastName }}</slot> </span>
我们可能会想要换掉备用的内容,用名而非姓来显示,如下:
<current-user> {{ user.firstName }} </current-user>
然而上面的代码并不会如期望的那样工作,因为只有<current-user>组件才可以访问到user对象,而我们提供的插槽内容实在父级模板中渲染的。
因此,为了让user对象能够在父级组件的插槽内容中可用,我们需要将user对象作为<slot>元素的一个绑定属性:
<span> <slot v-bind:user="user"> {{ user.lastName }} </slot> </span>
绑定在<slot>元素上的属性就被称为插槽prop。现在在父级的作用域中,我们就可以使用带值的【v-slot】指令来定义我们提供的插槽prop的名字:
<current-user> <template v-slot:default="slotProps"> {{ slotProps.user.firstName }} </template> </current-user>
在这个例子中,我们选择将包含所有插槽prop的对象命名为slotProps,但你也可以使用任意你喜欢的名字。
独占默认插槽的缩写语法
在上述的情况下,当被提供的内容只有默认插槽的时候,组件的标签是可以被当做插槽的模板来使用的,这样我们就可以把【v-slot】指令直接用在组件上而不需要包裹一层<template>:
<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"> slotProps is NOT available here </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】指令的值实际上可以是任何能够作为函数定义的参数的javascript表达式。所以在支持的环境下(单文件组件或现代浏览器),你都可以使用es205解构来传入具体的插槽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: 'Guest' } }"> {{ user.firstName }} </current-user>
关于这部分的内容理解起来比较抽象,需要对javascript中解构的知识有一定的基础。
动态插槽名
在vue2.6.0中新增了一个语法,可以将动态指令参数用在【v-slot】指令上,来定义动态的插槽名:
<base-layout> <template v-slot:[dynamicSlotName]> ... </template> </base-layout>
具名插槽的缩写
在vue2.6.0中,新增了具名插槽的缩写。即跟【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> <p>A paragraph for the main content.</p> <p>And another one.</p> <template #footer> <p>Here's some contact info</p> </template> </base-layout>
然而和其他的指令一样,该缩写只有在其有参数的时候才可用,这就意味着以下的语法是无效的:
<!-- 这样会触发一个警告 --> <current-user #="{ user }"> {{ user.firstName }} </current-user>
如果你希望使用缩写的话,必须始终以明确的插槽名取而代之:
<current-user #default="{ user }"> {{ user.firstName }} </current-user>
其他示例
插槽prop允许我们将插槽转换为可复用的模板,这些模板可以基于输入的prop渲染出不同的内容。这一特性在设计封装数据逻辑同时允许父级组件自定义部分布局的可复用组件的场景中是最有用的。
例如,我们要实现一个<todo-list>组件,它是一个列表且包含布局和过滤逻辑:
<ul> <li v-for="todo in filteredTodos" v-bind:key="todo.id"> {{ todo.text }} </li> </ul>
我们可以将每个todo作为父级组件的插槽,以此来通过父级组件对其进行控制,然后将todo作为一个插槽的prop进行绑定:
<ul> <li v-for="todo in filteredTodos" v-bind:key="todo.id"> <!-- 我们为每个 todo 准备了一个插槽,将todo对象作为一个插槽的prop传入。 --> <slot name="todo" v-bind:todo="todo"> <!-- 后备内容 --> {{ todo.text }} </slot> </li> </ul>
现在当我们使用<todo-list>组件的时候,我们就可以选择为todo定义一个不一样的<template>作为替代方案,并且可以从子组件中获取数据。
<todo-list v-bind:todos="todos"> <template v-slot:todo="{ todo }"> <span v-if="todo.isComplete">✓</span> {{ todo.text }} </template> </todo-list>
这只是作用域插槽用武之地的冰山一角,可以通过浏览各种第三方库的源码实现来深入了解作用域插槽的应用,诸如vue virtual scroller、vue promised和portal vue等库。
废弃了的语法
【v-slot】指令自vue 2.6.0起被引入,提供更好的支持【slot】和【slot-scope】属性的api替代方案。虽然在接下来的所有2.x版本中的【slot】和【slot-scope】属性依然会被支持,但是它们已经被官方废弃且不会再在vue 3中出现。
带有【slot】属性的具名插槽
在<template>上使用特殊的【slot】属性,可以将内容从父级传给具名插槽(新的语法使用【v-slot:插槽名】指令代替)。
<base-layout> <template slot="header"> <h1>Here might be a page title</h1> </template> <p>A paragraph for the main content.</p> <p>And another one.</p> <template slot="footer"> <p>Here's some contact info</p> </template> </base-layout>
或者可以直接把【slot】属性使用在一个普通的元素上:
<base-layout> <h1 slot="header">Here might be a page title</h1> <p>A paragraph for the main content.</p> <p>And another one.</p> <p slot="footer">Here's some contact info</p> </base-layout>
这里其实还有一个未命名的插槽(默认插槽),用来捕获所有未被匹配的内容。上述两个示例的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>
带有【slot-scope】属性的作用域插槽
在<template>上使用特殊的【slot-scope】属性,可以接收传递给插槽的prop(新的语法使用【v-slot:插槽名="参数对象名"】指令代替)。
<slot-example> <template slot="default" slot-scope="slotProps"> {{ slotProps.msg }} </template> </slot-example>
这里的【slot-scope】属性声明了被接收的prop对象会作为slotProps变量存在于<template>的作用域中,你可以像命名javascript的函数参数一样随意命名slotProps。
这里的slot="default"还可以被忽略为隐性写法:
<slot-example> <template slot-scope="slotProps"> {{ slotProps.msg }} </template> </slot-example>
另外,【slot-scope】属性也可以直接用于非<template>元素(包括组件):
<slot-example> <span slot-scope="slotProps"> {{ slotProps.msg }} </span> </slot-example>
【slot-scope】属性的值还可以接收任何有效的可以出现在函数定义的参数位置上的javascript表达式,这意味着在支持的环境(单文件组件或现代浏览器)下,你都可以在表达式中使用es2015解构,如下:
<slot-example> <span slot-scope="{ msg }"> {{ msg }} </span> </slot-example>
使用上面的<todo-list>作为示例,与它等价的使用【slot-scope】属性的代码是:
<todo-list v-bind:todos="todos"> <template slot="todo" slot-scope="{ todo }"> <span v-if="todo.isComplete">✓</span> {{ todo.text }} </template> </todo-list>
因为新的语法(v-slot)是在2.6.0+的版本才被支持的,因此如果低于此版本的话要注意使用旧版的语法或升级新的vue版本(更建议)。
"我还是很喜欢你,像木石前盟心誓许,无论结局。"