这篇文章上次修改于 805 天前,可能其部分内容已经发生变化,如有疑问可询问作者。

在某一天,小明同学拿到了一个前任(兼职)写出来的一个后台项目。

仔细一看,整个项目采用了 DvaJS(Redux 全局管理)的技术栈,感觉这玩意有点意思。但前任编写的代码全部是 JavaScript,没有 TypeScript,他起初以为这不算什么...

迷人的组件传参

直到某一天,小明同学需要开始正式接手这个项目的迭代开发,需要设计一个表单,而表单里面需要有一处上传图片功能的组件。他找到了前任写的「通用上传组件」。

这个组件有这么多 props 属性,它们都是干啥的呢?

props-1.jpg

  • onRef 看起来是把组件里面的状态引到外面
  • propsParams 看起来是告诉组件应该以什么效果展示,什么形式处理文件
  • editDetailData... 这是干嘛的?
  • logo 看起来是预设一张图片展示出来
  • setFieldsValue 看起来是把上传的文件写到表单状态里面
  • callBackFileList... 这又是干嘛的?
等他找到组件源代码之后搜索发现,这参数怎么没用到呢?更迷咯~

props-2.jpg

他试着全局搜索,发现用到这个组件的地方还都传了这个参数,却并不知道它到底有什么用

props-3.jpg

他只能忍声吞气,背负着这一屎坑组件,最后选择直接删除了所有的相关参数。而 editDetailData 这个参数,研究完代码之后看起来是将表单项目配合 logo 参数拿去填充 Antd 上传列表的标题去了。

这个二次封装的组件还有一个 State,这个 State 里面的 upload 最终作为 Antd Upload 组件的参数传了过去,更谜了。简单研究了下,如果要把现有表单的值回传,就只能通过 editDetailDatalogo 传入给 Upload 组件,然后通过 触发 Upload 组件的 onChange 回调再调用父组件的 setFieldsValue(最终父组件执行的是 Antd 的表单值设置方法)修改表单项,最终完成展示,感觉写的很离谱,但又不知道怎么吐槽...

这样的上传组件就把逻辑强制绑定在表单这个场景上使用了,非常不灵活,如果要切换到 Redux 托管表单数据的场景,状态设置混乱,就会产生很大的问题。

后续:小明同学最后选择基于 Antd 的 Upload 重构了一个同类组件,但改成了静态属性,仅接受首次传入的文件列表(value + onChange 配合表单组件的设计),后续组件外的状态修改(例如重新设置表单项的 value)均不会影响到组件的展示

后端需求变更,如何又产生一个新 Bug

某处业务表单用到了 Antd 3 的 CheckBox 组件实现多选功能,由于需求变更,这里被要求改成单选项。当小明同学将 CheckBox 换成 Radio.Group 组件之后点击表单自测发现,这貌似并没有问题。

// 表单某项的外部状态
const [checkedVal, setCheckedVal] = useState("")

...

// 表单提交前合并得到的数据
const _payload = {
  [hasCommitStatus !== 'ADD' && 'id']: queryDeviceTypesDetail.id,
  components,
  index,
  name,
  // 注意这里,最终提交表单的时候再把这个表单外部的 State 拼接进去的
  brand: checkedVal,
  img: img[0] ?. url,
  icon: icon[0] ?. url,
}

...

{/* 表单项 */}
<Form.Item label={languagePack.brand}>
  {getFieldDecorator('brand', {
    initialValue: checkedVal
  })(
    <Radio.Group
      options={plainOptions}
      // 就是下面这个 onChange,原作者就这么写的
      // 其实可以直接用 Redux 和表单状态配合存值的,也不知道为什么他这么写
      onChange={handleChangeCheckboxVal} 
    />
  )}
</Form.Item>

直到后续有人测试,发现这里选中的项目无法存储到后端,他仔细排查问题,其实是由于这里的回调从 number[] 变成了 Event 对象,这才揪出了原因,是他当初修改组件的时候并没有留意到回调参数类型的改变。

没有 TypeScript 的情况下就没有报错提示,而 Antd 的 Radio.Group 组件本身不需要更改后的 value 传回去都能独立运作,所以即便类型错误了,也能看到“正确”的效果。

// Before
const handleChangeCheckboxVal = checkedList => {
  setCheckedVal(checkedList)
}

// After
const handleChangeCheckboxVal = ev => {
  setCheckedVal(ev.target.value)
}

这种绕开表单状态的写法本身就是愚蠢的,耦合性太高了,我认为这就是给后人埋下炸弹的行为== 没有 TypeScript 的纠错,这样的 Bug 就很容易藏在里面,不易察觉。

正确的思路应该是从 Redux 拿到数据的时候就应该提取出来,拼凑出表单(新增和编辑都得一样)需要的键和值,而不是拿到值之后丢给页面额外处理。这样提交的时候就可以直接读取 Antd 的表单项,而不应该是从 checkedVal 这个表单外状态中获取再进行拼接。

总结

通过这两个实际案例,可以说 TypeScript 在多人协作的项目里面还是能起到不小的作用,自带文档属性。它能把类型错误或无效的组件参数抛出报错,处理组件输出和回调的时候,也能提前检查比对返回类型,减少很多可能因传参错误,导致结果和预期不一致的问题。

异步提交表单的情况,也可以提前将后端接受的类型录入进行类型定义,可以保证提交表单内容各个字段的类型是正确的。

当然这所有的基础,都需要一个合格规范的后端代码,以及配套的类型定义,如果你的 TypeScript 代码里面出现了大量 any@ts-ignore,那就还不够合格,需要继续努力了!💪