vue二次封装一个高频可复用组件的全过程
作者:Maic
前言
在我们的业务里,我们通常会二次封装一些高频业务组件,比如弹框,抽屉,表单等这些业务组件,为什么要二次封装?我们所有人心里的答案肯定是,同样类似的代码太多了,我想复用组件,或者原有组件可能达不到我想要的效果,我想基于原有组件自定义一些自己的接口,那么此时就需要二次封装了。二次封装虽好,但同时也会带来一定的心智负担,因为二次封装的组件可能会变得不那么纯粹。
本文是一篇笔者关于二次封装组件的思考,希望看完在项目中有所思考和帮助。
正文开始...
在内容开始之前,本文主要从以下几个方向去思考:
1、二次组件必须继承原有组件的所有特性
2、二次组件名必须见名知意
3、自定义暴露出来的接口越简单越好
4、留有自定义插槽,让用户可以自己选择
5、封装二次的组件,能根据schame
数据配置,让组件更通用
继承原有组件接口
在之前的项目例子中,我们以一个弹框组件为例
我们看下在业务中一般是怎么写的
<template> <div class="list-app"> <div><a href="javascript:void(0)" rel="external nofollow" @click="handleToHello">to hello</a></div> ... <list-modal title="编辑" width="50%" v-model="formParams" :visible.sync="dialogVisible" @refresh="featchList" ></list-modal> </div> </template> <script> import { sourceDataMock } from '@/mock'; import ListModal from './ListModal'; export default { name: 'list', components: { ListModal, }, ... }; </script>
我们再继续看下list-modal
这个组件
<!--ListModal.vue--> <template> <el-dialog :visible.sync="currentVisible" width="30%" v-bind="$attrs" > <el-form label-position="left" label-width="80px" :model="formParams"> <el-form-item label="日期"> <el-input v-model="formParams.date"></el-input> </el-form-item> <el-form-item label="名称"> <el-input v-model="formParams.name"></el-input> </el-form-item> <el-form-item label="地址"> <el-input v-model="formParams.address"></el-input> </el-form-item> </el-form> <span slot="footer" class="dialog-footer"> <el-button @click="closeModal">取 消</el-button> <el-button type="primary" @click="handleSure">确 定</el-button> </span> </el-dialog> </template>
我们会发现,这个list-modal
业务组件只是包了一层,当我们使用v-bind="$attrs"
时,vue
提供的这个api
会将父组件所有的props
继承,官方给了一大段解释
- $attrs
包含了父作用域中不作为 prop 被识别 (且获取) 的 attribute 绑定 (class 和 style 除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 (class 和 style 除外),并且可以通过 v-bind="$attrs" 传入内部组件——在创建高级别的组件时非常有用。
首先我们思考为什么要用这个$attrs
?上面一段话的意思是,父组件class
与style
会排除
我们从页面上可以看出title
与width
都是父组件传过来的,但是我们发现,实际上这两个外部看似自己传入的props
也是el-dialog
的props
,所以说我们必须要保持自己二次封装的组件也有el-dialog
所有能力,所以此时v-bind='$attrs'
就可以做到了
- $listeners
包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件——在创建更高层次的组件时非常有用。
在以上的$attrs
我们是将父级的所有的props
都拿到了,但是自定义事件呢,所以才有的了$listeners
所以你在父组件写了一个el-dialog
的自定义事件想要生效,那么必须在子组件绑定$listeners
<!--list/ListModal.vue--> <el-dialog :visible.sync="currentVisible" width="30%" v-bind="$attrs" v-on="$listeners" > ... </el-dialog>
正常来说一个高阶二次组件必须要有v-bind="$attrs"
与v-on="$listeners"
另外我们自己封装的二次组件里有v-model='formParams'
这个formParams
就是我们弹框内部表单的使用内容
v-model
关于v-model
实际上官方解释就是用在组件或者表单上创建双向绑定,如果把v-model
看成是一个内部提供的一个语法糖,那么它可以拆解成:value="value"
与:input=“handleInput”
,v-model
不仅仅是可以作用在表单元素上,并且还可以作用在组件上,同时也提供了一个model
的接口,提供自定义修改事件名称
<script> export default { name: 'list-modal', model: { prop: 'formParams', event: 'change', }, props: { visible: { type: Boolean, default: false, }, formParams: { type: Object, }, }, data() { return { currentVisible: false, }; }, watch: { visible(bool) { this.currentVisible = bool; }, currentVisible(bool) { this.$emit('update:visible', bool); }, } }; </script>
以上代码就自定义了model
的event
,prop
就是formParams
,同时props
上必须有引入formParams
不知道你有没有好奇,为啥我data
中定义了一个currentVisible
,而且watch
了visible
与currentVisible
,使用currentVisible
时,这里是有一个坑,因为弹框的icon
关闭操作不会触发最外层事件,也就是你点击右上角的关闭操作后,当你再次打开时,此时,就打不开了,所以就没直接用visible
了,我们需要另一个变量,然后去watch
最终达到我们需要的效果。
在这里有人会奇怪,传入子组件的formParams
直接在表单上使用了,嘿,这样不是直接修改props
吗,但实际上控制台并不会报错,如果你父组件传入的是一个基础数据类型,你在子组件里修改是会直接警告你不能修改的,但是你传入的是一个对象,你此时修改的是对象属性值,并没有修改原对象,所以一个非基础数据类型数据,修改内部值时,是不会警告的,这样做也是ok的。
插槽
在这个弹框中的确认和取消操作是用插槽slot="footer"
去显示的,如果你想自定义插槽,那么你可以通过具名插槽进行兼容处理
<el-dialog :visible.sync="currentVisible" width="30%" v-bind="$attrs" v-on="$listeners" > ... <template v-if="$slots.footer"> <slot name="footer" /> </template> <span v-else slot="footer" class="dialog-footer"> <el-button @click="closeModal">取 消</el-button> <el-button type="primary" @click="handleSure">确 定</el-button> </span> </el-dialog>
在我们的业务中有大量这样的XXXModal
弹框,如果我们只是这样包了一层,那么我们只是完成了组件的基本使用,也是符合我们常规业务需求,但是你会发现,我们绝大部份业务里的弹框内容都是表单,所以我能不能通过可配置的schame
数据去配置出来呢?
组件更抽象
我们在components
下新建了一个form-modal
组件,并注册成全局组件,我的目标是把弹框的内容区域做成可配置化,这样我只需要用配置数据就可以渲染出对应的内容
<!--src/components/form-modal/view/index.vue--> <template> <div class="form-modal"> <el-dialog :visible.sync="currentVisible" v-bind="$attrs" v-on="$listeners"> <el-form v-bind="formConfig.formAttrs" :model="formParams"> <div v-for="(item, index) in formConfig.fields" :key="index"> <el-form-item :label="item.label"> <!--自定义插槽--> <template v-if="item.slot"> <slot :name="item.slot" :row="{ ...item, formParams, index }" /> </template> <!--文本or文本域--> <template v-else-if="['text', 'textarea'].includes(item.type)"> <el-input :type="item.type" v-bind="item.attrs || {}" v-model="formParams[item.key]" ></el-input> </template> <!--下拉框--> <template v-else-if="item.type === 'select'"> <el-select v-bind="item.attrs" v-model="formParams[item.key]"> <el-option v-for="(sitem, index) in item.options.data" :key="index" :label="sitem[item.options.extraProps.label]" :value="sitem[item.options.extraProps.value]" > </el-option> </el-select> </template> </el-form-item> </div> </el-form> <span slot="footer" class="dialog-footer"> <el-button @click="closeModal">取 消</el-button> <el-button type="primary" @click="handleSure">确 定</el-button> </span> </el-dialog> </div> </template>
全局注册
// src/components/index.js import Vue from 'vue'; import FormModal from './form-modal'; const custCompoment = { FormModal, }; export const installCustComponent = () => { Object.keys(custCompoment).forEach((key) => { Vue.component(key, custCompoment[key]); }); };
main.js
// main.js import { installCustComponent } from '@/components'; installCustComponent(); ...
我们发现在模版里面有不少添加条件,实际上,这些条件主要根据你业务需要而定,除了模版方式,插槽,我们也可以预留一个自定义formater
的接口,像下面这样
<!--src/components/form-modal/view/index.vue--> <div v-for="(item, index) in formConfig.fields" :key="index"> <el-form-item :label="item.label"> <!--自定义render--> <template v-if="item.formater"> <component :is="'renderComponent'" :value="formParams[item.key]" :input="e => formParams[item.key] = e" v-bind="{ ...item }" ></component> </template> <!--自定义插槽--> <template v-else-if="item.slot"> <slot :name="item.slot" :row="{ ...item, formParams, index }" /> </template> <!--文本or文本域--> ... </el-form-item> </div>
那么此时你会发现有一个renderComponent
这样的自定义组件,我们必须引入进来
/* src/components/form-modal/view/render.js*/ export default { functional: true, props: ['value'], render(h, ctx) { const { formater, attrs, input: handleInput } = ctx.data.attrs; return formater(h, { attrs: { ...attrs, value: ctx.props.value, }, on: { input(e) { handleInput(e); }, }, }); }, };
在form-modal/view/index.vue
中我们必须引入,所以模版中就可以使用了
<script> // src/components/form-modal/view/index.vue import renderComponent from './render'; export default { name: 'form-modal', model: { prop: 'formParams', event: 'change', }, components: { renderComponent, }, props: { visible: { type: Boolean, default: false, }, formParams: { type: Object, }, formConfig: { type: Object, }, }, ... </script>
我们再看下我们之前业务弹框与schame
再次抽象后的两个组件,其实第二个全局组件就多了一个formConfig
属性,我们统一把内容抽离了出去,我们的form-modal
就变得更加通用,我们只需要关注formConfig
这份配置数据就行
/* eslint-disable func-names */ <template> <div class="list-app"> ... <list-modal title="编辑" width="50%" class="list-modal" style="border: 1px solid transparent" v-model="formParams" :visible.sync="dialogVisible" @refresh="featchList" @close="handleClose" > <div slot="footer">确定</div> </list-modal> <form-modal title="编辑" width="50%" class="list-modal" style="border: 1px solid transparent" v-model="formParams" :formConfig="formConfig" :visible.sync="dialogVisible2" @refresh="featchList" @close="handleClose" > <template slot-scope="{ row }" slot="number"> <el-input :type="row.type" v-bind="row.attrs || {}" v-model="row.formParams[row.slot]" ></el-input> </template> </form-modal> </div> </template> <script> import { sourceDataMock } from '@/mock'; import ListModal from './ListModal'; export default { name: 'list', components: { ListModal, }, data() { return { ... tableData: [], dialogVisible: false, dialogVisible2: false, formParams: { date: '', name: '', address: '', number: '1', scholl: '公众号:Web技术学苑', }, }; }, computed: { formConfig() { return { formAttrs: { labelWidth: '80px', labelPosition: 'left', }, fields: [ { type: 'text', key: 'date', label: '日期', attrs: { placeholder: '请填写日期', }, }, { type: 'text', key: 'name', label: '名称', attrs: { placeholder: '请填写名称', }, }, { type: 'select', key: 'address', label: '地址', attrs: { placeholder: '请选择地址', style: { width: '100%', }, }, options: { data: this.tableData, extraProps: { value: 'address', label: 'address', }, }, }, { type: 'text', slot: 'number', label: '编号', attrs: { placeholder: '请输入编号', }, }, { type: 'text', key: 'scholl', label: '毕业学校', attrs: { placeholder: '请输入毕业学校', }, formater: (h, props) => h('el-input', { ...props, }), }, ], }; }, }, }; </script> <style scoped> .list-app .el-form { text-align: left; } </style>
看下最终的结果
在我们自定义一个formater
的接口,我们注意到,实际上这里有用vue
的纯函数组件,我们注意到在render.js
中我们是申明了functional: true
,这里会有巨坑,如果是一个函数组件,在render
函数中是获取不到this
的,只能通过第二个ctx
参数获取父组件传入的props
信息
/* eslint-disable no-param-reassign */ export default { functional: true, props: ['value'], render(h, ctx) { // console.log(this, '---'); // 会是null,只能通过第二个参数ctx拿对应参数 const { formater, attrs, input: handleInput } = ctx.data.attrs; return formater(h, { attrs: { ...attrs, value: ctx.props.value, }, on: { input(e) { handleInput(e); }, }, }); }, };
并且我们修改数据,我们发现我们用了一个父组件传入的一个回调函数去修改,这在react
很常见,这里我们也是通过回调方式修改数据,因为vue
数据流是单向的,所以只能这种方式去修改了
因此在业务中我们的form-modal
就变得更通用,更高频了,这样会减少你重复劳动的时间,你只需要关注配置接口信息就行。
但是这样带来的负担是有的,如果这个form-modal
耦合了太多业务逻辑,那么带来的心智负担是有的,当你二次封装的一个高频组件,你组内小伙伴不能像使用第三方组件库那么快捷时,说明组件的接口设计还有提高的空间,判断一个组件好不好用的标准就是,零负担,而且人人能改,人人都能改动,如果因为业务特殊,当我们考虑二次封装一个组件参杂很多业务逻辑判断时,那我的观点是,还是不要进行二次封装了。
总结
以一个弹框组件为例,我们二次封装组件到底需要注意哪些问题,以及我们必须注意些什么,核心思想就是继承原有组件的特性,
v-bind='$attrs'
与v-on="$listeners"
是核心当我们二次封装一个组件时,我们自定义的一些接口能少就少,组件名必须见名知意
二次封装的组件不仅仅只是包一层,我们可以尝试用数据配置方式让组件更通用,预留一些接口插槽,或者自定义
formater
函数,不强制约束,让组件灵活性拓展性更强些组件的
props
名字尽量不要带来负担,最好与原有组件props
保持一致本文 code example
到此这篇关于vue二次封装一个高频可复用组件的文章就介绍到这了,更多相关vue二次封装高频可复用组件内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!