vue-高阶组件

As being evident from the discussion above, implementing HoCs in Vue isn't as trivial as in React because Vue's API and functionality is broader and more edge cases have to be taken care of.

灵感来源

开发 admin-ui 组件

https://knpm.klk.io/-/web/detail/@klk/admin-ui

本文也会以封装 element 组件为例

目标

封装一个成熟的组件,并达成以下几点

  • 传递给 HOC 的参数能正常传递给原组件
  • 传递给 HOC 的事件能正常绑定到原组件
  • 给 HOC 传递的 slot 能正常传递给原组件
  • HOC 内部能获取到从父组件传递的参数、事件、slot,并针对 HOC 的目标开发自定义功能

开发方式

v-bind="$attrs", v-on="$listeners"

  • 通过对原组件 v-bind="$attrs", v-on="$listeners" 将从父组件传递的参数和事件原样传递给原组件:
1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- parent.vue -->
<my-hoc
:data="tableData"
:border="true"
@select="handleSelect"
@select-all="handleSelectAll"
/>

<!-- myHoc.vue -->
<el-table
v-bind="$attrs"
v-on="$listeners"
/>

从 parent.vue 传递的参数 data, border 和事件 select, select-all 会原样传递给 el-table

  • 如果不想把从父组件传递的参数或事件全都传递给原组件,需要做部分筛选,可以使用计算属性 computed:
1
2
3
4
5
<!-- myHoc.vue -->
<el-table
v-bind="tableProps"
v-on="tableListeners"
/>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
export default {
computed: {
tableProps() {
const {
data
} = this.$attrs
return {
data
}
},
tableListeners() {
const {
select
} = this.$listeners
return {
select
}
}
}
}
  • 如果同时对原组件传递 $attrs 和与 $attrs 的某个 key 重名的参数,HOC 传递的参数优先级高于父组件:
1
2
3
4
5
6
7
8
9
10
<!-- parent.vue -->
<my-hoc
:data="tableData"
/>

<!-- myHoc.vue -->
<el-table
v-bind="$attrs"
:data="myData"
/>

el-table 会取从 HOC 传递的 myData

  • 如果同时对原组件传递 $listeners 和与 $listeners 的某个 key 重名的事件,原组件触发事件时,这两个函数都会调用,HOC 传递的事件函数执行顺序优先级高于父组件传递的事件函数:
1
2
3
4
5
6
7
8
9
10
<!-- parent.vue -->
<my-hoc
:select="handleSelect"
/>

<!-- myHoc.vue -->
<el-table
v-on="$listeners"
@select="myHandleSelect"
/>

当 el-table 触发 select 事件时,先触发函数 myHandleSelect,后触发函数 handleSelect

  • 将从父组件传递的 slot 传递给原组件:
1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- parent.vue -->
<my-hoc>
<template #mySlot>
<h1>el-table Slot Append</h1>
</template>
</my-hoc>

<!-- myHoc.vue -->
<el-table>
<template #append>
<slot name="mySlot" />
</template>
</el-table>

这里会将从父组件传递的名为 mySlot 的 slot 传递给 el-table 的 append slot,mySlot 的命名可自定义

  • 如果需要在 HOC 组件内以 this.xxxProp 的方式调用从父组件传递的参数,则需要在 HOC 内定义对应的 prop,否则只能以 this.$attrs.xxxProp 的方式调用。在 HOC 内定义了 prop 后,指定参数会从 HOC 的 $attrs 中转移到 $props,此时需要注意给 el-table 手动传参。

  • 根据 HOC 需要灵活设置 HOC 组件参数 inheritAttrs

此方式的优劣

优点

  • 代码风格较简洁
  • 易于理解,便于维护
  • Vue 官方推荐的写 HOC 时原样传递参数和事件的方式,容错率较高

缺点

  • 参数在 $attrs 和 $props 之间的切换可能会很频繁
  • 事件修饰符无法传递,需要以其它方式替代
  • 需要按照原组件的 slot 定义,逐个在 HOC 模版内如数定义一遍,slot 数量多时会引出较多代码量

相关资料

渲染函数 h() / createElement()

  • Vue 组件的选项之一,使用方式入口 h(),使用前墙裂建议先熟悉一下 基础
  • 适合场景:需要 JavaScript 的完全编程的能力
  • 关于渲染函数的介绍和使用,vue 官方文档已经介绍的比较全面,这里再介绍一些关于插槽的使用,也是我开发时花了比较多时间去理解的
1
2
3
4
5
6
7
8
9
10
{
// Vue 官方文档关于插槽相关属性的介绍:
// 作用域插槽的格式为
// { name: props => VNode | Array<VNode> }
scopedSlots: {
default: props => createElement('span', props.text)
},
// 如果组件是其它组件的子组件,需为插槽指定名称
slot: 'name-of-slot',
}

属性 slot:

1
2
3
<div>
<h1 v-slot:myslot>我是 mySlot</h1>
</div>

用 h() 对这个组件渲染,有多种方式,这里介绍使用到属性 slot 的方式:

1
2
3
4
5
6
7
8
9
10
11
12
h(
'div',
[
h(
'h1',
{
slot: 'myslot'
},
'我是 myslot'
)
]
)

思路:定义当前组件时,告诉这个组件它是它的父组件里面插槽名为 slot 对应值的内容

属性 scopedSlots:

1
2
3
4
5
6
7
<div>
<h1 v-slot:myslot1>我是 myslot1</h1>
<template v-slot:myslot2>
<h2>我是 myslot2 的 h2</h2>
<h3>我是 myslot2 的 h3</h3>
</template>
</div>

用 h() 对这个组件渲染,有多种方式,这里介绍使用到属性 scopedSlots 的方式:

1
2
3
4
5
6
7
8
9
10
11
12
h(
'div',
{
scopedSlots: {
myslot1: (props) => h('h1', '我是 myslot1'),
myslot2: (props) => [
h('h2', '我是 myslot2 的 h2')
h('h3', '我是 myslot2 的 h3')
]
}
}
)

思路:定义当前组件时,告诉这个组件它的各个 scopedSlots key 对应的内容分别要渲染什么

总结:可以理解为一个是被动,一个是主动

  • 插槽命名 不要 用驼峰式命名,有 bug

此方式的优劣

优点

  • 还是那句话,发挥了 Javascript 的编程能力,编程自由度高
  • 相比上一种方式,在插槽方面的处理,要优秀的很多,因为可以用编程的方式批量处理,同样,其它属性的赋值也可批量处理

缺点

  • 代码较冗长,不方便维护,开发成本较高
  • 要替代模版中的 v-model 指令,代价很大,因为不同表单组件的方式可能不一样,容易写出 bug,有点类似于开发 react 的受控组件和非受控组件的感觉
  • 类似于 v-model ,部分事件修饰符也需要手动实现,也有部分是 vue 有提供捷径的

相关资料

JSX ( To Be Completed…… )