如何优雅的编写难以维护的代码

3,353 阅读4分钟

前言

最近在做一个很多复合表格需要展示和编辑的项目,想分享一下如何build屎山😏

本项目用的是vue3+vite+ts

需求

demand.png

如图所示,有很多块区域,每块区域可以单独编辑保存,互不干扰。

点击编辑时,该区域内可以编辑的地方都变成输入框。

上图只是简单演示,实际上有非常多格子,会有合并单元格情况,甚至一块区域有多个表格。

设计

image.png

  1. Block:区域组件,封装数据的请求和保存,封装标题和编辑按钮。
  2. Table:表格组件,封装表格布局的组件。
  3. Row:行组件,表示表格每一行的组件。
  4. Col:列组件,表示表格每一列的组件,就是单元格。
  5. Edit:编辑组件,编辑时变成输入框,不编辑时显示文本。

可以通过不停嵌套搭配row和col形成合并单元格。

开淦

image.png

Block组件内部请求拿到的数据,通过作用域插槽暴露出来给Edit绑定使用。

现在还差了一点,由于编辑状态封装在Block里面,所以也要暴露isEdit来控制Edit是否展示输入框。

image.png

我们可以发现这样做看起来挺蠢的,上千个格子,每个Edit组件都要显式的绑定一下isEdit

当时我正在苦思,突然想到element的Form组件可以设置size属性,来控制里面的表单组件大小,然后读了一下源码发现是用Provide/Inject实现的,于是我也这样做。

// Block.vue
provide('isEdit', isEdit)

// Edit.vue
<template>
  <el-input v-if="isEdit" :modelValue="modelValue" v-bind="$attrs" />
  <span v-else >{{ modelValue }}</span>
</template>

<script setup>
const isEdit = Inject('isEdit')
</script>

到目前为止,项目还算正常。

迭代

现在业务提出需求,编辑过的区域需要高亮显示变红,而且是永久记录,事情开始有趣了😣

当时我们一群人激烈的讨论了好久实现的思路。

{
    id1: { a: { original: '123', fresh: '456' }, b: { original: '789', fresh: '456' } },
    id2: { c: { original: '123', fresh: '456' } },
}

如上图所示,接口这样返回,这里表示id1这条数据的字段a字段b修改过,id2这条数据的字段c字段b修改过,只要在这里出现的,就意味着修改过。

为什么会有多个id呢,因为有些区域可能是对象,自然就一个id,有些区域可能是数组,就是多个id,如下图所示:

image.png

如果是嵌套数组的话,就直接拍扁。

至于{ original: '123', fresh: '456' }是为了日后预留,万一以后提出要看到以前修改的旧值的需求呢。

然后Block组件内置请求,通过不同的参数来获取该区域相应的编辑记录。

现在拿到数据了,那该怎么比对呢?

image.png

这是我一开始设想过的各种方式,传记录、id和属性名,在Edit里面判断或者直接判断。但无论如何看起来都很蠢,首先v-model里面已经绑定的变量写了amount,然后判断是否编辑过还要再写一次amount

image.png

这样写虽然解决了重复问题,但是仍然要修改非常多代码,因为有非常非常非常多的格子,每个格子都要这样修改累死人了。

image.png

毕竟懒才是第一生产力,我决定尽量保持原来的业务代码不变,那Edit组件到底怎么获取记录,id,还要属性名呢?

id利用v-for遍历Row组件时设置key属性来获取,如果不是遍历,那就手动设置key属性。

属性名通过截取v-model绑定的值来获取。

快来瞧瞧具体实现吧。

获取记录

// Block.vue
provide('records', records)

// Edit.vue
const records = Inject('records')

isEdit一样,通过Provide/Inject传递

这里还算正常,下面全是骚操作了

获取属性名

// Edit.vue
<template>
  <el-input v-if="isEdit" :modelValue="modelValue" v-bind="$attrs" />
  <span v-else >{{ modelValue }}</span>
</template>

// 使用
<Edit v-model="item.amount" />

先来回顾一下Edit组件是这样编写与使用,我并没有手动绑定input事件再emit出去,而是利用v-model的语法糖自动生成更新事件,放在attrs中,然后内部vbind="attrs中,然后内部 v-bind="attrs" 就可以绑定事件了,不知道的同学可以看看这个 v3.cn.vuejs.org/guide/migra…

// vue2
console.log(this.$attrs)

// vue3
setup(props, { attrs }) {
    console.log(attrs)
}

我们可以打印出来瞧瞧

image.png

const fn = String(attrs['onUpdate:modelValue'])
const temp = fn.slice(0, fn.lastIndexOf('=')).trim()
const key = temp.slice(temp.lastIndexOf('.') + 1)

这样可以拿到<Edit v-model="item.amount" />绑定的属性名amount

  • modelValue是vue3的v-model默认绑定的变量名
  • 别被控制台迷惑,attrs['onUpdate:modelValue']拿到的函数,我这里使用String()把它转为字符串
  • fn打印出来就类似这样了$event => item.amount = $event,我们可以通过字符串剪切来获取amount

当然这里有大坑,我们等下再说

获取id

image.png

上面我们说到既然v-for时要设置key值,而key值是用id值来设置的,我们可以利用此来传递id

于是正常思路就通过props设置key来获取,可您猜怎么着,居然拿不到!

然后我再把attrs打印出来,也没有!

奇了怪了,这个key到底去哪了,于是我把this打印出来

image.png

最终在$_中里面的vnode中找到

原来是被vue用作虚拟dom中使用了

// Row.vue
setup(){
    const { vnode } = getCurrentInstance()
    provide(businessId, vnode.key)
}

// Edit.vue
const businessId = Inject('businessId**')**

vue3我们可以这样获取key,并和之前一样,通过Provide/Inject通信,不过这样做是有坑的,下面会说到

然后Edit组件就可以判断是否编辑过了,完整的代码如下:

const records = inject<Ref>('records', ref({}))
const businessId = inject<string>('businessId', '')
const hasEdit = computed(() => {
      if (!businessId) return false
      const fn = String(attrs['onUpdate:modelValue'])
      const temp = fn.slice(0, fn.lastIndexOf('='))
      const key = temp.slice(temp.lastIndexOf('.') + 1)
      return !!records.value[businessId]?.[key]
})

?.是es6可选链操作符,不懂的童鞋可以查一下

虽然我们拿到id了,但还可以做的更好

image.png

首先封装成hook

image.png

然后在RowTable组件中使用,因为有些并不是数组,就一个对象,而且有多个Row,直接在Table设一次id就方便许多。

拿不到id

这个时候坑就来了,有很多地方居然拿不到id, 这就牵扯到一个问题了,大家想一想,如父组件Provide一个变量叫a,然后子组件也Provide一个变量叫a,那请问孙组件Inject接收到的变量a到底是谁的值?

然后我试了试,答案拿到的是子组件,这是符合直觉的,就像样式position: absolute一直向上找到最近的position: relative,还有访问对象的属性一直跟着原型链向上找到为止。

出问题的原因是,虽然直接在Table设置key,Row不用设key了,可Row还是在Provide,所以要改成这样:

image.png

加个判断,只有当设置了key值才Provide

有些格子是没法正常反显

<Edit v-model="a ? item.b : item.c" />

这样写的话,我们上面的获取属性名的方法就失效了,所以就改成这样即可:

<Edit v-if="a" v-model="item.b" />
<Edit v-else v-model="item.c" />

其实还有更好的解决方法,但我懒了

测试环境所有的格子都是没法正常显示

经过排查发现,是在获取属性名的时候出问题了:

// dev
e => item.a = e

// test
e=>item.a=e

千万别利用空格截取字符串,因为只有开发环境才有空格,打包后的代码因为有压缩,所以是没有空格的

为了兼容不同的环境,所以可以使用trim()去除空格

结束语

其实这篇文章我主要分享的是利用key传递属性截取v-model绑定的属性名这两个歪门邪道,就本身这块业务来说有非常多的点我没有说,比如万一绑定的属性是a.b.c.d,那样该怎么做呢?编辑的时候格子太多,一下子变成输入框可能会卡顿,所以使用requestAnimationFrame分批更改是否编辑的状态。再比如,如果某个区域有多个table的话,那是不是该封装一个专门用来provide key的组件来包裹更方便呢,不用每个table都设置key等等非常多问题。

最后祝大家虎年虎虎生威,愿大家只用拉屎山第一坨翔,不用接着别人的拉😏