前情:源码没了,只剩线上跑着的编译产物
接手一个集装箱买卖销售系统的维护工作时,拿到的东西有点尴尬:系统在线上跑得好好的,但原始工程文件找不到了——没有 .vue 源文件、没有 package.json、没有构建配置,只剩一份编译产物。
这套系统是典型的三端结构:
- 后台管理端(Vue)—— 用于内部运营管理
- 员工端(uni-app)—— 现场业务人员使用
- 用户端(Vue)—— 对外展示与下单
目标不是"凑合改压缩代码",而是把它还原成可编辑、可重新编译的源码工程。下面是完整的过程记录。
第一步:先给产物做体检
动手之前先搞清楚"还剩什么",这直接决定后面工作量的大小:
- 有没有 source map?——有的话基本等于送分,能还原到接近原始代码;没有就只能硬啃。
- webpack 还是 vite?——看 chunk 命名规则、runtime 特征与加载方式。
- 压缩到哪一档?——是变量名单字母化,还是做了更激进的处理。
- 有没有骨架文件?——入口文件、运行时文件能反推模块边界和依赖图。
体检结果:没有 source map,webpack 产物,业务代码变量名全被压成 a/b/c/d 这类单字母。纯人工读代码基本没戏,必须上工具链。
第二步:找到可读性最好的"那一版"产物
这是整个还原过程中性价比最高的一步。
压缩产物通常分两种形态:
- minified:变量名被压缩,体积小,但可读性极差
- unminified / dev 模式:变量名可读,只是没做体积优化
同一套源码在两套构建配置下,产出的"逻辑结构"是一样的——只是名字被改了。所以只要拿到未压缩版产物作为参照,还原难度会下降一个数量级。
做法是:在构建产物里找线索,反推它用的构建开关,再用等价配置重新产出两份可对照的版本(一份压缩、一份不压缩),拿不压缩的那份当作"字典"来对齐压缩版的结构。
第三步:模块与文件结构还原
从入口文件读起,顺着模块加载调用图往下走,可以把整个依赖关系画出来:
- 业务代码与第三方库的边界在哪里
- 哪些 chunk 是按路由懒加载拆分出来的
- 每个模块导出了什么、被谁引用
再结合路由表反推,就能恢复出类似 src/views、src/components 的目录树。这一步做完,工程骨架就先立起来了。
第四步:变量名还原(最容易翻车的一步)
压缩后的代码长这样:
function a(b,c){var d=b.length;if(d>0){for(var e=0;e<d;e++){...}}}
最直觉的做法是"全局搜索替换",但这样必然撞车:a 在 A 模块里代表用户对象,在 B 模块里代表一个数组。全局替换成同一个名字,代码直接废掉。
正确做法是作用域感知的重命名(scope-aware rename):
- 先把代码解析成 AST;
- 建立完整的作用域树(函数作用域、块作用域、模块作用域);
- 只在每个变量的真实作用域内做替换——同一个名字在不同作用域里,可以分别还原成互不冲突的语义名;
- 根据使用特征推断语义(是数组?是 DOM 节点?是配置对象?是回调?)生成候选名;
- 回写代码,并保证替换前后 AST 结构等价。
这一步我们基于 Babel 写了一套重命名工具。但要强调:推断得对不对,不能靠感觉,必须靠验证器说话。
第五步:语义等价验证——不能只靠肉眼看
代码"看起来对了"和"真的等价"是两件事。所以在还原过程中同步写了一套验证器,每个阶段都跑一遍:
- 压缩代码与还原代码的模块依赖关系是否一致;
- 导出/导入的符号是否一一对应;
- 关键业务逻辑的分支结构是否相同;
- 变量重命名后,是否存在意外的引用覆盖或作用域泄漏。
三端各自维护了自己的验证脚本,输出"哪些文件通过了、哪些还有差异"。有差异就回到上一步查,直到清零。这套验证器是整个还原工程里最有价值的部分——它让"还原"变成一件可度量的事。
第六步:页面级验收——把每条路由都钉下来
代码层面等价还不够,还要证明页面表现一致。这里用 Playwright 把 SPA 的每条路由逐一访问:
- 驱动浏览器真实加载页面,等渲染完成;
- 把每个路由的渲染结果单独保存为一份独立 HTML;
- 与线上实际页面对比结构,确认没有缺块、错位、丢组件。
两个必须注意的坑:
- 后端接口要真实打通——不然后端渲染的数据对不上,页面结构看起来"缺一块",会误判成还原失败。部分页面本身依赖远程服务,必须让它先可用。
- CORS 要放行本地调试来源——不然本地打开一片空白,白折腾半天。
另外,我们最终选择不把页面打包成单文件 HTML。单文件捆绑看起来方便(双击就能开),但那是"存档"而不是"工程"——后续没法维护,也没法独立部署。我们要的是可以独立编辑、改完刷新即生效的多文件结构。
收尾:还原之后的工程形态
三端最终都还原成了可独立编译部署的工程:
- 前端与后端解耦,前端可以单独打包(后续也可以打进 App);
- 用户端单独部署在自有站点上,接口指向独立的后端服务;
- 后续任何需求改动都在源码层完成,不再触碰编译产物。
经验清单
- 动手前先做产物体检——有没有 source map、什么构建工具,决定了三天还是三周。
- 优先找未压缩的那一版——可读性直接决定工作量。
- 重命名必须做作用域感知——全局字符串替换是纯粹的坑。
- 每一步都要有可执行的验证器——"我看着像对的"不算还原,能跑过验证才算。
- 交付形态选多文件源码工程——别交单文件捆绑,那是存档不是工程。
- 页面验收要打通真实接口——否则你验证的是"缺数据的假象"。
整个过程的核心其实就一句话:把"不可读"变成"可读",再把"可读"变成"可验证",最后把"可验证"变成"可维护"。