阅读 324

如何重构自己一年前写的 Vue 组件

「本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!

前言

一年多以前写了一个业务项目,当时那个兴奋啊直接就上 Vue + TypeScript 组合拳(之前没写过 TypeScript)。如今暮然回首,垃圾代码藏在灯火阑珊处。

v2-46e6c496c26f97bf415c2313c8143c42_1440w.jpeg

借着这次新需求的机会,我主动在做完需求的基础上,对代码进行回检优化,曾有几次让人不堪回首,但是我还是一鼓作气把一年以来积累的经验用在此次重构上,感觉还是蛮有意思的。

358053aa5e375f07b001c37b78541645.jpeg

组件

本文出现代码都不是完整版本代码

重构的对象是某前端桌面端系统的一个可复用组件,先来看看优化前和优化后的组件在使用上的不同:

<!-- 优化前 -->
<template>
  <LetterDetail
    :data="letterDetailData"
    :info="cirInfo"
    :modal="isShow"
    @update-info="handleUpdateInfo"
    @close-modal="handleCloseLetterDetail">
  </LetterDetail>
</template>

<script lang="ts">
export default class LetterDetail extends Vue {
  private isShow: boolean = false
  private cirInfo: Array<object> = false
  private letterDetailData: object = false

  private handleUpdateInfo(id: string): void {
    this._getLetterDetail(id)
  }

  private handleCloseLetterDetail(letterId: string): void {
    api.getLetterDetail(letterId).then((res) =>{
      ...
    })
  }
}
</script>
复制代码

由上面代码分析:根据组件名 LetterDetail 顾名思义是信件详情可复用组件,并且它会在不同的业务组件内部被多次复用。

  • data 是传入的信件详情属性
  • info 是传入的物流信息属性
  • modal 是传入的是否展示该信件详情组件的开关属性
  • update-info 是有信件 ID 入参的 Emit 事件
  • close-modal 是关闭该信件详情组件时 Emit 事件

看起来是不是很复杂,其实根本 NO!!!是一年半前的小可爱把这段代码写复杂了,不要问我为什么,你要搭乘时光机回去问问她。

根据优化前的 LetterDetail 组件,我们大致也知道这个组件是干嘛的了对吧?不就是用来展示信件详情展示物流详情的嘛,而且还可以打开关闭的类似模态框一样的玩意,而且即使你在组件内部操作更新信件的状态也需要随时同步更新展示的信息。那我们来看看优化后的调用代码:

<template>
  <LetterDetail
    :visible="isLetterDetailShow"
    :id="mailId"
    @closed="handleLetterDetailClosed">
  </LetterDetail>
</template>

<script lang="ts">
export default class LetterDetail extends Vue {
  private isLetterDetailShow: boolean = false // 是否展示信件详情组件
  private mailId: string =  '' // 信件 ID

  private handleLetterDetailClosed(): void {
    this.isLetterDetailShow = false
    this.mailId = ''
  }
}
</script>
复制代码

首先从代码行数上减少了不少行,其中包括减少了信件详情组件的属性 data 和 info 以及抛出的事件 update-info。

即使在组件调用上也能以小观大重构的几个要点:

  1. 注释:在合适的地方为代码添加注释,比如:HTML、数据、方法...
  2. 命名:参考现有组件库进行命名,比如
    • visible 属性名(原来为 modal)借鉴 Element 里的 Dialog;
    • 布尔类型开关类数据变量尽量命名成 isXXXShowisLetterDetailShow
    • closed 事件名(原来为 close-modal)借鉴的也是 Element 里的 Dialog;
    • 事件处理方法尽量命名成 handleXXX动词handleLetterDetailClosed
    *其实命名属于代码规范的一种,它没有对错只有相对好坏,这个本强迫症患者也积攒了一篇文章,下篇文章发。(Flag 插在这里)懂得都懂给我点赞
  3. 逻辑:在优化前中存在 data 和 info 两种数据作为属性传入,优化后代码更改为只传入信件 id。说明思路已经从在组件外获取数据更改为传入最少数据在组件内请求所需要的更多数据。由于数据上做了一些优化,很多逻辑上的问题也要相应地修改。比如之前组件需要 update-info 同步更新展示的数据,但优化之后的代码都已经把数据整合到组件内部获取和更新,因此相对应事件就不需要了。

正文开启

一、注释

优化前的代码一句注释都没有(微笑.jpg),所以就连本人在一年多后重新开发新功能,那些代码都要看半天,再配上那个扭曲的命名,就是两个字“绝了”。

v2-fc5f2fee2369d3c664d88c878ab59a38_1440w.jpeg

注释上的代码优化可以从三个地方下手:

1.1 HTML

<template>
  <div id="letter-detail">
    <el-dialog title="信件详情">
      <div>
        <!-- 基本信息 S -->
        ...
        <!-- 基本信息 E -->

        <!-- 补充备注 S -->
        ...
        <!-- 补充备注 E -->

        <!-- 物流信息 S -->
        ...
        <!-- 物流信息 E -->
      </div>
      <div slot="footer" class="dialog-footer">
        <!-- 
          rcv_snd: 0 既是发件人又是收件人
          rcv_snd: 1 只是收件人不是发件人 
          rcv_snd: 2 只是发件人不是收件人
         -->
        <el-button v-show="!isYQS" v-if="letterDetail.rcv_snd == '2' || letterDetail.rcv_snd == '0'">撤回信件</el-button>
        <el-button v-show="!isYQS" v-if="letterDetail.rcv_snd == '1' || letterDetail.rcv_snd == '0'">签收</el-button>
        <el-button v-show="!isYQS" v-if="letterDetail.rcv_snd == '1' || letterDetail.rcv_snd == '0'">设置代签人</el-button>
        <el-button v-show="!isYQS" v-if="letterDetail.rcv_snd == '0' || letterDetail.rcv_snd == '1' || letterDetail.rcv_snd == '2'">异常设置和处理</el-button>
        <el-button v-if="letterDetail.rcv_snd == '0' || letterDetail.rcv_snd == '1' || letterDetail.rcv_snd == '2'">添加补充备注</el-button>
      </div>
    </el-dialog>
    <!-- 设置代签人模态框 S -->
    <el-dialog...
    </el-dialog>
    <!-- 设置代签人模态框 E -->
    
    <!-- 异常设置和处理模态框 S -->
    <el-dialog...
    </el-dialog>
    <!-- 异常设置和处理模态框 E -->
    
    <!-- 异常处理模态框 S -->
    <el-dialog...
    </el-dialog>
    <!-- 异常处理模态框 E -->
    
    <!-- 添加补充备注模态框 S -->
    <el-dialog...
    </el-dialog>
    <!-- 添加补充备注模态框 E -->
  </div>
</template>
复制代码

从上面优化代码可以看出,我根据不同功能增加了划分 HTML 代码段的注释,并且注明了开始 S 和结尾 E。同时在某些直接拿后端返回的数据字段赋值的地方(rcv_snd),增加了一些逻辑上的注释,防止后来开发人员一看到就懵逼。

1.2 数据

还有一个地方的注释,麻烦全世界的开发者能加都加把。。。你好我好大家好啊。

<script lang="ts">
export default class LetterDetail extends Vue {
  private myThis: any = this
  private isLoading: boolean = false // 是否加载中
  private isYQS: boolean = false // 信件是否已签收
  private letterDetail: any = {} // 信件详情
  
  private isAddRemarkShow: boolean = false // 是否展示添加备注模态框
  private remarkForm: any = { // 备注信息
    itemId: '',
    type: 'mail',
    remark: ''
  }
  private remarkList: Array<any> = [] // 备注列表

  private logisticsConstant: Array<any> = [] // 物流常量
  private logisticsInfo: Array<any> = [] // 物流信息
  
  ...
}
复制代码

在每一个初始化变量后面增加业务相关的单行注释,并且根据关联性换行处理。

1.3 方法

<script lang="ts">
export default class LetterDetail extends Vue {
  ...
  /* 更新信件详情 */
  private updateLetterDetail(id: string): void {...}
  
  /* 撤回信件 */
  private handleRecallLetter(): void {...}
  
  /* 签收 */
  private handleSign(): void {...}
  
  /* 整合物流常量和物流信息并更新 */
  public _getLogistics(logisticsInfo: any): void {...}
  
  /* 获取补充备注 */
  private _getRemark(itemId: string, type: string): void {...}
  
  ...
}
复制代码

尽可能为每一个方法都增加多行注释,并诠释其中的功能。

二、命名

命名就不贴代码讲了,本人作为强迫症终极患者,整理过一份代码风格指南并且牢记于心。后面直接开一篇文章细讲,当然从以上部分已经贴出的代码也能看出,命名已自成一套规律和风格。

关于命名的建议,就是参考统一。参考目前优秀的组件库中的代码风格或者 Vue 官网中的风格指南,并且做到上下文统一一致的命名规律。

三、逻辑

重构可复用组件还可以从入参属性、抛出事件、公共数据三个方面入手。

3.1 入参属性

先来看看优化前关于 Prop 的组件内部代码:

<script lang="ts">
export default class LetterDetail extends Vue {
  @PropSync('modal', { type: Boolean })
  syncedModal: Boolean

  @Prop()
  data: any

  @PropSync('info', { type: Array })
  syncedInfo: Array<object>
}
复制代码

上面 Prop 声明是 Vue + TypeScript 的写法。data(信件详情) 和 info(物流详情)其实都是从一个接口获取来的数据,只不过接口获取回来的数据需要处理和拆分。一年多前我就在组件外获取数据然后拆分再传入到 LetterDetail 中来,别问为什么,问就是玄学。这段代码如今看已经深感迷惑了。

我们再看看优化后关于 Prop 的组件内部代码:

<script lang="ts">
export default class LetterDetail extends Vue {
  @PropSync('visible', { type: Boolean })
  syncedVisible: Boolean

  @PropSync('id', { type: String })
  syncedId: string

  @Watch('id',  { immediate: true, deep: true })
  onSyncedIdChanged(newVal: string) {
    if (newVal) {
      this.updateLetterDetail(newVal)
    }
  }
}
复制代码

在优化后的代码中,已经将 data(信件详情) 和 info(物流详情)去掉,换成了 id (信件 ID)类型是字符串,并且监听 id 入参属性,当 id 值存在且变化时,我们就去请求获取信件详情的接口并更新。

3.2 抛出事件

先来看看优化前关于 Emit 的组件内部代码:

<script lang="ts">
export default class LetterDetail extends Vue {
  @Emit()
  closeModal(): void {
    this.logisticsInfo = []
    this.cisConstant = []
    this.signerForm.signerId = ''
    this.signerForm.signerName = ''
    this.signerForm.signerPathName = ''
  }

  @Emit()
  updateInfo(): string {
    return this.data.mail_id
  }
}
复制代码

看了这段代码我没什么感言,不是很懂为什么要加 updateInfo 抛出,可能当时由于把获取信件详情的逻辑放在了组件外部,当操作组件内部更新了信件状态时,就必须要在组件内部重新抛出信件 id,才能在组件外部进行重新获取信件详情更新。这就是挖了一个坑然后再挖了另外一个坑填第一个坑。

那我们直接来看优化后关于 Emit 的组件内部代码:

<script lang="ts">
export default class LetterDetail extends Vue {
  /* 关闭模态框通知 */
  @Emit('closed')
  closedLetterDetail(): void {
    this.letterDetail = {}
    this.logisticsInfo = []
    this.logisticsConstant = []
    this.signerForm.signerId = ''
    this.signerForm.signerName = ''
    this.signerForm.signerPathName = ''
  }
}
复制代码

直接去掉了 updateInfo 事件,留下了 closed 事件,因为在关闭信件详情模态框的时候,需要通知父组件去更新父组件对应的信息,比如在父组件中:

private handleLetterDetailClosed(): void {
  this.isLetterDetailShow = false
  this.mailId = ''
}
复制代码

3.3 公共数据

这一段代码是最傻的:

<script lang="ts">
export default class LetterDetail extends Vue {
  /* 获取更新物流信息 */
  public _getLogisticsInfo(): void {
    api.getCis().then(res => {
      this.cisConstant = res.data.data
      this.logisticsInfo = util.deepCopy(this.syncedInfo)
      // ...
    })
  }
}
复制代码

在旧代码中,物流信息 info 是从 Prop 来的,而这里的 getCis() 其实是为了获取物流信息状态常量,然后将 info 里面的常量字段使用 getCis() 获取的数据替换成中文,才是最终的物流信息。

上面的代码却写成了,每一次更新都需要重新获取一次物流信息常量,我???我直接登陆进来获取大部分常量保存在 store 然后需要的时候再拿出来用不香嘛?

所以更新后的代码,还顺带优化了请求次数呢!我们来看看优化后的代码:

<script lang="ts">
export default class LetterDetail extends Vue {
  @State('common') stateCommon: any
  
/* 获取物流常量,更新物流信息 */
  public _getLogistics(logisticsInfo: any): void {
    this.logisticsConstant = this.stateCommon.cisConstant
    this.logisticsInfo = logisticsInfo
    // ...
  }
}
复制代码

四、最后

以上三个重构大法并不是一定按照顺序去做代码优化的。在实际重构场景中,注释优化和命名优化注重实用性,即在对代码进行逻辑处理优化过程中主要保证逻辑的正确性,注释优化和命名优化对其进行辅助作用。在不断增加注释和规范命名过程中,使得逻辑代码优化更加清晰。

今天周五,摸鱼快乐!

文章分类
前端
文章标签