1. 框架还在跑支持已经停了先看清现实去年接手一个内部管理系统时我在代码仓库里翻到了angular.min.js的引用版本号停在 1.8.3构建脚本里写着angular: ^1.8.0。当时第一反应不是要不要重构而是一个很实际的问题这玩意儿现在从哪儿下载构建机换了一台、CI 镜像重建了一次npm install拉不下来整个发布链路就断了。这大概是所有还在维护 AngularJS 老项目的团队都会撞上的第一堵墙——AngularJS 结束官方支持之后框架代码获取这件事从敲一行命令变成了需要提前规划资产归档的工程问题而一旦业务上又冒出必须修的需求扩展支持就成了绕不开的活儿。这篇文章想聊的就是这两件事一是历史代码怎么稳稳拿到手并长期留住二是当官方不再受理 issue、不再发补丁的时候团队自己怎么建立一套能撑住的小型维护体系。适合的读者很明确——手上还养着一两个 AngularJS 前端项目、暂时没有重构预算、又必须保证线上能跑的技术负责人和一线开发。这里面有不少坑我自己踩过包括构建机静默失败、补丁打重了导致内存泄漏、以及一次因为压缩文件版本对不上排查了整整两天的低级事故。先摆正一个前提这个框架的许可证是宽松的开源协议官方停止维护不等于你不能用、不能改、不能分发。它只是从有人替你把关变成了你自己替自己把关。这两者之间差着一整套流程而流程的价值远大于代码本身。1.1 结束官方支持具体终结了哪些东西很多人以为停止支持就是代码不能下了其实恰恰相反代码一直在那儿只是再也没人去动它了。真正终结的东西可以拆成四层来看每一层对团队的影响完全不同。第一层是功能演进停止。不会再有新特性、新指令、新的性能优化也不会再有对新型浏览器能力的适配。这意味着你后面遇到的任何新问题都得自己判断是新框架的锅还是浏览器的锅。第二层是安全补丁停止。这是最要命的一层。官方不再主动审计漏洞也不再为已发现的漏洞发版。如果哪天某个前端通用攻击手法被披露而这个框架恰好受影响不会有人给你一个 1.8.4。这一点必须写进团队的风险台账里而不是嘴上说说。第三层是生态兼容性衰减。周边的东西——构建插件、测试运行器、类型定义、UI 组件库——会陆续把最低支持版本抬上去或者干脆删掉对老框架的适配代码。你今天能装上的某个工具半年后升级一个主版本就装不上了。第四层是官方渠道的可见性下降。文档站可能被折叠到归档目录Issue 区被设为只读问答社区的活跃度归零。这不影响代码运行但严重影响新人上手和问题排查效率。把这四层分清楚后面的策略才有针对性第一、四层靠归档 内部文档解决第二层靠自建补丁流程解决第三层靠依赖冻结解决。三件事的做法完全不同混在一起谈就会陷入到底要不要重写的无休止争论。1.2 为什么大多数团队短期内还得留着它我在几个不同规模的团队里都观察到一个共同现象喊重构的声音很大真正落地的时间点却一拖再拖。原因不是懒而是这类老项目的重构收益和风险严重不对称。一个跑了五六年的后台系统页面可能有上百个业务规则藏在各种$scope.$watch和自定义指令里没有单元测试需求文档早就过时。这种系统做整体迁移风险不在于新技术学不会而在于行为等价性无法验证。老系统里那些没人记得为什么这么写的边界逻辑迁移时一旦漏掉一个就是线上事故。反过来看保留它的成本其实可控代码不动就不出问题浏览器不瞎升级就不出问题依赖不瞎升级就不出问题。真正需要投入的只是把获取代码和打补丁这两条路径修好让它变成一个可自持的系统。我的经验是这套自持机制搭起来大概需要一到两周的人力之后每年维护成本很低这笔账比全面重构划算得多。所以下面的内容都建立在这个判断上短期不迁移先把它变成一个自己能管住的资产。2. 框架代码从哪里获取四条路径与优先级代码获取这件事看起来是一句话的事实际上不同路径的可靠性、完整性、可审计性差别很大。我按推荐优先级从高到低排了四条实际项目中建议至少同时保住前两条。2.1 路径一包管理器中的历史版本首选公共包管理仓库里的历史版本是永久可下载的这是最稳、最标准的一条路。框架在这个生态里的包名就是angular和后来那个同名不同姓的新框架不是一回事这一点新人特别容易搞混我见过有人装错了包排查半天。几个常用操作直接抄# 查这个包有哪些历史版本 npm view angular versions --json # 确认最新也是最终版本号 npm view angular dist-tags # 只下载压缩包不安装用于归档 npm pack angular1.8.3 # 精确安装到项目锁死版本 npm install angular1.8.3 --save-exactnpm pack拿到的.tgz文件建议直接提交到内部制品库或者放进仓库的vendor/目录。这个包体很小几百 KB 级别存一份成本可以忽略但关键时刻能救命。包解开之后里面的文件结构值得记一下因为后面打补丁、做按需加载都要用到文件用途是否常用angular.js未压缩主文件含调试信息开发环境angular.min.js压缩版主文件生产环境angular.min.js.mapSource Map线上排错angular-route.js路由模块单页应用必备angular-animate.js动画模块按需angular-resource.jsREST 资源封装按需angular-sanitize.jsHTML 净化有富文本就要angular-messages.js表单校验消息表单多的话要angular-cookies.jsCookie 读写封装按需angular-mocks.js测试替身只在测试环境注意angular-mocks.js千万别混进生产构建它体积不小而且会往全局挂一些调试用的东西。我在一个项目里见过它被打进主包白白多了几十 KB。2.2 路径二代码托管平台的标签与发布归档如果包管理器哪天出问题虽然概率很低第二道保险是代码托管平台上的版本标签和发布归档。这类平台上的仓库通常保留了完整的历史标签对应的源码压缩包和发布附件也是永久可取的。我建议的做法不是等出事再去下而是现在就把完整归档做下来内容包括三样源码压缩包、构建产物就是上一节那些 js 文件、以及文档站的静态快照。文档站一定要存因为这个框架的文档是纯前端渲染的单页应用官方站点以后万一关掉你连$compile有哪些参数都查不到只能去翻源码。归档目录我通常这样组织vendor/angularjs-1.8.3/ ├── dist/ # 构建产物直接用 ├── src/ # 源码打补丁时用 ├── docs/ # 文档静态快照 ├── types/ # 类型定义如果项目用 TS ├── package.tgz # 原始包文件 ├── BUILD-ENV.md # 构建环境说明见下一节 └── SHA256SUMS # 所有文件的校验和最后那个校验和文件不是形式主义。我有一次在传输过程中碰到过一个文件被截断min.js少了几百字节页面加载时报的错完全指向别的地方查了很久。后来养成习惯所有归档文件都生成一遍 sha256加载前或构建时校验一次几秒钟的事能省掉一整天的排查。2.3 路径三公共 CDN 与本地化落地很多老项目是在index.html里直接写script src...引 CDN 的。这种做法在生产环境上其实一直有隐患一旦 CDN 侧调整了文件路径策略你的页面就直接白屏而且这种故障往往没有任何预兆。我的立场很明确可以在开发和演示环境用 CDN 图省事生产环境必须本地化。理由有三个——可用性不依赖第三方、构建过程可复现、版本一定对得上。如果一时改不完至少要做两件事。第一给script标签加上子资源完整性校验属性把文件的哈希值写上这样即使 CDN 上的文件被替换浏览器也会拒绝加载而不是悄悄执行第二在页面上加一个加载失败的回退逻辑检测全局对象是否存在不存在就换成本地副本。script srchttps://公共CDN地址/angular.min.js integritysha384-此处填写真实哈希值 crossoriginanonymous/script script if (typeof window.angular undefined) { document.write(script src/vendor/angular.min.js\/script); } /script这段回退写法有点土但在老项目里特别管用改造成本几乎为零。哈希值怎么来对你本地归档的那份文件跑一遍openssl dgst -sha384 -binary angular.min.js | openssl base64 -A把结果拼上sha384-前缀就是。2.4 路径四内部制品库与构建环境镜像前面三条都是拿到文件这一条是让它一直能拿到。团队规模稍微大一点就应该在内部制品库私有仓库代理里给这个包建一个永久保留的策略禁止任何自动清理规则把它扫掉。这件事非常容易被忽略。默认的制品库策略通常是保留最近若干版本或者90 天未访问就清理而一个稳定的老项目可能半年都不装一次依赖正好落在清理窗口里。等到某天 CI 重建依赖没了你才发现。同样要固化的还有构建环境本身。这是一个特别隐蔽的坑这个框架当年的源码构建依赖的是很老的工具链在那个年代的 Node 版本上跑得好好的换到现在的新版本 Node 上光是装依赖就会因为各种老插件的编译问题失败一片。等你真的需要从源码重新构建一个补丁版本时会发现根本构建不出来。解决办法是把构建环境做成容器镜像里面固定 Node 版本、固定包管理器版本、固定全局工具版本然后把这个镜像推到内部镜像仓库长期保存。镜像里顺便把node_modules也打进去彻底绕开安装环节。这个镜像平时不用一年可能就用两三次但每次都能把你从补丁写完了却打不出包的绝境里捞出来。3. 依赖冻结把能用变成一直能用代码拿到手只是第一步。真正让老项目稳定的是一套把版本、文件、环境全部钉死的冻结策略。这一节讲具体怎么做都是可以直接照着改的。3.1 精确版本与锁文件一个都不能少package.json里的版本号前面那个^符号在老项目里是纯风险源。^1.8.0允许装 1.8.x 里的任意版本理论上无害但一旦你的私有源上某个版本被替换过、或者某个补丁版本的构建产物有细微差异问题就会以最难查的方式出现。正确写法是去掉所有范围符号全部精确到补丁号{ dependencies: { angular: 1.8.3, angular-route: 1.8.3, angular-sanitize: 1.8.3 } }同时把锁文件提交进仓库并且在 CI 里用严格按锁文件安装的模式不要让流水线自己去解析版本范围。锁文件里记录了每个包的完整性哈希这一层校验比版本号可靠得多。顺带说一件常被忽略的事同一个项目里所有angular-*模块的版本必须完全一致。主包是 1.8.3路由模块是 1.7.9这种混搭在某些场景下不报错但会在一些边缘行为上产生难以复现的差异比如路由解析的时序、动画的触发时机。我现在的做法是在构建脚本里加一个检查不一致就直接让构建失败。3.2 依赖资产的完整清单光冻结框架本身不够。老项目的依赖树往往很深真正让构建挂掉的经常是外围的东西。我一般会整理一张遗产依赖清单格式如下放进仓库根目录每次动构建相关的东西都对着看一遍。资产类型具体内容冻结方式检查频率框架本体主包及各功能模块精确版本 归档 tgz每次构建类型定义对应的 TypeScript 声明包精确版本 归档每次构建构建工具打包器、转译器、压缩工具镜像固化每季度测试运行器测试框架及其适配层镜像固化每季度UI 组件库依赖框架的第三方组件精确版本每次构建浏览器目标需要支持的浏览器及版本写进文档每半年这张表的价值在于它把隐性依赖变成了显性资产。团队里任何人接手看这张表就知道哪些东西不能随便升级以及为什么。3.3 离线可用性的验证方法冻结做完了不代表真的能离线跑。你验证过一次才有底气。验证方法很简单但必须定期做先把网络出口断掉或者在容器里禁用外网然后从零开始走一遍完整的构建流程包括装依赖、跑测试、出构建产物。整个过程如果不需要访问任何外部地址说明冻结到位了。我第一次做这个验证时发现有三处隐性外网依赖一个构建插件会去拉浏览器列表数据、一个压缩工具会下载字典文件、还有一个测试工具会去取远程配置。这三处平时完全无感一旦内网环境变了就会集体失效。后来全部改成内网镜像或者本地固定文件。建议把断网构建做成 CI 里的一条独立流水线每周跑一次。它不验证业务逻辑只验证可复现性挂了就说明资产归档出了缺口。4. 扩展支持需求怎么处理先分类再动手到这一节才是真正麻烦的部分。官方不再接需求了但业务还在跑总会冒出必须改的东西。我的经验是先别急着写代码先给需求分类因为不同类型的需求处理方式、风险等级、审批流程完全不一样。混着处理一定会出事。4.1 三类需求与对应的处理策略我一般把扩展需求分成三类每类的处理原则如下表类型典型场景风险等级推荐做法缺陷修复某个指令在特定条件下不工作中运行时补丁优先改源码兜底兼容适配新浏览器报错、新 CSP 策略被拦中高配置层解决避免改内核功能增强业务要求新增交互能力低在业务层实现不碰框架关键判断标准只有一条能不能在业务代码里解决。能就绝对不要动框架。这是我在吃过亏之后定下的铁律。举个例子。有个项目需要支持更严格的内容安全策略页面上不允许内联脚本执行。这个框架的表达式编译机制依赖动态构造函数在严格策略下会直接被浏览器拦住报的错是拒绝执行内联脚本这类看起来很莫名其妙的信息。第一反应是去改框架源码把表达式编译改成别的实现——这个工作量极大而且几乎必然会引入新问题。正确的做法在配置层框架本身提供了一个兼容严格策略的模式在启动前告诉它不要用内联方式改从页面元素的属性里读配置数据。改完之后问题解决一行代码都没碰框架内部。这就是典型的兼容适配在配置层解决。4.2 分叉管理怎么改才不失控确定必须改框架之后第一件事是建立一条清晰的维护分支而不是直接在压缩文件上手改。后者听起来荒唐但我真的见过。我的做法是在内部代码托管平台上做一份完整分叉分支结构设计成这样upstream分支只保存原始版本一个字符都不改作为对照基线internal/main分支所有补丁都打在这里每个补丁单独一个提交提交信息里必须写清楚问题现象、影响范围、修改点、验证方式每次发布打标签格式建议internal-1.8.3-01这样末尾数字是补丁序号这套规矩看起来有点重但它的核心价值是可回滚和可审计。半年后有人问这个文件为什么和原版不一样你打开提交记录一目了然哪天某个补丁引发新问题单独回退那一个提交就行不用整体重来。补丁文件本身也要控制粒度。我的原则是最多改一处逻辑改完立刻跑一遍完整回归。绝不搞既然都改了不如顺手把这块也优化一下这种顺手改动是老项目最大的杀手。4.3 打补丁的三种姿势与选择依据具体怎么把补丁生效有三种方式按侵入程度从低到高排第一种运行时装饰。框架提供了服务装饰机制可以在不修改源码的前提下替换某个服务的行为。这是最安全的方式因为它只影响你声明的那个点而且完全在业务代码里构建流程一行都不用改。// 用装饰器包住原有服务注入额外行为 app.config([$provide, function ($provide) { $provide.decorator($log, [$delegate, function ($delegate) { var originalWarn $delegate.warn; $delegate.warn function () { var args Array.prototype.slice.call(arguments); args[0] [内部] args[0]; originalWarn.apply($delegate, args); }; return $delegate; }]); }]);这种写法在修日志、加埋点、加超时控制这类需求上特别合适。判断标准如果你能在业务代码里写出这段逻辑就不要改框架文件。第二种构建期替换。把框架的某个文件替换成自己构建的版本或者用打包工具的别名机制指向自己的实现。适合那种必须改内核、但改动点很集中的情况。这种方式的坑在于构建产物的一致性。源码改了要重新构建而这个框架的构建工具链非常老前面说过必须用固定镜像。我第一次自己构建时输出的压缩文件和官方文件在字节层面差异很大压缩工具的版本不同导致 Source Map 对不上线上报错的堆栈全是乱的。后来做法是自己构建的版本统一不做压缩或者压缩后重新生成 Source Map别拿官方 Map 硬套。第三种直接改压缩文件。明确说除了极端的应急情况比如线上正在被攻击需要立刻止血不要用这种方式。改完之后没有任何可追溯性下次换人接手时完全看不懂。5. 自建安全响应没人发补丁了怎么办这一节可能是全文最不讨喜但最重要的部分。官方停止发版之后安全防护的责任完全转移到团队自己身上。指望我们内网系统没人盯上是不现实的攻击行为很多时候是自动化的、无差别的。5.1 建立一张自己的风险地图第一步不是写代码而是把风险点列清楚。我通常从四个维度做盘点输入到输出的路径。任何把用户输入直接塞进 HTML 的地方都是高危。这个框架提供了 HTML 净化模块但这个模块本身也需要正确使用——有些团队为了图方便直接绕过净化用最原始的绑定方式插入内容。我在一次代码审计中看到过一个页面路由参数直接拼进了 HTML任何能构造链接的人都能注入脚本。数据合并与对象扩展的路径。老版本框架里有一些深合并工具函数在处理来自外部的 JSON 时可能存在原型链污染的风险。审计时重点看有没有把外部数据直接丢进深合并函数。第三方依赖。这一条最容易被忽略。老项目往往同时引了旧版本的通用库框架本身没问题但那些库有已知问题。做法是把所有前端依赖列出来对照公开的漏洞库逐条核查该升级的升级升不动的就在外层加防护。拼接与求值。框架的表达式求值机制本质上是动态执行如果某个页面的表达式字符串里混入了用户可控内容风险等级非常高。审计时搜一遍有没有用变量拼接表达式的写法。5.2 补丁的验证标准自己打的补丁验证标准要比官方发版还严因为没有第二道把关。我定的最低要求是三条第一有可复现的问题样例。修之前先写一个能稳定复现的最小页面修完之后这个页面表现正确。没有复现样例的补丁不合并。第二有回归清单。改动点所在模块的所有已知用例跑一遍加上受影响模块的冒烟用例。老项目通常测试覆盖很低这时候就靠人工整理的冒烟清单哪怕只有二十条也比凭感觉强。第三有回滚方案。补丁发布前想清楚如果上线后出问题多长时间能退回原版退回后原来的问题会不会重新暴露、能不能接受这两个问题答不上来就别发。6. 常见问题与排查技巧实录下面这些是我和周围同行实际踩过的坑按现象—原因—处理整理成速查表遇到问题先扫一遍能省不少时间。6.1 高频问题速查表现象大概率原因处理方式装依赖报 404包名明明没错私有源未代理该包或代理规则过期检查源配置改从归档 tgz 装页面白屏控制台无报错压缩文件被截断或版本不一致校验文件哈希比对版本号报模块未定义主文件引入顺序错误或模块文件缺失检查 script 顺序与模块清单页面卡死风扇狂转监听表达式里做了对象比较或函数调用改为监听基本类型函数结果缓存新浏览器下功能失效严格策略拦截了动态求值启用兼容模式调整策略配置内存持续增长监听未注销、定时器未清理在销毁钩子里统一注销测试跑不起来测试环境与运行环境版本不匹配用固定镜像跑测试构建耗时突然翻倍压缩工具在新 Node 上降级运行切回固定版本的构建环境表格之外还有几个不那么标准化的经验我觉得更值钱。6.2 三条踩出来的实操心得第一条不要相信没改过所以没问题。老项目最危险的不是改动而是长期没人碰。我有一次接手一个两年没动过的项目本地跑得好好的发到新环境的容器里就白屏。查下来是新基础镜像的默认字符集和之前不一样某个中文资源文件的解析出了偏差最终影响到框架的模块注册顺序。这个链条绕得离谱但根因就是环境变了代码没变。所以每次换环境都要跑一遍完整回归别假设它是稳定的。第二条所有补丁都要写为什么不要只写改了什么。补丁说明里如果只写修改了某函数返回值半年后没人知道能不能删。写成因为某浏览器在该场景下不触发某事件导致列表不刷新故在此处补一次手动刷新——这样的说明即使代码看不懂也知道意图和边界后面的人敢动。第三条把降级当成一等公民。我现在的习惯是任何对框架层的改动都同时准备一个开关可以一键关掉回到原始行为。开关放在配置里不需要重新构建。这个习惯救过我一次——某个补丁在小流量环境验证完全正常全量上线后在一个特定操作序列下引发了死循环因为开关的存在五分钟就恢复了不用紧急回滚版本。开关的成本很低收益极高。6.3 迁移这件事什么时候该认真考虑最后聊一句迁移的判断标准。我不建议用框架老了这种感性理由推动迁移也不建议用还能跑就别动这种鸵鸟心态拖下去。比较务实的信号有这么几个安全审计出现无法修复的高危项、核心业务要在移动端做重交互、团队里已经没人愿意接手这块代码、以及招聘时这块成为明显阻力。在信号出现之前把前面说的资产归档、依赖冻结、补丁流程三件事做扎实这套系统完全能再稳稳跑好几年。我自己维护过的最久的一个项目从停止支持到现在又跑了三年多中间只打过两次补丁一次是浏览器策略适配一次是日志埋点需求都是运行时装饰解决的框架文件一个字节没动。真正让人安心的从来不是官方还在维护而是我知道出了问题该去哪儿找、该怎么改、改完怎么退回来。这套东西建起来之后框架停不停支持其实就没那么重要了。