记一次大规模重构

前言

一个健康发展的项目,是不应该有大规模重构的。一个健康的项目,它随时随地的进行着一些小规模的重构,循序渐进,不断改进,积少成多,不断保持项目的活力。但是现实情况是: 我们安逸于现状这样子,虽然它有一些小问题,但是我想我们能够忍受,我们的项目成员不想/敢做出改变,因为它(可能)会触发一些问题。直到有一天,我们不能忍了,因为我们离主流的技术已经很远,我们目前不能使用好的技术方案,我们的开发效率已经太低,我们有很多 bug 不能合理修复,就算修复也可能会带来更多的 bug。所以我们决定来一次大的重构。

因为我们都这样:小毛小病的,没有什么大碍。直到病入骨髓了,我们痛下决心,做一个大手术。其实小毛小病,生活注意一下,或者吃两服药,就可以药到病除,然后手术却没有那么轻松,如果运气不好,轻则留下浅浅伤疤,重则可能危及生命。就算手术顺利,也需要好长的一段恢复期啊。

但是如果真要进行一次大手术,我们应该要怎么做才能平安度过呢?说说我自己的真实故事吧。

动手术

我上个月来到现在的设计家上海团队。我的第一个大任务: 前端代码的分拆。设计家的整个前端是一个超复杂的 Web 应用,单单是 JavaScript 的代码行数就超过了 20w 行,不包含第三方库。在没有做任何的代码分离之前,我们所有的代码都是放在一起的,包括应用的核心框架,图形的底层操作API,业务组件(UI及业务逻辑)都放在一起。由于历史的原因,不同时期的代码不仅风格迥异,而且打包工具也不统一。这样的一个巨大的混杂的代码库会导致很多问题, 比如

等等这些可见的问题,还是一些隐藏的问题,比如不同的代码风格,到沟通成本上升。各模块之间的依赖交错复杂等等。

经过一个月的努力,终于把一个超级巨无霸单一项目分拆成了五个独立的项目,一个主项目(主要是网站的业务代码,是整个 3D 设计家的入口), 以及一个核心库项目(它是整个 3D 设计家的框架层代码),还有其他三个小的基础 API 项目,除了主项目,其他的项目都以 npm 包的形式为外界提供服务。每个项目自己的构建方式对外界透明的。

代码层

在进行分拆模块的时候,如下因素.

显然框架层面的代码最应该被独立出来,因为它的接口必须保持一定的稳定性,而且和业务逻辑完全无关,当然它的特殊的构建方式也让我第一时间把它分拆了出来成为独立的项目,对外以提供 npm 包的形式服务。这样我就可以第一时间在主项目中使用统一的构建工具来完成构建 (webpack)。

其次,一些提供特殊功能的组件,比如数学计算,图形操作等这类基础的 API,也立即被提出来。同类型的还有一些有着代理作用的代码,它们连接着我们团队的主项目和其他团队的库,这些代码通常被不同团队的开发者改动。

最后主项目就变成了一个纯业务逻辑的项目,主要进行各种功能的维护,新 feature 的开发,是最为 Active 的代码。而分离出去的项目都以独立的 npm 包为外界提供服务,它们的开发较为稳定一些,而不同的项目可以按照团队的技术选择进行多样化的开发,这样在独立项目以上可以多样化技术选择,而同一个项目内部则保持统一的,这样就既兼容了稳定性,而又保持了技术的良好更新迭代。

CI/CD 层

由于项目的拆分,如何保证各个项目中的代码依赖能够保持同步,保证开发效率的同时能够项目能够保持很好的持续集成和持续部署成为了我们首要解决的问题。

自动构建

分离出去的独立项目,它们的发布行为就演变成了一个 npm 包的自动构建和发布。为了保证项目稳定,每一个分离出去的独立项目都采用 release 和 develop 两种形式的 npm 版本特征,它们分别对应的项目的 releae 和 master 分支。当 master 有新的提交的时候,项目在 CI/CD 系统自动构建然后发布 x.y.z-develop-build-number 这样版本的 npm 包,同理,如果是 release 有新的提交,则会自动发布 x.y.z-release-build-number 这样版本的 npm 包。

关联触发

经过分拆之后,我们项目由原来的一个巨大的 A, 变成了五个小 a.

Super Big A => (a1, a2, a3, a4, a5)

那么显然,任何小的项目(a2, a3, a4, a5)有更新的时候,我们都必须要触发主项目 (a1) 的构建和并且进行相关的自动化测试。所以在我们的 CI/CD 系统中,我们在 Jenkins 的 job 配置中进行了项目的关连触发。并且主项目(a1)的构建会根据当前所在的 branch (release 还是 master)自动安装与其对应的其他项目(a2, a3, a4,a5)的最新依赖包。然后进行构建和自动部署到对应的开发环境(alpha) 或者生产环境。

反思

这次的大手术,整体来说结果是好的,虽然其中遇到了不少的问题,但是趟过了这些坑之后,也让我自己在更多的方面了解了自己,了解了团队,与此同时在提醒自己在哪些地方需要努力。我自己总结了下面几点我们没有做好的地方:

团队选择刚刚加入团队的我来进行这次大手术,确实不能说是一个正确的选择。虽然我自己对于大型前端的架构有一定架构能力,而且对于主流的构建工具也很熟悉。但是项目的重构不是一个小手术,我们需要一个对整个项目都十分熟悉的人来进行主导,而且最好能够有一个得力的助手来一起完成。因为任何的项目都有很多隐含的坑,特别这种需要大手术的项目,很多的坑稍微一动还有可能会致命,只有对项目十分熟悉的人,才能够知道如何避开(或者直接解决)哪些坑而不耽误重构。
当然,项目组最开始是安排了对项目了解最深的工程师来和我一起来进行的,不过进行到一半的时候,他离开了团队,最后只得我一个人来进行。这是一个谁也没有想到的意外。所以当重构的过程中,如果出现任何的问题,我几乎都会花两倍的时间去解决问题,因为我首先需要去知道到底为什么出现问题,原来采用什么方法来解决,现在我需要选择什么替代的方法来解决。更糟糕的是,我现在需要花大量的时间去和其他不固定的同事去沟通来保证各个模块在重构的过程不受到伤害。

在进行这次重构,我们并没有设立一个可以量化的目标,我们仅仅只是有这么一个愿景: 重构之后,项目还能和以前一样运转正常,并且开发效率能有所提高。但是这样一个笼统的希望对于整个重构没有任何帮助。我们需要设定一个可以量化的目标,比如所有的测试不能被破坏,构建的时间不能超过10分钟,构建工具必须统一等等。

这是一个老生常谈的问题了,没有完备的测试,每一次的改动都会让你惊心胆战。因为你很难确定你的改动会不会造成什么伤害。这也是后续我们希望可以在团队中推广的,也许每一次的开发都做到 TDD 很难,但是必要的单元测试希望可以做到。

由于项目的重构会影响到每一个开发者,所以最好让我们一个团队成员都能够理解到这次改动可能造成的影响,不然出现任何的问题,很多人都会感觉很意外,而且束手无策,这样无疑对于重构产生的问题的处理没有任何帮助,而且可能会雪上加霜。

虽然遇到了不少问题,甚至可能有的同事可能会反感:项目运行的好好的,干嘛搞事情啊。但是总体的结果是好的,这不仅对于让我们项目更加健康的发展做好了很好的基础,而且在今后也会大大的提高我们的开发效率,同时保证产品的快速稳定迭代。