vue如何二次封装一个高频可复用的组件

news2024/12/29 13:49:43

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

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

正文开始…

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

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个技巧》。内容比较详实,对各个知识点的讲解也十分到位。



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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2095.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

2004-2020中小企业板上市公司财务报表股票交易董事高管等面板数据

1200变量&#xff01;中小企业板上市公司面板数据大全 2004-2020年 1、时间&#xff1a;2004-2020年 2、数据范围&#xff1a;共计973家上市公司 3、数据指标&#xff1a;包括财务报表、股票交易、董事高管等1200变量 4、用途&#xff1a;进行上市公司高管股权激励与公司绩…

C语言刷题系列——1.将三个整数按从大到小输出

将三个整数按从大到小输出1.输入三个整数2.最大的值放在a中&#xff0c;最小值放在c中&#xff0c;剩余的一个放在bstep1&#xff1a;a和b比较step2&#xff1a;a和c比较step3&#xff1a;b和c比较3.最终的代码1.输入三个整数 先写好main函数、头文件 #include <stdio.h&g…

用高并发技巧解决redis热key问题

​ 这篇文章我将介绍工作中处理热key问题的常用手段&#xff0c;可能介绍的不是很全&#xff0c;毕竟不同的业务场景可能有不同的解决方案&#xff0c;但是相信通过这部分的介绍能提供一个热key问题的思路。 热key问题&#xff0c;简单来说就是对某一资源的访问量过高问题&…

Unity学习shader笔记[一百零八]简单萤火效果

之前用粒子系统基于原有萤火虫的粒子改了一波慢萤火效果就被惊艳到了&#xff0c;开始大家讨论&#xff0c;就都觉得这样大数量的粒子消耗挺大的&#xff0c;后面测试过才发现单纯的粒子系统在总粒子数量3000&#xff0c;每秒300的生成数量&#xff0c;屏幕呈现有1000多个粒子的…

【黄啊码】MySQL入门—17、在没有备份的情况下,如何恢复数据库数据?

大家好&#xff01;我是黄啊码&#xff0c;MySQL的入门篇已经讲到第16个课程了&#xff0c;今天我们继续讲讲大白篇系列——科技与狠活之恢复数据库 在没做数据库备份&#xff0c;没有开启使用 Binlog 的情况下&#xff0c;尽可能地找回数据。 今天的内容主要包括以下几个部分…

2022NISCTF--web

easyssrf 打开题目&#xff0c;显示的是 尝试输入&#xff0c; 发现输入flag有东西 读取文件 访问下一个网站 读取文件 不能以file开头 直接伪协议&#xff0c;base64解码 checkIn 奇怪的unicode编码 当选中左边的时候右边也会被选中 我们在vscode看看 这样的额 展示的是UTF-1…

Linux系统中利用open函数多次打开同一个文件操作方法

大家好。 今天的话主要和大家聊一聊&#xff0c;在Linux系统中如果一个文件被打开多次会出现什么情况。 目录 第一&#xff1a;多次打开同一个文件 ​第二&#xff1a;一个文件被打开多次&#xff0c;在内存中不会存在多份动态文件 ​第三&#xff1a;多次open打开同一…

第一章 - Windows安装VMware Workstation Pro

文章目录前言一、VMware Workstation Pro安装的前提条件二、VMware Workstation Pro下载三、VMware Workstation Pro安装前言 Linux是一个开源、免费的操作系统&#xff0c;其稳定性、安全性、处理多并发已经得到业界认可&#xff0c;目前很多企业级的项目都会部署到Linux系统…

结构体内存对齐

在知道了结构体类型的基本使用之后&#xff0c;我们需要深入探讨一个问题&#xff0c;即计算结构体的大小&#xff0c;这也是一个热门的考点&#xff1a;结构体内存对齐。 目录 一、结构体的对齐规则 二、例题 2.1 例题一 2.2 例题二 2.3 例题三 ​编辑 三、为什么存在内存…

【C++】vector,list迭代器失效

1.vector迭代器失效 vector容器的物理基础是线性表&#xff0c;底层是指针变量实现的。 在这里导致vector迭代器失效的原因会有两种-----插入失效&#xff0c;删除失效。 1.2插入数值导致迭代器失效 1.21扩容导致迭代器失效 我们在一块vector空间插入pos&#xff08;20&…

第三章 单向链表的讲解与实现

初阶数据结构 第一章 时间复杂度和空间复杂度 第二章 动态顺序表的实现 第三章 单向链表的讲解与实现 文章目录初阶数据结构前言一、什么是链表&#xff1f;二、节点的定义&#xff1a;三、单向链表接口函数1、打印&#xff1a;2、尾插&#xff1a;3、头插&#xff1a;4、尾删…

改进YOLOv7系列: 最新结合用于小目标的新CNN卷积构建块

&#x1f4a1;统一使用 YOLOv7 代码框架&#xff0c;结合不同模块来构建不同的YOLO目标检测模型。&#x1f31f;本项目包含大量的改进方式,降低改进难度,改进点包含【Backbone特征主干】、【Neck特征融合】、【Head检测头】、【注意力机制】、【IoU损失函数】、【NMS】、【Loss…

Linux-进程控制

进程控制进程创建fork函数写时拷贝fork常规用法fork调用失败的原因进程终止进程等待进程程序替换程序替换的原理如何程序替换进程创建 fork函数 fork之前父进程独立运行&#xff0c;fork之后&#xff0c;父子两个执行流分别执行。 进程具有独立性&#xff0c;代码和数据必须独立…

机器学习HMM模型

目录1 马尔科夫链1.1 简介1.2 经典举例1.3 小结2 HMM简介2.1 简单案例2.2 案例进阶2.2.1 问题阐述2.2.2 问题解决3 HMM模型基础3.1 什么样的问题需要HMM模型3.2 HMM模型的定义3.3 一个HMM模型实例3.4 HMM观测序列的生成3.5 HMM模型的三个基本问题4 前向后向算法评估观察序列概率…

计算机毕业设计-SSM高校社团招新系统-JavaWeb大学生社团管理系统-源码+文档+讲解

注意&#xff1a;该项目只展示部分功能&#xff0c;如需了解&#xff0c;评论区咨询即可。 本文目录1.开发环境2.系统的设计背景3 前后台功能设计3.1 前台功能3.2 后台功能4 系统页面展示4.1 学生功能模块展示4.2 干部功能模块展示4.3 管理员功能模块展示5 更多推荐6 部分功能代…

如何用IDEA提高你的开发效率

前言 ​ 作为一名java开发工程师&#xff0c;IDEA无疑是我日常接触最多的工具。因此&#xff0c;能否高效使用IDEA软件&#xff0c;一定程度上决定了我们的开发效率。本文将主要介绍IDEA中的四个便于提高开发效率的功能&#xff0c;常用快捷键、实时模版、后缀补全、文件和代码…

《本地计算机DNS缓存文件》

C:\Windows\System32\drivers\etc 36.152.44.95 www.baidu.com 正常访问www.baidu.com可以DNS抓包&#xff0c;将百度的IP及域名加入文件位置的hosts文件中即该IP和域名将不再请求网络上的DNS服务器&#xff0c;即加快域名解析&#xff1b; 具体作用&#xff1a; 1.加快域名解…

什么是RPC框架?

什么是RPC&#xff1f; In distributed computing, a remote procedure call (RPC) is when a computer program causes a procedure (subroutine) to execute in a different address space (commonly on another computer on a shared network), which is coded as if it wer…

创新能力 | 产品经理实践中常犯的七大错误

做产品是一个既感性又理性的过程&#xff0c;纵然有很多前辈同行的经验传承和技巧指导&#xff0c;但在落到实处是时&#xff0c;总难免犯一些错误。有些是经验不足导致&#xff0c;有些则是产品经理对于人性的浅见寡闻。本文作为产品经理实践指南专题的中级篇&#xff0c;阐述…

用 AWTK 和 AWPLC 快速开发嵌入式应用程序 (2)-走马灯

AWPLC 目前还处于开发阶段的早期&#xff0c;写这个系列文章的目的&#xff0c;除了用来验证目前所做的工作外&#xff0c;还希望得到大家的指点和反馈。如果您有任何疑问和建议&#xff0c;请在评论区留言。 1. 背景 AWTK 全称 Toolkit AnyWhere&#xff0c;是 ZLG 开发的开源…