写公共函数、钩子、组件的能力是前端核心的能力。但在日常中,又看到一堆让人摇头的代码。且写这种代码的程序员,也并非新手,但依旧写出让人很难受的代码。
这次索性来个批斗贴, 吐槽一下常见的代码的毛病,不吐不快!
毛病1:大量使用钩子(复杂函数)
先看问题代码(片段):
需求是:生成带水印的图片
//使用示例
import useWaterMark from "@/customModule/hooks/useWaterMark"; // 印制水印
const {
waterMarkedUrl,
showWaterMark,
getDefaultWaterMarkList,
initWaterMarkedUrl,
drawFakePreviewImg,
} = useWaterMark();
//使用的片段
function use(){
showWaterMark.value = true;
waterMarkedUrl.value = await drawFakePreviewImg({
cardImageUrl: productEditStore.originalImage,
cardMode: cardStyleByProductSel.value,
waterMarkList: getDefaultWaterMarkList(
!!productEditStore.productSelectedExtra?.useAigc,
cardStyleByProductSel.value
),
isMetal: cardType.value == "metal",
materialId,
halfId,
});
}
看到这个公共钩子的使用示例,脑子都是嗡嗡的,一个钩子抛出了5个参数,且里面还有函数,函数还要配置参数,用你一个公共钩子我得了解清楚10个参数才能用,妈呀,不看源码不让用是吧?我的乖乖,这需求也不复杂啊。
改造后的代码
//使用示例
import { drawFakePreviewImg } from "@/customModule/utils/waterMark";
cardImg.value = await drawFakePreviewImg({
cardStyle: cardMode.value,
isAigc: !!aigcStore.currentAigcTypeId,
cardImageUrl: productEditStore.originalImage,
});
改造完后的代码,可阅读性直接升了一大档。
看到同事的代码时,我第一个问题就是:“为什么要用钩子,不用函数?” 同事楞着回答不上,仿佛作为vue3的新方式《钩子》理所当然就是更好的方式。这简直错得离谱。在vue3中,函数就是要优于钩子。函数一般只返回一个参数,阅读性要强于钩子,因此能用函数就不要用钩子。
这时候有人肯定会说,react大量使用钩子,你怎么能说钩子不好。我只能说,在vue中写出这种钩子的人,在react中,写出的公共钩子也是稀烂。原因很简单,无论是公共函数还是组件,阅读性是最重要的。没有阅读性,这公共代码就是失败的。在vue3中滥用钩子的人,在react也必定写一堆复杂阅读性糟糕的钩子,让使用者极其恶心。
写代码,特别是公用代码,一定要有阅读性!一定要有阅读性!一定要有阅读性!
题外话:如果有小伙伴不服,觉得有些公共内容,就是很复杂,就是阅读性低。受限于篇幅,我就不展开说了,反正不服就来评论区辩。
毛病2:业务功能和基础功能不分家
还是上面那个钩子函数
先看问题代码(片段):
const {
waterMarkedUrl, //水印图片链接
showWaterMark, //是否显示水印
getDefaultWaterMarkList, //获取水印打印默认参数
initWaterMarkedUrl,//重置水印图片
drawFakePreviewImg,//绘画预览图函数
} = useWaterMark();
从参数的备注,眼尖的小伙伴应该看出来,这个钩子把业务和基础功能完全混在了一起。waterMarkedUrl,showWaterMark,initWaterMarkedUrl这三个是业务逻辑,而getDefaultWaterMarkList,drawFakePreviewImg是基层功能原生方法。业务功能和基础功能逻辑就不该放在一起。
这直接把整个钩子的复杂程度直接拉升了一个量级。这种钩子不用多想,除了作者本人,别人要改,基本不可能。
我也不想多解释为什么不好改,只记得第一次看这钩子函数ts文件时,没半分钟,脑壳就发胀疼痛了(含没睡好原因)。面对这种函数,还是重构更合理,一是时间花得更少,二是能活久点。
业务就是业务,基础功能就是基础功能,一定要分开,这两者分开了,函数复杂度起码下降一半,阅读性能上升5层楼这么高,也更灵活。能大大提升使用者和修改者的寿命,望大家广而告之。
之前怎么分,这个不展开了,这篇帖子主要是吐槽代码毛病和梳理代码思路为主,并非详细的技术贴。
毛病3:该抽象不抽象
还是上面那个钩子,钩子里面的一个函数。下面是函数的入参ts数据类型
问题代码(简化片段):
type PreviewParams = {
/** 卡片图片URL */
cardImageUrl?: string;
/** 是否为AI生成 */
isAigc?: boolean;
/** 是否为金属材质 */
isMetal?: boolean;
/** 材质ID */
materialId?: string;
/** 半成品ID */
halfId?: string;
}
改造后的代码
type typeObjType =
| { type: "Pvc" }
| { type: "Metal"; materialId: string; halfId: "0" };
type PreviewParams = {
/** 制卡图片URL */
cardImageUrl?: string;
/** 是否为AI生成 */
isAigc?: boolean;
typeObj?: typeObjType;
};
这两个代码的区别是:把下面三个参数抽象成了一个对象。
/** 是否为金属材质 */
isMetal?: boolean;
/** 材质ID */
materialId?: string;
/** 半成品ID */
halfId?: string;
这样做有什么好处呢?提高阅读性!
将有关联的参数抽象在了一起,更容易理解参数的意义。比如halfId这个参数,只有在金属材质这种情况,才需要用到,那么halfId就该和它关联性的参数抽象成一个对象。该搞对象的时候,就该搞对象,真正的程序员,就该勇于搞对象。
如果不搞对象,将全部的参数都放在一层,使用者或修改者每次用你的函数,还得花时间去理解几个参数的关联性,得烦死人。
麻烦各位好汉,把关联性的参数放在一起!谢谢!后端接口返回的参数也是,在复杂逻辑的情况,不要把一堆平铺参数直接丢给前端整理。(灬ꈍ ꈍ灬)
搞搞对象吧,各位好汉们。
毛病4:不该抽象的乱抽象
问题代码:
<compA
:data1="data1"
:data2="data2"
:data3="data3"
:data4="data4"
:data5="data5"
:data6="data6"
@change1="change1"
@change2="change2"
@change3="change3"
></compA>
改造后的代码
<compA
v-if="imageUrl"
:data1="data1"
:data2="data2"
:data3="data3"
:data4="data4"
@change1="change1"
></compA>
<compB
v-else
:data5="data5"
:data6="data6"
@change2="change2"
@change3="change3"
></compB>
两段代码的区别在我把一个组件拆分了两个。
而我要拆的原因也很简单,就是太难受啦!我接到的需求是要重构旧代码,逻辑变,但样式不变。个人秉承多一事不如少一事原则,能复用绝不修改,但发现要直接复用,实在太难了。
明明是两个不同的功能,两者逻辑也完全不一样,但仅仅是因为位置一样,就把两个毫不相干的功能写在了一个组件中,且这组件还跟父组件有大量的数据交互。
我的乖乖啊,我想修改其中一个组件的逻辑,我就必须考虑另外一个组件的逻辑,时不时还要给另外一个功能加些判断。 我实在想不明白,上一任作者为啥要这样对我?是嫌我活太少吗?明明改一个组件的活,非要搞成改两个组件。明明只是改这个组件,为啥非要我去阅读另外一个组件的逻辑?
我就想问,图啥???牛马何必为难牛马呢?
抽象不等于把位置相当的模块合成一个组件,前端的模块化思维,不是这样玩的,模块化优先考虑的是逻辑!!!把逻辑搞成一块块的,不是让你把页面位置搞成一块块的。
有些时候,哪怕两个功能样式是一样的,我都分成两个组件,因为两个功能往往就是两个逻辑,如果想要代码阅读性高,就不要把不相干的逻辑放在一起!勉强没有幸福。
如果想复用样式,又不嫌麻烦,那可以封装个样式组件。若嫌麻烦,那直接copy一份样式过去进行修改,都比两个功能写在一个组件上强!
毛病5:过度封装,过度深入
问题代码:
//选择的sukId,['logoId',蒙层id,签名颜色id]
const chooseSkuId = ref(['1','3','04'])
//使用范例
<template>
<el-select v-model='chooseSkuId[2]'></el-select>
<el-radio-group v-model='chooseSkuId[0]'></el-radio-group>
</template>
改造后的代码
const logoId = ref('1')
const maskId = ref('3)
const colorId = ref('04')
//使用范例
<template>
<el-select v-model='colorId'></el-select>
<el-radio-group v-model='logoId'></el-radio-group>
</template>
别问我为什么他们要这样做,我只知道我修改的时候很痛苦。我也不想知道他为什么这么做,这种封装,就是反人性,强行增加使用者的阅读成本。 类似的情况还有很多,比如将数据逻辑分散到爷父孙上写,一个逻辑分成两三个ts文件之类的。不过一下子没找到示例,后面找到示例再补上相关例子。暂时就不虚空索敌了。
毛病6:黑箱子函数
问题代码:
//使用范例
<template>
<el-select v-model='colorId'></el-select>
<el-radio-group v-model='logoId'></el-radio-group>
</template>