那些年我给一个10年的老项目做改造升级

2024年5月2日

背景

事情要从 2019 年 从一家创业公司离职开始说起,加入了一家千万级用户的 SaaS 建站系统,在那时候我是希望沉淀前端技术能力,而这算一个还不错的 offer 。

进去发现技术栈跟我想象的差别太大了,还是用的 svn,技术栈是 jquery + Vue 2.6.10,很多库都是自研的,有些项目甚至不能用 ES6,但代码量有 60 多万行,业务也是重前端,我想着有机会能落地一些事情,于是在短暂的做了 3 个多月的业务后,我开始思考如何更好地升级这些老的项目,使得开发效率和体验变得更好。

回顾起来,当时有三个比较大的影响项目开发效率和质量的点:

  1. 前端工具链严重落后,无法支持新语法、本机开发、无模块化等。
  2. 未前后端分离,在 JSP 拼接 HTML 和 写 JS 逻辑。
  3. 少量组件化,大量 DOM 操作,而大部分 bug 都出现在此。

开启工程化

由于项目是未前后端分离的,结构上是单页面多 bundle ,跟 Vue-CLI 这类的单入口、多入口多模板的模式不匹配,我们选择直接操作 Webpack 来封装项目的 Cli,而打包入口的规范是可以和目录结构约定一起的,于是我们重新构建了这类项目的目录结构规范,大体如下:

项目结构

有了目录约定关系后,我们可以很方便地自动查找打包的入口。

得益于 Webpack 各种 Loader、Plugin 机制,我们定制了可以打包 CSS 入口的 CSSEntryPlugin、url 图片版本号自动更新 等插件。

在规范上,引入了 ESLint + Prettier + CommitLint + Husky 的基础保证,在 pre-commit 的时候卡点检测代码规范,同时也自定义了一些业务规则,比如不允许直接写死官网地址和 HTTP 协议头、不推荐使用 jq.ajax 而是封装好的 request 库等。

在自动化上,我们接入了 gitlab CI 进行自动化构建,但是由于当时还是物理机和 Webpack4,如果全量打包 60 多万行代码耗时会将近 8 分钟,于是我们根据前后 commit 的 diff 找到更改范围,只打包对应的目录入口,每次 CI 构建下降至 17 秒左右,但是这个方案的劣势就是有依赖的公共文件找不到影响的打包入口,为了解决这个问题,我们专门写了个库来解析整个项目的反向依赖关系从而查找到顶层打包入口。

最后来说说如何引入本机开发和 HMR, 由于是没有前后端分离,也就是没有页面数据的接口,无法直接在 localhost 输出,必须得拿到 JSP 返回才行,所以我们采取的方案是直接请求页面地址然后正则替换资源 cdn 和 地址为 localhost 处理,详细流程可以看下图。

但 HMR 会有一个问题, 如果单个页面有多个 bundle, webpack-dev-server 会注入多个 HMR Runtime 导致互相竞争从而无法热更新成功,我们的做法是构造一个 fake main_entry.js , 其他所有要打包的资源入口都 dependOn 这个主入口,这样只会注入一个 HMR Runtime, 从而避免了冲突。

dev-server流程图

其实上面的方案也并非一开始的样子,中间经过了多轮的迭代,现在回顾起来,有几点可能可以做得更好。

首先,技术视野要开阔,未前后端分离的工程化方案其实在网上很少,跟公司匹配的更少,一定要找到核心解决问题的方向,不需要面面俱到。

第二,不要把半成品推广起来用,这样会耗费你的信用,尤其是团队是前后端都需要写的工程师存在的时候,因为他们不知道你做这些事情的意义,只知道更新后不能用了,基建研发好的节奏应该是先吃自己的狗粮,自己测试一段时间后,再给核心的能看懂这件事情的人使用,收集意见,不断迭代,最后才是发布测试版给大家使用,同时做好备用方案不要影响别人的工作。

第三,不要追新而是把项目需要的核心功能研究透,一个新的工具能解决新的问题,但其他问题解决的没有现有的工具好,你的项目需要用吗? 比如 Vite 刚出来的时候,大家都对 3 秒启动,更快的 HMR 趋之若鹜,但是放在老项目里启动完首屏没做好按需加载(改造成本高),首屏白屏也要 1 分多钟,而 webpack5 首次构建 1 分多钟,但二次构建只需要 30 秒,哪个更合适呢?更别提 SSR 的支持,Node 环境的升级 等等问题,所以当时我们使用了一段时间后还是换回 Webpack5,当时给出的判断是新项目的不需要 SSR 的可以用(基于当时的判断,不涉及现在的新版本)。

模块化、组件化

业务代码的改造第一步是新代码全部 ESModule,旧代码逐步迁移,也写了 toESModule 的工具,大致是把 IIFE、window 的方法都转成 ESModule 导出,这件事也比较好做,花费 3、4 人两个版本即可迁移并完成上线。

推行组件化实质是将业务模块由原来的 DOM 拼接 + JQuery 改为 Vue 组件,这是一个长期的有着中风险的工作,因为存在着几十个业务模块并且每个版本还在迭代中,人手跟不上。后面在前后端分离的章节里讲到我这边会根据 JSP 的代码特征做了一层转换能够将 DOM 拼接都转成 Vue 组件的 template,从而提升 60% 的生产力。

这里要注意的是,改为 Vue 组件和 JQuery 混用的情况,尽量只把 Vue 组件当作一个 UI 视图渲染,其他的状态逻辑还是交回 JQuery,否则 DOM 操作视图和 Vue 更新视图会竞争,从而出现意想不到的 bug,当然,这只是针对已有的历史模块,新业务模块的编写还是由 Vue 统一管理视图和事件,少混用 JQuery。

对于外部的组件依赖,除了基础组件库外,可以把业务通用的方法、组件都放在私有仓库上,不需要 copy 到其他仓库,这个有着很大的作用,因为我们是做建站业务的,很多端都有共同的功能,只不过是 PC 端、手机端、小程序端的区别。

推动前后端分离

前后端分离是按季度的排期的中长期任务,当时预计的是 4 个前端,1 个后台一个季度从开发、灰度到上线,结果来看是用了 6 个月才把事情结束。其中比较困难的点有三个,第一,建站系统中间预览区的 iframe 是否要去掉;第二,需要重新设计一套页面级别的 SSR (之前的是模块级别的 SSR); 第三,不仅是分离前后端还是分离两个业务,涉及两个业务部门的协同。

简单概要说一下这几点,详细的可以新开一篇文章来总结。

第一点是否去掉 iframe,业务代码涉及很多 iframe 内部和外部的变量操作,内部都是通过顶层全部变量来做,而我们首要目标是废掉这些全局变量,很自然需要考虑能否去掉 iframe,当时我们的判断是去掉对更好,但这也引发了很多问题,比如 CSS 优先级##和命名冲突,伪静态路由要映射到 VueRouter 之类的严重的问题。

第二点是要支持页面级别的 SSR, 在没有这个之前,我们的方案做每个业务模块的预渲染,实际上是没有客户端激活这一步的(当时也因为要支持 IE9 以下),现在没有这个限制并且是页面使用 Vuex 来管理,是可以考虑做成页面级别的 SSR ,但这里面也有很多权衡,比如 Node 渲染前能否匹配之前使用 JSP 渲染的效果,其中涉及很多 WebFilter 过滤器的操作,还有是否有库支持公司通用的配置中心、监控系统等等。

第三点主要是协同问题,不仅涉及代码上的分离,还涉及业务之间的分离,需要不停和其他部门的前端进行沟通,全局视角很关键,因为一旦设计错了就是两个主要业务买单,避免方案只涉及自己业务而另一个业务不适合,还有就是仓库之间的分离和合并,当时我们定的是业务前端每天合并我们的分支,我们提供人手帮忙解决冲突。

业务开发流程优化

上面说的是工程方面的优化居多,相当于打地基,把土壤弄好, 实际我们大部分时间都在做的都是需求功能的开发,所以在做工程优化的时候同时也考虑如何能够在业务开发这个链路上优化得更好。

我们定了需求开发流程整个大的流程图,增加了需求评审会,大需求的技术方案的设计和评审流程,也避免了技术优化需求比业务需求更多的情况(每个版本技术优化需求不超过 20%)。

此外,我们最近也用上了 Vue 2.7 的 composition API 功能,期待有效地沉淀能重用的业务逻辑。

总结

尽管在做技术升级的过程中伤痕累累,踩了不少坑,但我们能明显感受到紧急工单变少了,bug 工单变少了,开发体验直线上升,从人效来看也有提升,也锻炼了团队成员的技术(能留人了),anyway, 只要判断是有价值,大胆放手去干。