在 Vue.js 中使用插槽
已发表: 2022-03-10在最近发布的 Vue 2.6 中,使用槽的语法变得更加简洁。 对插槽的这种更改让我对发现插槽的潜在力量重新产生了兴趣,以便为我们基于 Vue 的项目提供可重用性、新功能和更清晰的可读性。 插槽真正能够做什么?
如果您是 Vue 新手或者没有看到 2.6 版的变化,请继续阅读。 了解插槽的最佳资源可能是 Vue 自己的文档,但我会尝试在这里给出一个概要。
什么是老虎机?
插槽是 Vue 组件的一种机制,它允许您以不同于严格的父子关系的方式组合组件。 插槽为您提供了将内容放置在新位置或使组件更通用的出口。 了解它们的最佳方式是亲眼目睹它们的行动。 让我们从一个简单的例子开始:
// frame.vue <template> <div class="frame"> <slot></slot> </div> </template>
这个组件有一个包装器div
。 让我们假设div
在那里围绕其内容创建一个风格框架。 该组件可用于将框架包裹在您想要的任何内容周围。 让我们看看使用它的样子。 这里的frame
组件是指我们上面刚刚制作的组件。
// app.vue <template> <frame><img src="an-image.jpg"></frame> </template>
开始和结束frame
标签之间的内容将被插入到slot
所在的frame
组件中,替换slot
标签。 这是最基本的方法。 您还可以通过填写以下内容来指定要进入插槽的默认内容:
// frame.vue <template> <div class="frame"> <slot>This is the default content if nothing gets specified to go here</slot> </div> </template>
所以现在如果我们像这样使用它:
// app.vue <template> <frame /> </template>
“This is the default content if nothing gets specified to go here”的默认文本将显示,但如果我们像以前一样使用它,默认文本将被img
标签覆盖。
多个/命名插槽
您可以向一个组件添加多个插槽,但如果这样做,除了其中一个之外,所有插槽都需要有名称。 如果有一个没有名称,则它是默认插槽。 以下是创建多个插槽的方法:
// titled-frame.vue <template> <div class="frame"> <header><h2><slot name="header">Title</slot></h2></header> <slot>This is the default content if nothing gets specified to go here</slot> </div> </template>
我们保留了相同的默认插槽,但这次我们添加了一个名为header
的插槽,您可以在其中输入标题。 你像这样使用它:
// app.vue <template> <titled-frame> <template v-slot:header> <!-- The code below goes into the header slot --> My Image's Title </template> <!-- The code below goes into the default slot --> <img src="an-image.jpg"> </titled-frame> </template>
和之前一样,如果我们想将内容添加到默认插槽中,只需将其直接放在titled-frame
组件中即可。 但是,要将内容添加到命名插槽,我们需要使用v-slot
指令将代码包装在template
标签中。 您在v-slot
之后添加一个冒号 ( :
),然后写下您希望将内容传递到的插槽的名称。 请注意v-slot
是 Vue 2.6 的新版本,因此如果您使用的是旧版本,则需要阅读有关已弃用插槽语法的文档。
范围插槽
您需要知道的另一件事是插槽可以将数据/函数向下传递给它们的子级。 为了证明这一点,我们需要一个完全不同的带有插槽的示例组件,它比前一个更做作:让我们通过创建一个将当前用户的数据提供给其插槽的组件来从文档中复制示例:
// current-user.vue <template> <span> <slot v-bind:user="user"> {{ user.lastName }} </slot> </span> </template> <script> export default { data () { return { user: ... } } } </script>
该组件有一个名为user
的属性,其中包含有关用户的详细信息。 默认情况下,该组件显示用户的姓氏,但请注意,它使用v-bind
将用户数据绑定到插槽。 有了这个,我们可以使用这个组件将用户数据提供给它的后代:
// app.vue <template> <current-user> <template v-slot:default="slotProps">{{ slotProps.user.firstName }}</template> </current-user> </template>
为了访问传递给插槽的数据,我们使用v-slot
指令的值指定范围变量的名称。
这里有一些注意事项:
- 我们指定了
default
的名称,尽管我们不需要为默认插槽指定名称。 相反,我们可以只使用v-slot="slotProps"
。 - 您不需要使用
slotProps
作为名称。 你可以随心所欲地称呼它。 - 如果您只使用默认插槽,则可以跳过该内部
template
标签并将v-slot
指令直接放在current-user
标签上。 - 您可以使用对象解构来创建对作用域槽数据的直接引用,而不是使用单个变量名。 换句话说,你可以使用
v-slot="{user}"
代替v-slot="slotProps"
然后你可以直接使用user
代替slotProps.user
。
考虑到这些注释,上面的例子可以改写成这样:
// app.vue <template> <current-user v-slot="{user}"> {{ user.firstName }} </current-user> </template>
还有几件事要记住:
- 您可以使用
v-bind
指令绑定多个值。 所以在这个例子中,我可以做的不仅仅是user
。 - 您也可以将函数传递给作用域插槽。 许多库使用它来提供可重用的功能组件,稍后您将看到。
-
v-slot
的别名是#
。 所以不用写v-slot:header="data"
,你可以写#header="data"
。 当您不使用作用域插槽时,您也可以只指定#header
而不是v-slot:header
。 至于默认插槽,您需要在使用别名时指定default
名称。 换句话说,您需要编写#default="data"
而不是#="data"
。
您可以从文档中了解更多的小点,但这应该足以帮助您理解我们在本文其余部分讨论的内容。
你可以用老虎机做什么?
插槽不是为单一目的而建造的,或者至少如果它们是,它们已经超越了最初的意图,成为做许多不同事情的强大工具。
可重用模式
组件总是被设计成可以重用的,但是某些模式对于单个“普通”组件是不切实际的,因为你需要的props
数量来定制它可能过多或者你需要通过props
传递大部分内容和潜在的其他组件。 插槽可用于包含模式的“外部”部分,并允许将其他 HTML 和/或组件放置在其中以自定义“内部”部分,从而允许具有插槽的组件定义模式和注入到插槽是唯一的。
对于我们的第一个示例,让我们从简单的东西开始:一个按钮。 假设您和您的团队正在使用 Bootstrap*。 使用 Bootstrap,您的按钮通常与基类 `btn` 和一个指定颜色的类绑定在一起,例如 `btn-primary`。 您还可以添加大小类,例如 `btn-lg`。
* 我既不鼓励也不阻止你这样做,我只是需要一些东西作为我的例子,而且它是众所周知的。
现在让我们假设,为简单起见,您的应用程序/站点始终使用btn-primary
和btn-lg
。 你不想总是在你的按钮上写下所有三个类,或者你不相信一个菜鸟会记住这三个类。 在这种情况下,您可以创建一个自动包含所有这三个类的组件,但是如何允许自定义内容呢? prop
是不实用的,因为button
标签中允许包含各种 HTML,所以我们应该使用 slot。
<!-- my-button.vue --> <template> <button class="btn btn-primary btn-lg"> <slot>Click Me!</slot> </button> </template>
现在我们可以在任何地方使用它,无论您想要什么内容:
<!-- somewhere else, using my-button.vue --> <template> <my-button> <img src="/img/awesome-icon.jpg"> SMASH THIS BUTTON TO BECOME AWESOME FOR ONLY $500!!! </my-button> </template>
当然,您可以使用比按钮更大的东西。 坚持使用 Bootstrap,让我们看一下模式,或者至少是 HTML 部分; 我不会进入功能......但是。
<!-- my-modal.vue --> <template> <div class="modal" tabindex="-1" role="dialog"> <div class="modal-dialog" role="document"> <div class="modal-content"> <div class="modal-header"> <slot name="header"></slot> <button type="button" class="close" data-dismiss="modal" aria-label="Close"> <span aria-hidden="true">×</span> </button> </div> <div class="modal-body"> <slot name="body"></slot> </div> <div class="modal-footer"> <slot name="footer"></slot> </div> </div> </div> </div> </template>
现在,让我们使用这个:
<!-- somewhere else, using my-modal.vue --> <template> <my-modal> <template #header><!-- using the shorthand for `v-slot` --> <h5>Awesome Interruption!</h5> </template> <template #body> <p>We interrupt your use of our application to let you know that this application is awesome and you should continue using it every day for the rest of your life!</p> </template> <template #footer> <em>Now back to your regularly scheduled app usage</em> </template> </my-modal> </template>
上述类型的插槽用例显然非常有用,但它可以做的更多。
重用功能
Vue 组件不仅仅与 HTML 和 CSS 有关。 它们是用 JavaScript 构建的,因此它们也与功能有关。 插槽可用于创建功能一次并在多个地方使用它。 让我们回到我们的模态示例并添加一个关闭模态的函数:
<!-- my-modal.vue --> <template> <div class="modal" tabindex="-1" role="dialog"> <div class="modal-dialog" role="document"> <div class="modal-content"> <div class="modal-header"> <slot name="header"></slot> <button type="button" class="close" data-dismiss="modal" aria-label="Close"> <span aria-hidden="true">×</span> </button> </div> <div class="modal-body"> <slot name="body"></slot> </div> <div class="modal-footer"> <!-- using `v-bind` shorthand to pass the `closeModal` method to the component that will be in this slot --> <slot name="footer" :closeModal="closeModal"></slot> </div> </div> </div> </div> </template> <script> export default { //... methods: { closeModal () { // Do what needs to be done to close the modal... and maybe remove it from the DOM } } } </script>
现在当你使用这个组件时,你可以在页脚添加一个可以关闭模式的按钮。 通常,在 Bootstrap 模态的情况下,您可以将data-dismiss="modal"
添加到按钮,但我们希望将 Bootstrap 特定的东西从将插入此模态组件的组件中隐藏起来。 所以我们向他们传递了一个他们可以调用的函数,而他们对 Bootstrap 的参与并不知情:
<!-- somewhere else, using my-modal.vue --> <template> <my-modal> <template #header><!-- using the shorthand for `v-slot` --> <h5>Awesome Interruption!</h5> </template> <template #body> <p>We interrupt your use of our application to let you know that this application is awesome and you should continue using it every day for the rest of your life!</p> </template> <!-- pull in `closeModal` and use it in a button's click handler --> <template #footer="{closeModal}"> <button @click="closeModal"> Take me back to the app so I can be awesome </button> </template> </my-modal> </template>
无渲染组件
最后,您可以利用您所知道的使用插槽来传递可重用功能并剥离几乎所有的 HTML 并只使用插槽。 这就是无渲染组件的本质:一个只提供功能而没有任何 HTML 的组件。
使组件真正无渲染可能有点棘手,因为您需要编写render
函数而不是使用模板来消除对根元素的需求,但这可能并不总是必要的。 让我们看一个让我们首先使用模板的简单示例:
<template> <transition name="fade" v-bind="$attrs" v-on="$listeners"> <slot></slot> </transition> </template> <style> .fade-enter-active, .fade-leave-active { transition: opacity 0.3s; } .fade-enter, .fade-leave-to { opacity: 0; } </style>
这是一个奇怪的无渲染组件示例,因为它甚至没有任何 JavaScript。 这主要是因为我们只是创建了一个内置无渲染函数的预配置可重用版本: transition
。
是的,Vue 有内置的无渲染组件。 这个特定示例取自 Cristi Jora 的一篇关于可重用过渡的文章,并展示了一种创建无渲染组件的简单方法,该组件可以标准化整个应用程序中使用的过渡。 Cristi 的文章更深入,并展示了可重用过渡的一些更高级的变体,因此我建议您查看一下。
对于我们的另一个示例,我们将创建一个组件来处理在 Promise 的不同状态期间显示的内容的切换:挂起、成功解决和失败。 这是一种常见的模式,虽然它不需要大量代码,但如果不提取逻辑以实现可重用性,它可能会混淆很多组件。
<!-- promised.vue --> <template> <span> <slot name="rejected" v-if="error" :error="error"></slot> <slot name="resolved" v-else-if="resolved" :data="data"></slot> <slot name="pending" v-else></slot> </span> </template> <script> export default { props: { promise: Promise }, data: () => ({ resolved: false, data: null, error: null }), watch: { promise: { handler (promise) { this.resolved = false this.error = null if (!promise) { this.data = null return } promise.then(data => { this.data = data this.resolved = true }) .catch(err => { this.error = err this.resolved = true }) }, immediate: true } } } </script>
那么这里发生了什么? 首先,请注意我们正在接收一个名为promise
的Promise
道具。 在watch
部分,我们观察 Promise 的变化,当它发生变化时(或立即在组件创建时,感谢immediate
属性)我们清除状态,然后调用then
并catch
Promise,当它成功完成或更新状态时失败。
然后,在模板中,我们根据状态显示不同的插槽。 请注意,我们未能保持它真正的无渲染,因为我们需要一个根元素才能使用模板。 我们还将data
和error
传递给相关的插槽范围。
这是一个使用它的例子:
<template> <div> <promised :promise="somePromise"> <template #resolved="{ data }"> Resolved: {{ data }} </template> <template #rejected="{ error }"> Rejected: {{ error }} </template> <template #pending> Working on it... </template> </promised> </div> </template> ...
我们将somePromise
传递给无渲染组件。 当我们等待它完成时,我们正在显示“正在处理它……”,这要归功于pending
的插槽。 如果成功,我们会显示“Resolved:”和分辨率值。 如果失败,我们会显示“Rejected:”以及导致拒绝的错误。 现在我们不再需要在该组件中跟踪 Promise 的状态,因为该部分已被拉出到它自己的可重用组件中。
那么,我们可以做些什么来处理promised.vue
中的 slot 周围的span
呢? 要移除它,我们需要移除template
部分并向我们的组件添加一个render
函数:
render () { if (this.error) { return this.$scopedSlots['rejected']({error: this.error}) } if (this.resolved) { return this.$scopedSlots['resolved']({data: this.data}) } return this.$scopedSlots['pending']() }
这里没有什么太棘手的事情。 我们只是使用一些if
块来查找状态,然后返回正确的作用域槽(通过this.$scopedSlots['SLOTNAME'](...)
)并将相关数据传递给槽作用域。 当您不使用模板时,您可以跳过使用.vue
文件扩展名,方法是将 JavaScript 从script
标签中提取出来,然后将其插入.js
文件中。 在编译这些 Vue 文件时,这应该会给您带来非常轻微的性能提升。
这个例子是 vue-promised 的精简版和稍微调整的版本,我建议不要使用上面的例子,因为它们涵盖了一些潜在的陷阱。 还有很多其他很好的无渲染组件示例。 Baleada 是一个完整的库,其中包含提供此类有用功能的无渲染组件。 还有 vue-virtual-scroller 用于根据屏幕上的可见内容控制列表项的呈现,或 PortalVue 用于将内容“传送”到 DOM 的完全不同部分。
我出去了
Vue 的 slot 将基于组件的开发提升到了一个全新的水平,虽然我已经展示了很多可以使用 slot 的好方法,但还有更多的方法。 你能想到什么好主意? 您认为老虎机可以通过哪些方式升级? 如果您有任何想法,请务必将您的想法带给 Vue 团队。 上帝保佑编码愉快。