IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> JavaScript知识库 -> vue如何二次封装一个高频可复用的组件 -> 正文阅读

[JavaScript知识库]vue如何二次封装一个高频可复用的组件

在我们的业务里,我们通常会二次封装一些高频业务组件,比如弹框,抽屉,表单等这些业务组件,为什么要二次封装?我们所有人心里的答案肯定是,同样类似的代码太多了,我想复用组件,或者原有组件可能达不到我想要的效果,我想基于原有组件自定义一些自己的接口,那么此时就需要二次封装了。二次封装虽好,但同时也会带来一定的心智负担,因为二次封装的组件可能会变得不那么纯粹。

本文是一篇笔者关于二次封装组件的思考,希望看完在项目中有所思考和帮助。

正文开始…

在内容开始之前,本文主要从以下几个方向去思考:

1、二次组件必须继承原有组件的所有特性

2、二次组件名必须见名知意

3、自定义暴露出来的接口越简单越好

4、留有自定义插槽,让用户可以自己选择

5、封装二次的组件,能根据schame数据配置,让组件更通用

继承原有组件接口

在之前的项目例子中,我们以一个弹框组件为例

我们看下在业务中一般是怎么写的

<template><div class="list-app"><div><a href="javascript:void(0)" @click="handleToHello">to hello</a></div>...<list-modaltitle="编辑"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?上面一段话的意思是,父组件classstyle会排除

我们从页面上可以看出titlewidth都是父组件传过来的,但是我们发现,实际上这两个外部看似自己传入的props也是el-dialogprops,所以说我们必须要保持自己二次封装的组件也有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> 

以上代码就自定义了modelevent,prop就是formParams,同时props上必须有引入formParams

不知道你有没有好奇,为啥我data中定义了一个currentVisible,而且watchvisiblecurrentVisible,使用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-optionv-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-modaltitle="编辑"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-modaltitle="编辑"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的各个知识点进行了总结,整理成了《Vue 开发必须知道的36个技巧》。内容比较详实,对各个知识点的讲解也十分到位。



有需要的小伙伴,可以点击下方卡片领取,无偿分享

  JavaScript知识库 最新文章
ES6的相关知识点
react 函数式组件 & react其他一些总结
Vue基础超详细
前端JS也可以连点成线(Vue中运用 AntVG6)
Vue事件处理的基本使用
Vue后台项目的记录 (一)
前后端分离vue跨域,devServer配置proxy代理
TypeScript
初识vuex
vue项目安装包指令收集
上一篇文章      下一篇文章      查看所有文章
加:2022-10-31 11:46:33  更:2022-10-31 11:51:21 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/17 16:44:55-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码