这些代码毛病(非新手),你中了几个?

107 阅读7分钟

写公共函数、钩子、组件的能力是前端核心的能力。但在日常中,又看到一堆让人摇头的代码。且写这种代码的程序员,也并非新手,但依旧写出让人很难受的代码。

这次索性来个批斗贴, 吐槽一下常见的代码的毛病,不吐不快! 98f91097837ebf51b491a2aaded7fc78.png

毛病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>

后面继续跟新