最近看的组件封装和高阶组件的相关概念,做一下简单的总结。
高阶组件是什么
在说高阶组件之前,有一个概念是高阶函数。
简单地说,高阶函数也就是传入一个函数,最后返回一个新函数。
那么高阶组件,就是函数接收一个组件,返回新的组件,并对组件做一层包装拓展。
在 vue 中,vue 单文件会被转换为对象,template 部分会被转换为对象中的 render 函数。
所以思路就是对该对象进行一些处理。
有点像装饰器模式。
恶心的弹窗组件
每次在写 admin 页面的时候,总是会出现这样的代码,以 elementUI 为例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| <template> <el-dialog :visible.sync="visible1" :title="title1"> <my-com /> <div slot="footer"> <el-button>Confirm</el-button> </div> </el-dialog> <el-dialog :visible.sync="visible2" :title="title2"> <my-com /> <div slot="footer"> <el-button>Confirm</el-button> </div> </el-dialog> <el-dialog :visible.sync="visible3" :title="title3"> <my-com /> <div slot="footer"> <el-button>Confirm</el-button> </div> </el-dialog> <el-dialog :visible.sync="visible4" :title="title4"> <my-com /> <div slot="footer"> <el-button>Confirm</el-button> </div> </el-dialog> </template> <script> import MyCom from 'my-com' export default { data() { return { visible1: false, visible2: false, visible3: false, visible4: false, title1: 'title1', title2: 'title2', title3: 'title3', title4: 'title4' } }, } </script>
|
这样写非常重复,并且在状态很多的时候,管理起来会很恶心。
visible,title 这一类变量,其实属于弹窗自身的内部属性,如果在代码的其他地方没有使用,那么最好是隐藏在弹窗组件中,没必要暴露出来。
我们希望只关心弹窗内部的组件部分(my-com),而不是每次都做一些繁复的无用的代码拷贝。
组件封装
常见的做法,是将弹窗再封装一层,将 my-com 作为插槽的内容。如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| <template> <el-dialog :visible.sync="visible"> <slot /> <div slot="footer"> <el-button>Confirm</el-button> </div> </el-dialog> </template> <script> import MyCom from 'my-com' export default { props: ['visible'], data() { return { visible: true, } }, methods: [ open() { this.visible = true } ], } </script>
|
使用:
1 2 3 4 5 6 7 8 9 10 11
| <template> <my-dialog ref="dialog"> <my-com> </my-dialog> </template> <script> import MyCom from 'my-com' export default { components: { MyCom } } </script>
|
这时有一个问题,el-dialog 被我们包含在组件内部了,如果要为其添加一些属性怎么办,例如前面的 title。
一种方法是增加 props
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <template> <el-dialog :visible.sync="visible" :title="title"> <slot /> <div slot="footer"> <el-button>Confirm</el-button> </div> </el-dialog> </template> <script> export default { props: ['visible', 'title'], } </script>
|
缺点是灵活性较差,每新增一个属性就需要改一次组件、增加一个 prop。
更好的办法是使用 $attrs、$props、$listener 等。其中:
- $attrs 包含了 dom 上所有属性
- $props 包含了已经在 props 上声明的属性
- $listeners 包含了所有事件属性
my-dialog.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <template> <el-dialog :visible.sync="visible" v-bind="$attrs" v-on="$listeners"> <slot /> <div slot="footer"> <el-button>Confirm</el-button> </div> </el-dialog> </template> <script> export default { props: ['visible'], } </script>
|
如果不想把从父组件传递的参数或事件全都传递给原组件,需要做部分筛选,可以使用计算属性 computed:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| <template> <el-dialog :visible.sync="visible" v-bind="dialogAttr" v-on="dialogListeners"> <slot /> <div slot="footer"> <el-button>Confirm</el-button> </div> </el-dialog> </template> <script> export default { props: ['visible'], computed: { dialogProps() { const { title } = this.$attrs return { title, } }, dialogListeners() { const { close } = this.$listeners return { close, } }, }, } </script>
|
但还有一种使用场景,我们希望使用起来更加简洁,类似于 elementUI 的 this.$confirm。
如果没有 elementUI, 或者项目中已经有一个实现好的弹窗了,这种情况怎么基于现有组件,实现类似于 this.$confirm 的调用呢。
基于现有弹窗实现 this.$dialog
这种实现形式称为 HOC 高阶组件。
假设我们已经有一个弹窗组件(用 elementUI 的 el-dialog 为例)。
- 首先,引入弹窗组件。
1 2 3
| import { Button as ElButton, Dialog as ElDialog } from 'element-ui' import Vue from 'vue'
|
- 定义弹窗的几种状态,确定,关闭,和取消。
1 2 3 4 5 6 7 8 9
| let instance let promise
const statusMap = { confirm: 'resolve', close: 'reject', cancel: 'reject', }
|
定义一个方法,这个方法接收两个参数:
该方法接收弹窗组件,并在其基础上,包裹了一层,添加了新的方法,如 open,cancel 等等。
实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| const hoc = function (com, options) { return { name: 'MyDialog', components: { com }, data() { return { visible: false, loading: false } }, methods: { open() { this.visible = true }, close() { this.visible = false promise && promise.reject() }, handler() { this.visible = true return new Promise((resolve, reject) => (promise = { resolve, reject })) }, _action(type, beforeAction) { const done = () => { this._hide(type) } if (typeof beforeAction === 'function') { beforeAction(done) } else { done() } }, _hide(type) { this.visible = false this.loading = false if (promise) { promise[statusMap[type]](this.$refs.child) return } this.$emit(type) }, }, render() { } }
|
- 由于我们没办法在 js 中写 template 标签,这个时候 render 函数就派上用场了。(也可以用 JSX)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
| const hoc = function (com, options) { return { name: 'MyDialog', components: { com }, render(h) { const _this = this const args = { props: { ...(options || {}), ...this.$attrs, visible: this.visible, }, on: { ...this.$listeners, 'update:visible': (val) => (_this.visible = val), }, scopedSlots: this.$scopedSlots, ref: 'dialog', } const footer = h('div', { slot: 'footer' }, [ h( ElButton, { on: { click: () => _this._action('cancel', args.props['before-cancel']), }, }, 'Cancel' ), h( ElButton, { props: { type: 'primary', loading: _this.loading, }, on: { click: () => _this._action('confirm', args.props['before-confirm']), }, }, 'Confirm' ), ]) return h(ElDialog, args, [ h(com, { props: options?.props, ref: 'child', }), footer, ]) }, } }
|
- 挂载方法,将生成的弹窗挂载到指定的位置,如果没有指定位置,将插入到 body 中。
其中 Vue.extend 方法,会接受一个组件,并返回该组件的构造器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| const dialogService = function (com, options) { const DialogConstructor = Vue.extend(hoc.call(this, com, options)) instance = new DialogConstructor() let target = document.body if (typeof options.el === 'string') { target = document.querySelector(options.el) || target } if (options.el instanceof HTMLElement) { target = options.el } instance.$mount() target.appendChild(instance.$el) instance.visible = true return instance }
|
- 这里提供两种输出, 一是 MyDialog 组件实例, 二是 $dialog 方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| const $dialog = function (component, options = {}) { return dialogService.call(this, component, options) }
$dialog.confirm = function (component, options = {}) { return $dialog(component, options).handler() }
const MyDialog = hoc()
export { $dialog, MyDialog }
|
- 最后,提供一个 install 方法,方便用 Vue.use() 安装到全局 Vue 的原型上。
1 2 3 4 5 6 7
| export default { install(Vue) { Vue.prototype.$dialog = $dialog }, }
|
这样,就可以直接调用 this.$dialog,而不需要在模板中写新元素了。
-
版权声明: 本博客所有文章除特别声明外,均采用
CC BY 4.0 CN协议
许可协议。转载请注明出处!
Жизнь, как качели - то вверх, то вниз.