本文记录了我工作中如何对后台管理项目代码进行优化的。项目使用了 Vue 和 Element UI 框架。
持续小重构可以在提测阶段或是在其他没有开发任务的时候进行,甚至当你在开发时发现需要修改的代码难以修改的时候。我觉得一个合格的开发人员,应该有持续优化的意识,切不可到代码烂到没法维护的时候才着手进行大重构,那将举步维艰。
一、减少魔法数的使用
看一下项目中的下面的代码:
|
|
通过代码,我们可以知道当订单状态为 1 和 9 时,可以设置定金。1 和 9 虽然不知道是什么,但它们确实让代码正确运行了,这毫无疑问是魔法,人类称之为 魔法数。可以看到,魔法数降低了代码的可读性。现代人类社会是不需要魔法这种东西。
那么如何如何解决问题呢?最简单但不是很好的办法是在代码后面加点注释: // 1: 待支付,9:定金待支付 ,这样我们就可以马上知道 1 和 9 代码什么。但不可能每个魔法数后面都加注释吧,尤其是文件内魔法数多次出现的情况下。
一般来说,我们是通过声明可以可读性良好的变量名解决的:
|
|
可读性好多了。不过这里的订单状态其实是一组枚举值。最好的做法是在全局维护唯一的枚举值对象,从里面取需要的状态。如果不知道怎么命名枚举值,可以找后端要枚举值,因为枚举值本来就是后端定义和约定的。
首先我们需要用一个存放全局枚举值的模块文件。
|
|
在需要用到的地方引入:
|
|
如果枚举值只是局部使用,可以不必放到公共目录下,直接在当前文件下定义即可。
二、删除无用代码
经常可以发现一些 定义了却未被使用的变量和方法,乃至文件,其实可以直接删了。
无用变量和方法出现的几个原因:
- 复制类似模块后在上面修改,一些多余代码遗留下来了。有时候会加一个 tab,和旁边的 tab 的功能类似,但去掉并添加了一些功能。一般的做法是,复制 tab 对应的代码,并在上面做修改,但改完能正常运行了可能就不管了,文件里可能遗留一些多余代码。
- 产品希望暂时去掉一些功能,可能以后要加回来。注释了 Vue 单组件文件中 template 里的 HTML,但相关的方法没有注释掉
- 纯粹改着改着忘记了自己定义了这么一个方法。
解决方法:
- 文件内搜索变量名和函数名,看看出现的次数和位置。如果只出现一次,那多数是多余的代码了。但也要注意方法是否可能被其他组件通过 ref 调用,以及通过 inject / provide 注册的方法。
- 对于要暂时隐藏的功能,除了注释掉 template 里的内容,建议也注释掉涉及到的变量和方法,并简单地加上哪个版本注释掉什么功能的注释。此外,如果产品如果没说以后可能要加回来,我还是建议直接删了,因为大概率不会加回来,而且我们还有版本控制工具。如果是某些文件或目录暂时不用了,后面可能会用回来,建议修改改名字,在文件名后面加上
([版本]废弃)标记,如index.vue可以改为index(6.5废弃).vue。如果多个版本过去后还处于废弃状态,就可以考虑将其删除了。 - 提交前务必检查一下代码,确保没有多余的代码才提交,方法可以用前面提到的第一个方法。
无用 CSS 样式同样处理。
三、组件拆分
解耦是提高代码可读性、可维护性的最有效方式。人读取信息的容量是有限的,如果一个类(或组件)有大量的变量的方法,我们称之为 上帝类。掌控一切,全知全能的类,人类想要从中获取自己想要的信息并尝试控制它,必然不会简单。我们尝试阅读将很多功能糅合在一起的代码时,就会接受到大量的信息,其中小部分是我们想要知道,绝大部分是不需要也不想知道的。我们解耦、模块化的根本原因,本质上其实是为了提高可读性,你看不懂甚至误解,你就必然会犯错误。
通过组件拆分,我们不仅可以更专注于特定的小模块,还可以降低误改其他模块的代码几率。
项目中,还是有不少组件比较庞大的,尤其是一些比较老的、改动较少的、复杂(一开始就没设计好)的组件。
弹窗组件封装
一般来说,弹窗是可以并建议拆出去的。尤其是一个页面有多个弹窗存在的情况。如果不封装,一个组件就会有一大堆弹窗的相关变量,而且拆分其实并不难。
项目中常见的弹窗分为 表单弹窗 和 列表弹窗 两大类。对此我们可以将 ElementUI 的 el-dialog 组件封装为一个 有具体业务的弹窗组件,模板如下:
|
|
弹窗 closed 时调用清空表单项数据(表单弹窗)或者清空列表筛选项(列表弹窗)。
实例:订单详情页的组件化思路
几个月前我对订单详细页做了组件上的拆分,这里简述一下我的组件化的思路。
- 为什么拆分?原来弹窗和列表很多,里面涉及的变量也很多,全都塞到一个组件里,信息量太大,有组件化的必要。
- 如何拆分?从视图上的分层来斟酌如何分组件,其实看一眼其实就大概知道怎么分了,当时第一级的组件拆分如下:
div.viewport
├─── order-info(订单信息)
├─── paid-info(支付信息)
├─── pay-action(支付操作栏,中间的按钮和倒计时)
├─── usage-info(使用信息)
├─── contract-info(使用信息)
└─── add-deposit-dialog(添加定金弹窗,应该放到订单信息里,因为懒,没做)
拆完这些第一级组件后,弹窗就根据分类放到对应的一级组件中就好了。另外,还要考虑数据的传递问题,一般是通过 props 传入数据来控制组件;当弹窗进行了操作后,通过自定义事件通知页面组件重新请求数据。
- 效果如何?挺好,后面加弹窗,就不用再加到原来唯一的组件文件上了,可以找到相关的组件下面加弹窗。比如后来有一版在使用信息列表的操作栏中加入 冻结订单 的弹窗操作,直接在 usage-info 里加就好了,很解耦。
四、命名
- 尽量和后端字段统一。可以看看后端的 api 接口中如何对一些事物命名的,比如刚加入外呼工作台时,术语 “外呼” 其实用的是 market, 但我一开始接入这个新功能时起的名是 dialog,以为就只是打电话的,在路由和方法中都用这个术语。后来发现有点不妥,还是 market(市场)比较贴切一些。如果对后台提供的术语无法理解或者觉得不妥,可以和后端交流一番(物理),提出你的建议,最后达成共识即可。
- 不要起类似 editLog2 的变量名。可读性不好,editLog 是后端返回的数据结构,editLog2 为做了映射的结构。这个 2 完全没有体现这个变量的意思,可以考虑命名为 mappedEditLog。
- 通过上下文简化命名。比如一个名为 allocated.vue 文件下,请求列表没有必要写 getAllocatedStudents 的方法名,使用 fetchData 即可。
五、一些技巧
推荐使用 optsMap 过滤器
写了个 optsMap 的过滤器,对于类似 [{id: num, name: ‘str’}, …] 的数组, 传入 id 返回对应 name。这个过滤器可以保证下拉筛选项(el-option)和列表返回值映射的 一致性,同时可以减少代码的书写。如:
|
|
consumeTypeOpts 是一个 options 数组,用在 “扣费原因” 下拉筛选器里。然后我们发现请求的列表的返回值也带了一个“扣费原因”的枚举值,我们要对其进行 code 指向文本内容的映射。我们可以使用 {{ row.consume_type | optsMap(consumeTypeOpts) }} 复用这个 consumeTypeOpts 里的数据,而不用专门写 { 1: '使用券', 2: '券过期' }[id] || '--' 这种多余且需要人工对照保证一致性的代码。
扩展公用方法
如果一些公用方法无法使用新需求,我们可以在上面追加一些参数,进行向下兼容的扩展。
比如 formatQuery,本来是将传入的对象进行深拷贝,并去掉其中为零值(如 undefined,空字符串、空数组)的键的方法,用于发起请求时处理筛选项对象。因为有一个版本出现了大量时间范围的筛选项,清空时间范围筛选项时,会导致数据变成 [null, null],且后端开始规范,不允许 get 请求中在请求字符串用空值(如 ?a=&b=3,这是 a = null 格式化导致的),所以我决定对其进行扩展。
formatQuery 不会去掉值为 null 和 [null, null] 或 [undefined, undefined] 的属性,于是我给这个过滤器加上了第二个配置项参数 { nullVal: Boolean, nullArray: Boolean } ,来处理这两种情况。很好地解决了问题。
现在也有一些过滤器,比如 validSubjectFormat 筛选器,是可以进行扩展的,这种过滤器会把数组的 id 映射到用逗号分隔的对应文本,这个逗号其实可以定为默认值为逗号的分隔符,而不是写死。我们只要多提供一个参数 sep 来指定分隔符即可。
需要注意的点:确保不会因为修改代码,导致原来的代码受到影响,即保证向后兼容。
使用一个状态变量取代多个布尔值变量
如果多个状态(用布尔值表示开启和关闭)中,一个状态为开启时,其他状态必须为关闭状态,应该用一个枚举来保存状态。
|
|
门面模式(外观模式)
如果有一些常用的调用多个方法的逻辑,我们可以将其封装成粗粒度的新方法,提高方法的易用性。这就是门面模式。
|
|
另外,如果某个地方要同时调用多个接口,可以考虑让后端合并为一个接口,尤其是移动端用户使用不稳定的移动网络的情况。使用更少的接口请求,响应的时间就更快。
请求用的 sending 变量的操作的正确位置
为了防止用户对一个按钮连续点击多次,导致一些不可预料的问题。我们会在必要的时候,在请求的时候使用一个 sending 布尔变量来解决这种问题。具体做法为:
|
|
sending 应该紧靠请求 api 的方法,
防止中间过程因为一些原因抛出错误,导致 sending 变为 true,但请求没发生,从而不可能完成请求将 sending 变为 false, 最终导致按钮一直处于 sending 为 true 状态,请求完后的 then、catch 和 finally 同样道理,要第一时间将 sending 设置为 false。
业务代码与非业务代码分离
如列表请求对象,应该把(某种意义上)与业务无关的 start、length、sort_name、sort_order 和业务代码分开来。
query: {
// 非业务代码
start: 0,
length: 20,
sort_name: '',
sort_order: '',
// 业务代码
name: '',
grade: [],
}
另外也建议属性顺序按照筛选项 UI 的顺序排布,方便对照查漏。
在 el-table 中使用 slot-scope="{ row }"
在使用 Element 的 el-table 时,在 el-table-column 组件中使用的往往是 slot-scope="scope",其实完全可以将 scope 改为使用 { row } ,这样可以写更短的 row.name,而不是 scope.row.name,减少单词拼写错误的风险,语义也没有损失。除非要用到 scope.$index 之类的变量。