背景

在业务发展过程中,经常有一些意外的情况出现,比如突然页面白屏了、某些浏览器兼容不了、发布比较慢等等各种问题,很自然的,发生了什么问题,那么就解决吧,这种直截了当的思路,一般不会错,但是带来的结果就是不断的救火的过程。也是因为这个背景、这个原因,我开始着手治理前端项目,从各个方面,一边不影响现有业务的进化,一边做着项目的优化治理。这些问题的发生,也让感受到有一些更加深层次的底层问题,不仅仅只是代码问题。

一个ToB的产品,从前端的形态角度,其中一个dashboard式的中后台的管理系统的这么一种形式。大多数的页面、组件是非常相似重复的,基本上是纯工作量,也有一些非常复杂的逻辑的页面和功能,比如富文本编辑器(这个功能的代码量比项目里面其他功能的代码加起来可能还要多)、活动编辑(页面看似简单,逻辑极其错综复杂,各种规则配置)等等。对于前端代码层面来说,其具有了两面性,有工具产品的复杂技术性,也有纯粹展示的重复简单性。

  • test
  • ste
  • st
  • st

问题

解决问题之前,首先得搞清楚问题到底是什么。从问题的外在表现,大概分为下面几类问题:

  • 纯粹的bug:点着点着,会出现报错,白屏等问题 - 通常这个是因为代码层面的逻辑漏洞所导致,测试不充分,代码的鲁棒性不够!
  • 兼容问题:在某些浏览器中行为不一致,或者干脆白屏(比如在IE中)!
  • 工程效率问题:知道了代码问题,修复、hotfix发布流程不稳定,有时候需要长达1个小时都发不完!本地调试,hot reload,编译的速度都有一些问题。
  • 人为操作的问题:操作发布,本地编译,合并代码,发布分支等等都需要手动选择然后完成,自己也是多次操作出错!
  • 其中还隐含了一个问题就是,这些bug的发现都是后知后觉的,没有比较好的办法主动发现问题。

解决思路

这些问题总结来说就是 人为干预过多、规范测试的缺乏、工程效率的低下。
这些问题也不是什么新鲜的问题,很多开发者,很多人都会遇到这些相似的问题,开发者几乎每天都会遇到这些问题。出现了问题,然后改,出了改、出了改这种救火的方式适合临时解决,但是不是长久之计。不能仅仅思考为什么会出现问题,然后解决问题,但其实问题是解决不完的。必须假设肯定会出问题,所以容错、主动发现错误是非常重要的。

在这样的理念下,整体的解决思路是三步走。

three.png

  • 完善主动发现问题的机制
  • 提高工程效率,提高解决问题的效率,直观的讲是从 编码 - 编译 - 部署 整个流程的效率优化
  • 深入代码层面,优化代码结构、架构,沉淀规范

由外而内的解决,如下图所示,一层一层,直到代码层面。代码重构放在最后去做是因为如果没有较好的主动问题发现,工程效率(发布、部署等等持续集成),那么代码的修改会非常的危险,而且代码的重构是最话时间的,不可能一蹴而就,而是通过多个迭代,一点点的迁移。

解决过程细节总结

执行原则

在治理的过程中,给自己定一个简单的原则

  • 有好的轮子就不要造轮子
  • 在解决的过程中,使用的是最成熟的方案,使用已有的好轮子。

第一步,主动发现

出现问题不可怕,可怕的是不知道会有问题发生,不知道哪里出了问题,永远是别人比自己先发现问题。
最外层的机制是通过一些手段,能够让我们主动感知问题发生,然后在别人发现前修复之,扼杀在摇篮之中。
这一步比较快速的可以完成,主要添加以下一些机制来保障:

  • 错误监控,通过接入内部的Sentry来查看,上报没有被catch住的错误。
  • 数据监控,通过接入MTA来查看全方位的数据(pv,uv,性能,访问深度,自定义事件等等),这些数据一方面对于业务也有指导意义,一方面可以给我们技术参考,哪些页面的性能等指导,通过自定义事件可以比较精准的发现隐藏的逻辑问题等等
  • 静态分析,静态扫描代码,可以主动发现一些代码层面的问题(安全漏洞,高全复杂度的逻辑等等)

这些机制虽然不能保证可以发现任何一个问题,但是一定程度上可以了解到应用运行的一些状况,通过一些打点数据,可以提前知道发现一些问题。然后可以主动的去解决。

第二步 - 工程效率

本地工程改造

使用react-scripts 来替换原有的自定义webpack脚本。react-scripts基本上可以达到零配置的构建react SPA的应用,其本质是对webpack的封装,使用之大大减少了自己维护构建脚本的成本,只需要一些小的改动就可以满足。和原来相比,有以下提升:

  • 减少脚本的维护成本,去除原本自己维护的webpack脚本,以及一些hack的脚本
  • 增加原本缺少的lint规则,更加严格的lint,可以提高代码的质量
  • 更快构建速度,更快的热重载

对于基于react单页应用的零配置构建,社区中已经非常的成熟,完善。我觉得在小团队的情况下,去了解这些技术原理使用是有必要的,但是自己重头配置调试已经非常的没有必要,徒增增加维护成本。

发布流程改造

老的发布流程:开发完毕 -> 本地编译 -> 提交git -> 提交ars单 -> 走ars单的发布

这个流程中遇到有几个问题:
1.本地编译,每个开发者环境不同,导致每个人编译的hash不同,产生冲突,并且这些编译后的文件提交到git,使得git仓库臃肿
2.ars发布需要必须走完测试、beta、生产的流程,并且ars在任何一个流程,都需要比对文件,由于前端编译后会带上内容的hash作为文件名的一部分,这就导致每次发布文件名不同,积累的文件过多,而ars需要每次diff比较,导致任何一步都非常的缓慢,有时候修复个bug,走一边发布流程,1个小时都没发完。

新的流程:开发完毕 -> 提交git -> 跑测试 -> 编译机编译 -> 发布静态文件(js,css等)-> 发布index.html至web服务器

自定义整套流程,把编译放在统一的编译机上处理。真正发布的时候,先把资源类静态文件(js,css,media等)发布到cdn服务器,由于这文件带有hash,所有不会覆盖线上的静态文件,暂停流程,等cdn生效后,把index.html入口文件发布到web服务器中,从而线上生效。细节的具体流程见下图。

CiFlow.png

新老流程的主要区别就是采用持续集成工具,把编译、测试、部署等流程都标准化自动化,重点在于自动化。流程改变重要的不是流程改成了怎样,而是尽可能的标准化、自动化,减少过人为参与关键部分的可能,从而减少人为操作的可能,并且使得每一次的编译部署产物都是相同的。

改变所产生的效果是:

  • 统一编译环境,不会因为开发人员本机环境不同,可能导致的编译结果的差异
  • 没有人为的编译操作,减少出错的可能性
  • 使用自定义ci流程,使得发布流程更加可自定义,可控,可以加入更加自定义的能力(比如灰度等等)
  • 使用ci,缩短了发布的时间,现在大约5 - 10分钟完成从编译、测试、beta到线上部署的流程。

第三步 - 代码层面改造

有了上一层的改造,整个开发的效率得到提高,现在终于到了代码层面的改造了。到了这一层,就需要深入代码细节来做改造。

架构、基础组件库变更

项目原始使用的react + mobx的状态管理方式,mobx本身是非常厉害的库,但是mobx本身脱离了react未来的发展路线,更加的大的问题是mobx相当于带来了一种双向数据绑定的效果,这导致代码的书写可以随意的赋值变量值,然后ui自动刷新,随着代码量增多,尤其是比较复杂的功能,这种方式导致状态的不可控,不可追踪。当然不是说mobx不可能单向数据流,毕竟单向数据流是一种理念,但是mobx太方便了,导致很难强制,所以改用Redux,redux强制了单向数据流的写法,直接规定死,这样防止过于自由导致不可控。

随着react hook的发布,react团队不断的强化 immutable + functional 的理念,可以看到未来react还是会往这个方向发展。从mobx改为采用 redux + immer 方式来管理应用的状态,拥抱react hook,优先functional component形式。其他保持不变。

还有一个变动就是基础组件库的变更,由于早期采用的基础组件库bere不在维护,所以改用tea组件库作为替换。

代码改造没法一蹴而就,整个重构通过了3个版本的迭代完成(3个月左右),所以在一定的时间内有两种不同风格的架构、代码、组件库存在。改造的原则是:

  • 新的页面功能采用新的方案
  • 旧的功能分迭代,分批重构,充分测试

在重构的过程中,出现过一些改造的问题,虽然是按照老的逻辑平迁,但还是会遇到一些坑:

  • mobx平迁为react hook的方式,会有以下问题需要注意,react hook状态更新,刷新ui的方式并非是mobx的同步,而是异步,合并结算的。对于需要同步的值,需要使用useRef来暂存,从而达到同步的效果。这也是我在重构的过程中,不断碰到的问题,保持小心。

这些坑让自己警醒反思,要细致小心,以方面也让我加深了对react的理解。

其他

除了上述和技术本身相关度较大的改造,还有一些周边的完善,来进一步提高项目的可维护性。

  • 完善文档,通过Readme, Wiki。写清楚本地调试,发布部署流程,目录结构,代码工程规范等等
  • 测试:主要是关键公共组件、方法的单元测试,并且和ci结合
  • 指定工程、代码、目录的规范。
  • 一些细节补充:
    • 使用Error Boundary来使得错误的逻辑不至于使得整个APP崩溃。Error Boundary检测到错误后fallback为一个提示
    • 页面、组件相关文件自包含,所有相关的文件放在同一个文件夹内,通过业务来区分。而非早期的按page、component这种类型来分。
    • 由于css是有重构的同事提供,通过使用 purgecss 在编译前把不需要的css去除,减少css的大小。

我相信不仅仅代码需要写得好,也需要注重注释、文档这种细节,很多时候一个逻辑,写得时候很清楚,但是自己过几月再看,或者换一个来看,可能就看不懂了,项目的可传承性、可维护性也是非常重要的。

写在最后的思考

虽然可以通过各种不同的方法来提高代码质量,减少人为的操作,但是不管如何去做,在智能化没有来到前,最后都会有一个极限,最后产品的逻辑代码还是需要每一位开发者自己来书写,需要测试来测试,人为的引入错误依然是无可避免的。隐约记得 Erlang的作者 Joe Armstrong,在设计Erlang语言的时候,就考虑到如何容错,可以自动从错误中恢复,在AT&T使用Erlang编写的电信系统中,创造了几十年没有发生事故的记录。

一方面,我们开发者需要细致、考虑全面,另一方面,需要知道发生错误是不可能避免的,所以考虑如何从错误中恢复,如何把错误保持在最小粒度可能是更重要的命题!

路漫漫其修远兮,共勉之~