【Vue 进阶】从 slot 到无渲染组件
什么是插槽
插槽(slot
)通俗的理解就是“占坑”,在组件模板中占有位置,当使用该组件的时候,可以指定各个坑的内容。也就是我们常说的内容分发
值得一提的是,插槽这个概念并不是 Vue
提出的,而是 web Components
规范草案中就提出的,具体入门可以看 使用 templates and slots[1] ,Vue
只是借鉴了这个思想罢了
在 Vue 2.6.0 中,我们为具名插槽和作用域插槽引入了一个新的统一的语法 (即 v-slot
指令)。它取代了 slot
和 slot-scope
,这两个目前已被废弃但未被移除且仍在文档中的 attribute
本文的例子基于 Vue 2.6.X,所以用的都是 v-slot 的语法。
本文 DEMO
已全部放到 Github[2] 和 沙箱[3] 中,供大家学习,如有问题,欢迎评论提出。
默认插槽
我们新建父组件 Parent
和子组件 Child
,结构如下:
父组件:
<!-- 默认插槽 -->
<h3>默认插槽</h3>
<Child>
<div class="parent-text">Hi, I am from parent.</div>
</Child>
子组件:
<div class="child">
<slot></slot>
<div>Hello, I am from Child.</div>
</div>
父组件调用 Child
组件的时候,会在 Child
标签中将内容传入到子组件中的 <slot>
标签中,如下所示
也就是最后的渲染结果如下:
<div class="child">
<div class="parent-text">Hi, I am from parent.</div>
<div>Hello, I am from Child.</div>
</div>
后备内容
我们可以在子组件中的 <slot>
中加入一些内容,像下面一样
<div class="child">
<slot>当父组件不传值的时候,我就展示,我只是一个后备军</slot>
<div>Hello, I am from Child.</div>
</div>
当父组件调用的时候, 子组件标签内没有相关的内容时候,<slot>
标签内的内容就会生效,否则就不会渲染,可以理解就是个“备胎”
如父组件调用上面子组件:
<!-- 后备内容 -->
<h3>后备内容</h3>
<Child1></Child1>
结果如下:
具名插槽
当然,插槽可以不止一个,这个主要是为了能够灵活的控制插槽的位置以及组件的抽象。我们可以通过在子组件的 slot
标签中设置 name
属性,然后在父组件中通过 v-slot:
(或者使用简写 #
) + 子组件 name
属性值的方式指定要插入的位置。如果是默认插槽的话,v-slot:default
即可
如下父组件:
<!-- 具名插槽 -->
<h3>具名插槽</h3>
<Child2>
<template v-slot:footer><div>我是底部</div></template>
<template #header><div>我是头部</div></template>
<template v-slot:default>
<div>我是内容</div>
</template>
</Child2>
子组件
<div class="child">
<slot name="header"></slot>
<slot></slot>
<div>Hello, I am from Child.</div>
<slot name="footer"></slot>
</div>
需要留意的是,最后渲染的顺序是以子组件的顺序为主,也就是上面的例子,渲染出来如下:
作用域插槽
有时候,我们想在一个插槽中使用子组件的数据和事件,类似如下(注意:user
是定义在 Child3
组件中的数据):
<Child3>
<template>
<div>我的名字:{{user.name}}</div>
<div>我的年龄:{{user.age}}</div>
<button @click="callMe">Clicl Me</button>
</template>
</Child3>
会直接报错:
原因在于父组件取不到子组件的数据,这里记住一个原则:父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。
那我们怎样才能获取到子组件的数据或者事件呢?我们可以直接在子组件中通过 v-bind
的方式将数据或者事件传递给父组件中,如下所示
<div class="child">
<div>Hello, I am from Child.</div>
<!-- 将user和callMe通过 v-bind 的方式传递 -->
<slot :user="user" :callMe="callMe"></slot>
</div>
然后在父组件中的插槽内,通过类似 v-slot:default="slotProps"
接受子组件传递过来的数据
<Child3>
<!-- slotProps 可以自定义-->
<template v-slot:default="slotProps">
<div>我的名字:{{slotProps.user.name}}</div>
<div>我的年龄:{{slotProps.user.age}}</div>
<button @click="slotProps.callMe">Clicl Me</button>
</template>
</Child3>
以上 slotProps
可以自定义,而且可以使用解构赋值的语法
<!-- 解构赋值 -->
<template v-slot:other="{ user, callMe}">
<div>我的名字:{{user.name}}</div>
<div>我的年龄:{{user.age}}</div>
<button @click="callMe">Clicl Me</button>
</template>
实例:解耦业务逻辑和视图
我们经常会遇到一个场景,就是两个组件的业务逻辑是可以复用的,但是视图却不一样,比如我们经常会有类似切换开关的需求,功能包括:
关闭开关 打开开关 切换开关 开关关闭或者打开的时候不一样的内容
我们可以很快的写出它的一个 JS
业务逻辑代码:
export default {
data() {
return {
currentState: this.state
}
},
props: {
state: {
type: Boolean,
default: false
}
},
methods: {
openState() {
this.currentState = true;
},
closeState() {
this.currentState = false;
},
toggle() {
this.currentState = !this.currentState;
}
}
}
但是可能现在我的样式一是这样的
然而另外一个地方的样式是这样的(只是举个例子,现实可能更加的复杂,甚至有可能一些按钮直接就隐藏掉了)
这个时候,插槽就派上了用场。上面提到作用域插槽可以将数据和事件从子组件传递给父组件,这就相当于对外暴露了接口。而且可以将 HTML
中的 DOM
以及 CSS
交给父组件(调用方)去维护,子组件通过 <slot>
标签插入的位置即可,主要逻辑如下:
子组件:
<template>
<div class="toggle-container">
<slot :currentState="currentState" :setOn="openState" :setOff="closeState" :toggle="toggle"></slot>
</div>
</template>
父组件:
<Toggle1 :state="state" class="toggle-container-two">
<template v-slot:default="{currentState, setOn, setOff, toggle }">
<button @click="toggle">切换</button>
<button @click="setOff">关闭</button>
<button @click="setOn">打开</button>
<div v-if="currentState">我是打开的内容</div>
<div v-else>我是关闭的内容</div>
</template>
</Toggle1>
我们现在采用的是单文件的方式书写的,实际上子组件还是会有相关的 HTML
结构,如何做到子组件完全不需要渲染自己的 HTML
呢?那得了解下无渲染组件的实现
进阶:无渲染组件的实现
无渲染组件(renderless components)是指一个不需要渲染任何自己的 HTML
的组件。相反,它只管理状态和行为。它会暴露一个单独的作用域,让父组件或消费者完全控制应该渲染的内容。Vue
中,提供了单文件组件的写法。像上面的示例一样,我们始终还是在子组件中进行了一些渲染的操作,那如何做到真正的不渲染组件呢?
比如上面的 toggle
例子,我们已经做到了子组件暴露一个单独的作用域,让父组件或消费者完全控制应该渲染的内容。现在我们需要将单文件中的 template
结构(slot
标签外层的 div
)完全交给父组件,但单文件组件中 slot
标签是不能作为 template
的根元素的
这个时候,我们需要了解一下 Vue 渲染函数(render function
)
归根结底,Vue
及其所有的组件都只是 JavaScript
。单文件组件最后会被构建工具,如 webpack
,将 CSS
抽取形成一个文件,其他的内容会被转换成 JavaScript
,类似如下:
export default {
template: <div class="mood">...</div>,
data: () => ({ todayIsSunny: true })
}
当然,这个不是它的最终形态,模板编译器会提取 template
属性内容并将其内容编译为 JavaScript
,然后通过 render
函数添加到组件对象中。最终形态应该是如下:
render(h) {
return h(
'div',
{ class: 'mood' },
this.todayIsSunny ? 'Makes me happy' : 'Eh! Doesn't bother me'
)
}
具体的渲染函数可参见官网[4],虽然写 render
函数的成本会高一些,但是它的性能会比单文件组件好很多。
以上的例子,只有插槽的时候,我们只需要在 render
函数中,使用 this.$scopedSlots.default
代替掉 <slot>
标签即可
代码如下:
export const toggle = {
data() {
return {
currentState: this.state
}
},
render() {
return this.$scopedSlots.default({
currentState: this.currentState,
setOn: this.openState,
setOff: this.closeState,
toggle: this.toggle,
})
},
props: {
state: {
type: Boolean,
default: false
}
},
methods: {
openState() {
this.currentState = true;
},
closeState() {
this.currentState = false;
},
toggle() {
this.currentState = !this.currentState;
}
}
}
以上就可以做到子组件完全不渲染自己的 HTML
了
总结
本文介绍了一些 Vue
插槽的基本知识,包括
默认插槽 后备内容 具名插槽 作用域插槽
然后介绍了一下,如何通过插槽实现业务逻辑和视图的解耦,再结合渲染函数实现真正的无渲染函数
本文 DEMO
已全部放到 Github[5] 和 沙箱[6] 中,供大家学习,如有问题,可以评论提出。
这么用心了,求个赞,哈哈
希望对大家有所帮助~
往期优秀文章推荐
【Vue进阶】——如何实现组件属性透传?[7] 前端应该知道的 HTTP 知识【金九银十必备】[8] 最强大的 CSS 布局 —— Grid 布局[9] 如何用 Typescript 写一个完整的 Vue 应用程序[10] 前端应该知道的web调试工具——whistle[11]
参考:
Vue 插槽(slot)使用(通俗易懂)[12]
vue 2.6 中 slot 的新用法[13]
(译)函数式组件在Vue.js中的运用[14]
Building “Renderless” Vue Components[15]
参考资料
使用 templates and slots: https://developer.mozilla.org/zh-CN/docs/Web/Web_Components/Using_templates_and_slots
[2]Github: https://github.com/GpingFeng/vue-slot
[3]沙箱: https://codesandbox.io/s/hopeful-nash-id826?file=/src/main.js
[4]官网: https://cn.vuejs.org/v2/guide/render-function.html
[5]Github: https://github.com/GpingFeng/vue-slot
[6]沙箱: https://codesandbox.io/s/hopeful-nash-id826?file=/src/main.js
[7]【Vue进阶】——如何实现组件属性透传?: https://juejin.im/post/6865451649817640968
[8]前端应该知道的 HTTP 知识【金九银十必备】: https://juejin.im/post/6864119706500988935
[9]最强大的 CSS 布局 —— Grid 布局: https://juejin.im/post/6854573220306255880
[10]如何用 Typescript 写一个完整的 Vue 应用程序: https://juejin.im/post/6860703641037340686
[11]前端应该知道的web调试工具——whistle: https://juejin.im/post/6861882596927504392
[12]Vue 插槽(slot)使用(通俗易懂): https://juejin.im/post/6844903920037281805
[13]vue 2.6 中 slot 的新用法: https://juejin.im/post/6844903885476200461
[14](译)函数式组件在Vue.js中的运用: https://juejin.im/post/6844903752164442120
[15]Building “Renderless” Vue Components: https://css-tricks.com/building-renderless-vue-components/