写在前面
马上到了金三银四的时间,很多公司开启了今年第一轮招聘的热潮,虽说今年是互联网的寒冬,但是只要对技术始终抱有热情以及有过硬的实力,即使是寒冬也不会阻挠你前进的步伐。在面试的时候,往往在二面,三面的时面试官会结合你的简历问一些关于你简历上项目的问题,而以下这个问题在很多时候都会被问到
在这个项目中你有遇到什么技术难点,你是怎么解决的?
其实这个问题旨在了解你在遇到问题的时候的解决方法,毕竟现在前端技术领域广,各种框架和组件库层出不穷,而业务需求上有时纷繁复杂,观察一个程序员在面对未知问题时是如何处理的,这个过程相对于只出一些面试题来考面试者更能了解面试者实际解决问题的能力
而很多人会说我的项目不大,并没有什么难点,或者说并不算难点,只能说是一些坑,只要google一下就能解决,实在不行请教我同事,这些问题并没有困扰我很久。其实我也遇到过相同的情况,和面试官说如何通过搜索引擎解决这些坑的吧不太好,让面试官认为你只是一个API Caller,但是又没有什么值得一谈的项目难点
我的建议是,如果没有什么可以深聊的技术难点,不妨在日常开发过程中,试着封装几个常用的组件,同时尝试分析项目的性能瓶颈,寻找一些优化的方案,同样也能让面试官对你有一个整体的了解
上篇分享了我在项目中是如何根据功能划分模块以及性能优化的技巧,这章我会记录设计和封装组件的过程
技术栈是Vue + Element的单页面应用
封装组件
有人会说,github上那么多好的开源组件,好的开源库放着不用,为啥自己还要折腾呢?
其实我认为自己动手封装一个组件还是很有意义的,因为如果是从零开始编写的组件,你能够更好的掌握自己组件的所有功能,并且还能根据公司的业务需求定制一些特殊的功能,除此之外,理解一个组件内部的实现机制也有助于提升个人的编码能力,而不是别人问起来你只知道我用过某个组件,很好用,但是不知道是怎么做到的。所以我还是比较推荐去尝试编写几个常用的组件
因为是后台管理系统,核心的组件肯定是表单组件和表格组件,公共组件是基于element组件的二次封装,组件的设计遵循以下的思路
- 高内聚低耦合,尽可能少的暴露组件的api,将功能尽量封装在组件内部
- 组件内部根据业务需求设置了一些组件默认的配置项,另外再通过不同页面传入不同配置项提高组件的通用性
设计组件的目的就是让组件进一步解耦,将配置项和模板标签分离,一方面是减少在业务逻辑组件中的代码量,另一方面就是单独抽离的配置项使得能够通过后台动态传递给前端,或者自己建一个配置项的js/ts文件(如果有规范的开发者文档还可以使用nodejs编写一个读取开发者文档一键写入配置项的脚本,进一步提升开发效率)
表格组件
表格组件设计大致分为以下几个部分
- 尽量贴近element组件库的api
- 传入一个表头的配置项数组,通过这个配置项动态生成el-table-columns标签
- 交互复杂的表头列的解决方式
尽量贴近element组件库的api
组件中使用了$attrs,$listeners实现属性和监听事件的跨级传递,使得在页面中给自定义组件中的传入的属性能够通过自定义组件内部的转发直接成为el-table标签的属性,达到跨级的属性传递,而$listeners和$attrs类似,能够监听el-table组件中触发的事件,将事件转发 到页面中的自定义组件上
自定义组件:
这样做的目的就是能让你的自定义组件和el-table组件有相同的用法,传入的属性,监听的事件也是相同的
在页面中使用自定义组件,可以看到z-table和el-table的用法几乎是相同的,只需要额外传入一个columns的属性:
表头的配置项设计
继续给这个表格组件添加表头标签,这里我把一些不必要的判断都去除了,只留下了核心的逻辑,实际在组件内部只需要循环这个配置项动态生成el-table-column标签就可以了
自定义组件:
配置项文件:
这里的核心是在于这个v-bind
,当v-bind不携带参数,且绑定的值是一个对象时,它会遍历这个对象的所有属性,将属性和值一一做绑定
什么意思呢?这里结合配置项文件来说明,如果传入上述的配置项,组件的内部实际是这样子的
抛开key不谈,在配置项的每个元素中暴露一个attrs属性,里面保存了所有el-table-column标签可以接受的属性。例子中label,prop,width这3个属性是在配置项每个元素的attrs属性中的,通过v-bind="column.attrs"让这3个属性和它们的值分别在el-table-column标签中做了绑定,从而达到了模板和配置项解耦的目的
交互复杂的表头列的解决方式
对于一些需要特别处理的表头列的数据,我在组件内部利用插槽和作用域插槽,通过插槽定义表头列的插入位置,再通过作用域插槽将信息返回给父组件,在父组件中定义如何显示,使得表格组件非常的灵活能够应对大部分业务需求
可以看到具名插槽的名字也是通过配置项传入的,并且作用域插槽将整个表单内部的数据通过scope传给父组件,在复杂的业务场景,无法通过配置项解决问题的时候,通过插槽和作用域插槽让父组件去决定如何去处理数据
配置项中添加插槽属性:
页面组件:
在页面组件中,可以和element提供的作用域插槽的使用方式相似,通过scope可以访问到组件内部的所有数据并且交给页面组件去做复杂的逻辑处理
其他功能
针对公司的需求,我对组件做了进一步的改造
- 使用render函数使得表头显示能够更加灵活
- 配置项暴露一个函数能够让当前列的数据执行这个函数达到预处理的效果
- 配置项中设置一个二维数组,能够让数据字段组合,达到数据显示在不同的行数的效果
- 添加了操作图标
- 添加了数据(code码)转对应中文语义的功能
源代码
表单组件
表单组件相对于表格组件在实现方面要困难一点,因为表单的控件非常多,每个配置项又需要非常灵活,这里我借鉴了之前在知乎看到的一篇博客,文章中虽然没有把代码列出来,但是罗列了整体的实现方案,随后我根据文章中的思路设计了这个表单组件
设计大致分为以下几个部分
- 表单配置项设计
- 表单验证
- 表单请求
- 表单控件之间的联动
- 调用后端接口生成表单控件的选项
表单配置项设计
根据上面的表格组件的封装思路,还是利用$attrs做根元素属性的传递,用v-bind在配置项中设置组件内部的属性
表单组件:
配置项文件:
和表格组件不同的是,因为表单组件分为el-form-item标签和表单控件2部分,这2个部分都需要在配置项中对应配置属性,在配置项中使用itemAttrs控制el-form-item标签的属性,使用attrs控制表单控件的属性
这里还用到了component标签,通过配置项的tag标签动态生成el-input的表单控件,但是可以看到这里我并没有直接将tag的值设为el-input,那input是如何变成el-input的呢?
这里我又定义了每个组件通用的配置项,使得不需要每次都在组件的attrs中声明一些重复的属性,比如placeholder,clearable等
通用配置项文件:
最重要的是我建立了组件配置项和通用配置项之间的关联,通过组件配置项中的tag属性找到通用配置项对应的对象,结合上面的例子如果tag的值是input,那就会从通用配置项中找到input属性对应的对象,并且将真实的tag值指向通用配置项的component,这里就是el-input
而这种关联又是怎么建立起来的呢,其实还是用了Object.assgin做了对象之间的合并
核心逻辑如下(参数formItem指的是组件配置项formItems中的每个元素):
这里定义了一个computeFormItem的函数,通过传入配置项数组的每个元素,根据元素的tag值找到通用配置项(basic对象)中相应的值,随后用了Object.assgin做了合并,关于这个computeFormItem函数我稍后在后面的表单控件之间的联动中会详细去讲
通用和组件配置项都有了,接下来要实现的是表单组件需要上传给后端的数据对象
这里我的思路是通过配置项中声明的字段名(key)动态生成数据对象,这样可以减少传入的配置项的数量,在组件内部声明Model变量保存数据对象
但是这里有2点需要注意
- 因为组件内部声明的Model是一个空对象,Vue的响应式系统是监听不到对象创建了新的属性,需要使用$set来设置,使得能够强制更新视图
- 这里需要通过formItems对Model数据对象赋值,如果放在mounted钩子中执行的话拿到的是一个空数组,所以我这里使用watch来监听formItems,并且使用immediate立即执行(用computed声明一个新数组理论上也可以)
表单验证
表单验证方面尽量贴合element组件的传入方式,保持所有在el-form-item标签中写的属性都写在itemAttrs中,所有在表单控件中写的属性都写在attrs中,所以可以在itemAttrs中编写表单验证方面的逻辑
表单请求
表单请求方面,因为在重构时新建了api文件夹,存放的是一个个后端接口的api函数,做到一个页面对应一个api文件夹中的一个接口文件
每个接口文件中可以导出多个接口的函数
在表单组件中只需要声明一个api的props让页面组件传入就可以了
随后给提交按钮绑定click事件,进行表单验证最后执行接口函数,传入Model这个数据对象即可
在接口函数调用成功返回响应数据后,我这里通过触发after-submit的事件让页面组件监听这个事件,并且把响应数据传给页面组件,这样页面组件就能拿到响应的数据并且做一些处理了
页面组件监听after-submit事件:
表单控件之间的联动
这一部分我认为也是最难实现的,在日常的业务需求中可能需要某个控件控制另外一个控件显示与否
核心的思路就是在配置项中定义一个getAttrs的函数,这个函数根据当前Model,也就是数据对象中的某个值动态的生成一个attrs对象,最后将这个attrs对象通过合并到当前配置项的attrs中,另外还定义了一个ifRender函数,可以控制表单控件是否被渲染,最后我们的配置项可能长这样
接下来表单组件内部要实现如何执行这2个函数,依旧是之前的computeFormItem这个函数,它用来计算出当前表单组件的配置项
和上面的computeFormItem函数不同的是,我这里传入了第二个参数Model,使得当前的表单组件配置项能够根据Model动态的变化,在内部执行getAttrs函数传入这个Model,返回的对象通过Object,assgin合并到当前的配置项中,而对于另一个ifRender函数,也传入Model,返回一个Boolean值,最后用这个Boolean值在模版中通过v-if控制是否渲染表单控件
这里要分析一下整个表单最核心的部分:computeFormItem函数,它的作用是根据当前Model中的数据变化,动态的生成一个新的配置项,因为我们的表单控件是根据配置项映射而成的,需要改变表单控件只能去修改配置项
根据上面那个图可以发现,我们并没有直接使用页面组件传来的formItems配置项,而是根据_formItems循环渲染的标签,而_formItems是基于formItems并且经过computeFormItem生成的配置项,只要Model中的数据改变,这个配置项就需要重新计算生成新的值,所以我选择把_formItems放在计算属性中
这样,只要依赖项(这里是Model和formItems)变了,就会触发函数重新计算出新的_formItems
下拉框/单选框/复选框
在表单组件中,我使用component标签动态生成表单控件,但是对于一些有子节点的表单控件通过component实现就有些困难,这里我将含有子节点的组件(下拉框/单选框/复选框)又进行了一层封装,消除了子节点,让所有属性都在component这一层配置
自定义select组件
这样以来只要在配置项中声明一个options属性,通过component标签将组件转为自定义的select组件,让options属性就会变为select组件的属性,这样在自定义的select组件内部可以通过$attrs.options获取到它(这里注意value,label必须都要显式的声明否则会报错,因为element组件内部会对传入的属性验证)
组件配置项文件:
这里再次利用通用配置项文件,将组件配置项中声明的select组件的配置项映射到自定义的select组件中
调用后端接口生成表单控件的选项
在真实的业务需求中,部分下拉框,单选框的选项是通过拉取后端的接口生成的。放在表单组件中的话还是需要修改配置项,在页面组件中修改formItem。找到下拉框/单选框的key,将接口的数据赋值给options属性
总结
可以看到表单组件还是比较复杂的,其实这个表单组件相对于表格组件来说还是有一定的局限性,后续可能会给它设计插槽的功能。另外真实的业务需求肯定是更加复杂多变的,不管怎么说,一些交互逻辑不是特别复杂的表单这个组件还是能hold住的,本人能力有限,这里也只是给一个思路,希望后续能够愈发完善
源代码
一键生成配置项
介绍一款我自己写的工具库,可以和表格组件完美配合,读取开发者文档,一键生成组件的配置项,免除多字段输入的错误和重复劳动,有帮助的话希望各位赏个 star ~