实战 6:可用 slot-scope 自定义列的表格组件——Table
上一节,我们基于 Render 函数实现了在表格中自定义列模板的组件 Table,虽说 Render 函数能够完全发挥 JavaScript 的编程能力,实现几乎所有的自定义工作,但本质上,使用者写的是一个庞大的 JS 对象,它不具备 DOM 结构,可读性和可维护性都比较差。对于大部分写 Vue.js 的开发者来说,更倾向于使用 template 的语法,毕竟它是 Vue.js 独有的特性。本小节则在上一节的 Table 组件基础上修改,实现一种达到同样渲染效果,但对使用者更友好的 slot-scope 写法。
什么是 slot-scope
slot(插槽)我们都很熟悉,它是 Vue.js 组件的 3 个 API 之一,用于分发内容。那 slot-scope 是什么呢?先来看一个场景,比如某组件拥有下面的模板:
-
{{ book.name }}
使用者传递一个数组 books
,由组件内的 v-for
循环显示,这里的 {{ book.name }}
是纯文本输出,如果想自定义它的模板(即内容分发),就要用到 slot,但 slot 只能是固定的模板,没法自定义循环体中的一个具体的项,事实上这跟上一节的 Table 场景是类似的。
常规的 slot 无法实现对组件循环体的每一项进行不同的内容分发,这就要用到 slot-scope,它本质上跟 slot 一样,只不过可以传递参数。比如上面的示例,使用 slot-scope 封装:
-
{{ book.name }}
在 slot 上,传递了一个自定义的参数 book
,它的值绑定的是当前循环项的数据 book,这样在父级使用时,就可以在 slot 中访问它了:
限时优惠
{{ slotProps.book.name }}
使用 slot-scope 指定的参数 slotProps
就是这个 slot 的全部参数,它是一个对象,在 slot-scope 中是可以传递多个参数的,上例我们只写了一个参数 book
,所以访问它就是 slotProps.book
。这里推荐使用 ES6 的解构,能让参数使用起来更方便:
限时优惠
{{ book.name }}
除了可以传递参数,其它用法跟 slot 是一样的,比如也可以“具名”:
{{ book.name }}
限时优惠
{{ book.name }}
这就是作用域 slot(slot-scope),能够在组件的循环体中做内容分发,有了它,Table 组件的自定义列模板就不用写一长串的 Render 函数了。
为了把 Render 函数和 slot-scope 理解透彻,下面我们用 3 种方法来改写 Table,实现 slot-scope 自定义列模板。
方案一
第一种方案,用最简单的 slot-scope 实现,同时也兼容 Render 函数的旧用法。拷贝上一节的 Table 组件目录,更名为 table-slot
,同时也拷贝路由,更名为 table-slot.vue
。为了兼容旧的 Render 函数用法,在 columns 的列配置 column 中,新增一个字段 slot
来指定 slot-scope 的名称:
{{ col.title }}
{{ row[col.key] }}
相比原先的文件,只在 'render' in col
的条件下新加了一个 template
的标签,如果使用者的 column 配置了 render 字段,就优先以 Render 函数渲染,然后再判断是否用 slot-scope 渲染。在定义的作用域 slot 中,将行数据 row
、列数据 column
和第几行 index
作为 slot 的参数,并根据 column 中指定的 slot 字段值,动态设置了具名 name
。使用者在配置 columns 时,只要指定了某一列的 slot,那就可以在 Table 组件中使用 slot-scope。我们以上一节的可编辑整行数据为例,用 slot-scope 的写法实现完全一样的效果:
{{ row.name }}
{{ row.age }}
{{ getBirthday(row.birthday) }}
{{ row.address }}
示例中在
内的每一个 就对应某一列的 slot-scope 模板,通过配置的
slot
字段,指定具名的 slot-scope。可以看到,基本是把 Render 函数还原成了 html 的写法,这样看起来直接多了,渲染效果是完全一样的。在 slot-scope 中,平时怎么写组件,这里就怎么写,Vue.js 所有的 API 都是可以直接使用的。
方案一是最优解,一般情况下,使用这种方案就可以了,其余两种方案是基于 Render 的。
方案二
第二种方案,不需要修改原先的 Table 组件代码,只是在使用层面修改即可。先来看具体的使用代码,然后再做分析。注意,这里使用的 Table 组件,仍然是上一节 src/components/table-render
的组件,它只有 Render 函数,没有定义 slot-scope:
{{ row.name }}
{{ row.age }}
{{ getBirthday(row.birthday) }}
{{ row.address }}
在 slot-scope 的使用上(即 template 的内容),与方案一是完全一致的,可以看到,在 column 的定义上,仍然使用了 render 字段,只不过每个 render 都渲染了一个 div 节点,而这个 div 的内容,是指定来在
中定义的 slot-scope:
render: (h, { row, column, index }) => {
return h(
'div',
this.$refs.table.$scopedSlots.name({
row: row,
column: column,
index: index
})
)
}
这正是 Render 函数灵活的一个体现,使用 $scopedSlots
可以访问 slot-scope,所以上面这段代码的意思是,name 这一列仍然是使用 Functional Render,只不过 Render 的是一个预先定义好的 slot-scope 模板。
有一点需要注意的是,示例中的 data
默认是空数组,而在 mounted 里才赋值的,是因为这样定义的 slot-scope,初始时读取 this.$refs.table.$scopedSlots
是读不到的,会报错,当没有数据时,也就不会去渲染,也就避免了报错。
这种方案虽然可行,但归根到底是一种 hack,不是非常推荐,之所以列出来,是为了对 Render 和 slot-scope 有进一步的认识。
方案三
第 3 中方案的思路和第 2 种是一样的,它介于方案 1 与方案 2 之间。这种方案要修改 Table 组件代码,但是用例与方案 1 完全一致。
在方案 2 中,我们是通过修改用例使用 slot-scope 的,也就是说 Table 组件本身没有支持 slot-scope,是我们“强加”上去的,如果把强加的部分,集成到 Table 内,那对使用者就很友好了,同时也避免了初始化报错,不得不把 data 写在 mounted 的问题。
保持方案 1 的用例不变,修改 src/components/table-render
中的代码。为了同时兼容 Render 与 slot-scope,我们在 table-render
下新建一个 slot.js 的文件:
// src/components/table-render/slot.js
export default {
functional: true,
inject: ['tableRoot'],
props: {
row: Object,
column: Object,
index: Number
},
render: (h, ctx) => {
return h('div', ctx.injections.tableRoot.$scopedSlots[ctx.props.column.slot]({
row: ctx.props.row,
column: ctx.props.column,
index: ctx.props.index
}));
}
};
它仍然是一个 Functional Render,使用 inject
注入了父级组件 table.vue(下文改写) 中提供的实例 tableRoot
。在 render 里,也是通过方案 2 中使用 $scopedSlots
定义的 slot,不过这是在组件级别定义,对用户来说是透明的,只要按方案 1 的用例来写就可以了。
table.vue 也要做一点修改:
{{ col.title }}
{{ row[col.key] }}
因为 slot-scope 模板是写在 table.vue 中的(对使用者来说,相当于写在组件
之间),所以在 table.vue 中使用 provide 向下提供了 Table 的实例,这样在 slot.js 中就可以通过 inject 访问到它,继而通过 $scopedSlots
获取到 slot。需要注意的是,在 Functional Render 是没有 this 上下文的,都是通过 h 的第二个参数临时上下文 ctx 来访问 prop、inject 等的。
方案 3 也是推荐使用的,当 Table 的功能足够复杂,层级会嵌套的比较深,那时方案 1 的 slot 就不会定义在第一级组件中,中间可能会隔许多组件,slot 就要一层层中转,相比在任何地方都能直接使用的 Render 就要麻烦了。所以,如果你的组件层级简单,推荐用第一种方案;如果你的组件已经成型(某 API 基于 Render 函数),但一时间不方便支持 slot-scope,而使用者又想用,那就选方案 2;如果你的组件已经成型(某 API 基于 Render 函数),但组件层级复杂,要按方案 1 那样支持 slot-scope 可能改动较大,还有可能带来新的 bug,那就用方案 3,它不会破坏原有的任何内容,但会额外支持 slot-scope 用法,关键是改动简单。
结语
理论上,绝大多数能用 Render 的地方,都可以用 slot-scope。对于极客来说,喜欢挑战各种新奇的写法,所以会在 Vue.js 中大量使用 Render 函数、JSX 甚至 TS;而对于求稳的开发者来说,常规的 template、slot、slot-scope 写法会是好的选择。如果非要选一种,那要从你团队的整体情况来定,如果团队大部分是写后端为主的,那可能更倾向于 TS;如果写过 React,或许 JSX 是不错的选择;如果实在不知道选什么,那就求稳吧!