1. 先别急着执行升级命令Angular 的版本升级大概是前端项目里最容易被拖延、又最躲不过去的事。手头项目如果还停在 Angular 8、9每次看到新版本发布心里都会有个声音算了能跑就行。但技术债这东西藏不住等到新同事接手、依赖包连续报高危漏洞、或者想上一个新功能却发现跟老版本框架完全没法兼容的时候升级就被迫提上日程。这篇内容是“Angular 持续提升”系列的第四篇我把这些年做版本升级踩过的坑和整理出来的步骤一次说清楚。很多人在升级前最喜欢做的事就是直接在项目根目录敲ng update angular/corelatest接着期待一切自动完成。实际情况往往是命令在依赖解析阶段就报错或者一半包升上去了另一半还停在远古版本整个项目直接进入“既不能构建也不能回退”的尴尬状态。所以我先把结论放在最前面升级 Angular 和换轮胎有点像你不可能在车速 120 的时候直接把四个轮子全换掉必须先降速、分段换、每换完一个都要检查一次再上高速。这个系列面向的读者主要是刚接手老 Angular 项目的新人、被升级任务逼到头秃的运维或全栈以及想跟技术主力同步升级路线的前端负责人。后面的内容不会只讲“执行哪条命令”而是把命令背后的版本关系、依赖链路和常见失败原因都展开讲。1.1 为什么说盲目执行 ng update 是大忌ng update是 Angular CLI 在 v62018 年引入的原生升级工具设计思路很好读取当前 package.json 中的版本信息再把官方为每个包写的 migration 脚本串起来执行自动帮你改一些配置文件和代码。但这个工具能跑通的前提是你的项目依赖本身是干净的、版本关系是在官方支持范围内的。假如从 Angular 7 直接跳到 Angular 17中间隔着多个大版本。Angular 包之间有严格的 peer dependency 约束而且每个大版本会同步调整对 Node、TypeScript、RxJS、Zone.js 的版本要求。直接跳级会让这些依赖关系瞬间乱掉。比如 Angular 8 时代项目里用的是 RxJS 6.5、TypeScript 3.4Angular 17 需要 TypeScript 5.2 以上。如果你只升级了angular/core而没有同步处理 TypeScriptCLI 在编译阶段就会抛出各种类型不兼容的错误。这些错误的表现形式相当迷惑有时候报错位置根本不在你改过的代码里而在 node_modules 深处的类型声明文件中。所以第一条原则是不要一次跨越超过一个大版本除非你的项目极其简单、没有第三方库、没有复杂构建配置。即使官方支持跨多个版本命令行里确实可以指定任意 target version也强烈建议把目标拆成“13 → 15 → 17”这样的短期台阶每爬一层就构建一次、测试一遍让每次失败的范围都足够小。我见过不少团队试图一口气从 Angular 8 升到 16结果光迁移工具之间的冲突就够喝一壶最后不得不花整个迭代来收拾残局。1.2 升级前需要确认的三件事在敲任何升级命令之前我会先花半小时做三件事。第一件是明确当前版本和中间目标版本。查看 package.json 里的dependencies和devDependencies重点看angular/core、angular/cli、angular/material之类的版本号然后打开官方网站的升级指南工具 update.angular.io。这个工具可以选择起点和终点它会列出每一步该执行的命令、需要手工修改的 API 变更点以及可能影响到的第三方库清单。第二件事是检查 Node.js 和包管理器版本。这不是小事Angular 16 要求 Node 至少 16.13.0Angular 17 要求 Node 18.13.0 以上Angular 18 则要求 Node 18.19.0 以上。如果你本机还停在一个很老的 Node 12升级完 Angular 之后大概率连 npm install 都跑不起来。我一般用nvm管理 Node 版本升级前单独切到目标版本重新执行npm ci先把依赖树装干净再动手。第三件事是确认版本仓库干净并开一个专门的升级分支。所有未提交的改动先 stash 或 commit然后从主干分支拉一个类似chore/upgrade-angular-17的分支。原因很朴实升级过程中迁移脚本会修改不少文件如果没有独立分支升级到一半发现思路错了想回到升级前的状态会非常痛苦。建立一个干净可回滚的起点是后面所有操作的前提。等所有步骤跑完、验证通过再把这一个分支的改动合并回主干不要顺手把其他业务代码混进来。2. 升级路径怎么规划版本升级的路线规划比执行命令本身更能决定成败。不同起点的项目面临的技术债类型完全不同。AngularJS 1.x 的老项目、停留在 Angular 4/5 的项目、以及已经到 Angular 8/9 但被 View Engine 卡住的项目升级策略应该说三种不同的故事。很多技术团队把升级失败归咎于命令执行不到位但实际上错在起点判断。这一节我从三种最常见的起点出发分别给出路线建议。如果你所在的项目正好卡在某一个阶段可以直接跳到对应小节对照。2.1 AngularJS 1.x 到 Angular 2这其实是迁移不是升级如果项目还在 AngularJS比如 1.4.6 之类的版本先要明确一条边界Angular 2 不是 Angular 1.x 的延续而是一次框架层面的重写。它把 MVC 架构换成了组件化架构把双向绑定的$scope换成了组件输入输出模块系统也完全不同。所以从 AngularJS 到 Angular 2 不是一两条命令能完成的事官方给这条路径的定义就是“迁移”常见做法是采用angular/upgrade的 UpgradeModule在老应用中逐步呈上 Angular 组件一块一块迁移。这里我不展开讲完整迁移方案只说一个对版本升级决策很关键的结论如果你还在 AngularJS优先级不是急着升到某一个新版本而是先把业务模块按组件边界拆出来写清楚数据流向再考虑用 UpgradeModule 做渐进式迁移。真正开始迁移后通常会直接落在 Angular 13 以上因为 Angular 13 以后的独立组件和现代 CLI 能显著降低混迁复杂度。换句话说AngularJS 老项目的版本升级本质上是一个以周甚至月为单位重构项目的过程别再指望ng update帮你搞定。2.2 Angular 4/5 时代的老项目先补测试再走主线另外一个常见起点是 Angular 4 或 Angular 5这类项目大多诞生在 2017 年前后用的是 HttpClient、Router 这些现代 API但项目里通常没有好好配测试升级的时候心理负担特别大。如果你手头是这样的项目我建议的路径是先围绕核心业务模块补上冒烟测试和关键链路的单元测试然后再执行从 5 → 6 → 8 → 9 → 11 → 13 这样的渐进升级。为什么拍成这个顺序因为 Angular 5 到 6 之间引入了ng update机制包版本开始统一很多老手习惯的懒加载语法也不同了6 → 8 主要是 CLI 和依赖结构的变化做完这一步项目结构会变得非常接近现代 Angular8 → 9 则是关键的 Ivy 编译器切换这个步骤的迁移工具同时会重写大量代码适合在测试覆盖已经补上的情况下推进。每完成一个目标版本至少跑一遍生产构建并把构建产物用日记方式记录下来方便后面排查到底是哪一版本引入的尺寸或性能变化。Angular 4/5 项目还有一个容易被低估的风险老项目的angular.json或.angular-cli.json配置与新版差异巨大比如项目里可能还在用environment.ts的旧路径、assets配置写法也不一样。这些不是靠一条命令能吸收的差异手工调整时最好对照官方升级指南逐条核对。2.3 从 View Engine 跨到 IvyAngular 8/9 的转折点View Engine 是 Angular 2 到 8 默认使用的编译架构Ivy 是从 Angular 9 开始成为默认的下一代编译器。对升级来说这个转折点的意义在于Ivy 在编译原理上更接近现代 JavaScript 生态大量编译期逻辑从“运行时”提前到“构建期”很多老技巧因此失效。比如以前有人靠ViewChild加static: true/false来控制查询时机在 Ivy 中static的语义约束更严格用enableProdMode()手动控制生产环境的场景也有变化。从 8 升级到 9 时官方 migration 会自动处理很多模板改动但有些是处理不了的。我印象最深的是自定义装饰器、动态创建组件的ComponentFactoryResolver以及一些老的 AST 工具在 Ivy 下编译产物不同会导致单元测试里的TestBed初始化报错。所以这一阶段升级之后不要只看页面能不能打开一定要把单元测试跑到稳定通过。Ivy 还有一个附带好处打出来的包更小、构建更快这是升级后最容易跟老板交代的收益点。现在回头看Angular 9 到 13 之间的升级本质上都是在 Ivy 已经铺好的地基上做增量调整。如果你已经顺利越过 8 → 9 这条线后面的 10、11、12、13 四步虽然版本号多但整体迁移成本是逐年下降的心态可以放轻松一点。3. 一条可复制的升级实操路径路线规划好之后真正上手操作时最需要的是一个确定性的流程。我现在每做一个 Angular 升级都会走下面四步先让官方升级指南生成任务清单再用ng update做自动化迁移接着处理第三方库和编译错误最后把测试和构建全部跑绿。整个流程走完通常一两天时间就能完成一个大版本升级前提是你别在中间频繁换思路。下面每一步我都会给出命令和理由。命令不是让你直接复制跑完就结束重点在于理解它到底帮你做了什么。3.1 借助官方升级指南锁定每一步命令update.angular.io 是官方维护的升级指引工具我每次升级都先拿它生成一张“任务清单”。它的界面很简单选择当前版本和目标版本呈现每一步需要安装的依赖、需要执行的命令列表。它生成的内容和 npm 官方文档、升级日记是同步的比自己去翻 changelog 靠谱得多。举个例子假设项目当前是 Angular 13目标版本是 Angular 17。官方推荐的执行顺序通常是先升级angular/cli17再升级angular/core17因为 Angular CLI 负责项目脚手架、构建器和 server 相关配置先更新它能让后续的依赖解析处于新工具链之下。然后要配套更新angular/material或angular/cdk如果项目在用再手工处理 TypeScript、RxJS 版本。这些步骤在页面里都会列清楚。既然升级指南工具已经把步骤都列清楚了为什么还会有这么多人翻车我观察到的原因是很多人把它当成一个“命令生成器”扫描一眼就关掉并没有真正理解每一步的依赖关系。我推荐的做法是把生成的每一行命令当成一个检查点执行完一行就停下来看一眼当前 package.json 里发生了什么变化确认没问题再走下一步。宁可慢一点也不要一把梭。3.2 ng update 完整执行流程与参数细节具体操作的时候我的标准命令是# 1. 确认当前分支干净、当前版本可构建 git status ng build # 2. 更新 Angular CLI ng update angular/cli17 --migrate-onlyfalse # 3. 更新核心包注意这里不是 latest ng update angular/core17 angular/common17 angular/compiler17 angular/forms17 angular/router17 angular/platform-browser17 --allow-dirty这里有个很多人会踩的误区直接npm install angular/core17只会更新 package.json 里记录的版本并不会执行 Angular 官方为版本升级准备的 migration 脚本。而ng update这个命令的核心价值恰恰是执行那些脚本——像inject()替代Testbed.get()、模板里ngTemplateOutlet语法的自动转换、路由配置的类型调整等都是靠这些脚本批量处理的。所以升级的时候必须以ng update为中心而不是以 npm install 为中心。--migrate-onlyfalse这个参数的值默认其实就是 false但写上它更明确告诉 CLI 既要更新依赖也要跑迁移脚本。如果只更新依赖不跑迁移很多代码层面的变化就得全靠手工工作量成倍增加。--allow-dirty则允许当前工作区不是完全干净时也能执行命令我一般只在升级分支上才用它否则建议保持默认。3.3 迁移脚本跑完之后的代码调整migration 脚本能处理的通常是机械性的改动但真正耗时间的是代码里那些编译器无法自动识别、需要你根据新版本语义手工调整的部分。我从 Angular 14 一路升到 17 的真实过程中遇到最多的手工改动来自这几个地方HttpClientModule这类模块导入在 standalone 化的项目里会被替换成provideHttpClient(...)如果项目还保留大量 NgModule则需要逐个决定保留还是迁移。路由定义里旧的loadChildren: path/to/module#LazyModule字符串写法现在更推荐使用函数式导入例如loadChildren: () import(./lazy/lazy.module).then(m m.LazyModule)。ngIf/ngFor结构指令Angular 17 的新控制流语法if/for已经趋向默认。虽然老语法还能用但新写的模板开始向新语法迁移。这些改造建议在官方迁移指南里都会有对应章节。我不建议一次把所有文件全部重写那样容易引入大量无关 diff评审成本很高。比较稳妥的做法是每个模块升级完成后只对该模块里被 migration 工具标记或编译器报错的模板进行最小改动保持风格统一即可。升级的目标不是把代码“重写到一个完美状态”而是让项目能够在目标版本上稳定、可维护地运行。4. 高频报错与排查方法升级过程中报错并不可怕可怕的是不知道去哪里找原因。Angular 的报错信息整体上做得还算可读但经过 CLI、TypeScript、Webpack 多层包装之后很多真实原因会被淹没在一大堆堆栈里。这一节我把我自己线上遇到过、以及在群里看到频次最高的几类报错整理出来每条都给出排查思路。按大类分报错集中在依赖解析、编译检查、运行时注入三个层面。你遇到问题时先判断它属于哪一层排查范围至少能缩小一半。4.1 依赖版本冲突npm 报错怎么读升级中最常见的报错就是 npm 的 ERESOLVE 或 peer dep 冲突。很多朋友看到一屏红字就慌其实核心逻辑很简单npm 要求全项目依赖树中同一个包不能存在多个相互冲突的主版本而 Angular 的各个包之间是强耦合的。你项目里如果同时装了老版本的angular/animations和新版本的angular/core报错几乎是必然的。排查时先看报错信息里提到的是哪个包的哪个版本约束。比如提示angular/compiler17.0.0需要angular/core17.0.0但当前项目里angular/core16.0.0就说明 core 没升到位。处理顺序是先把所有angular/*包统一到同一版本再把配套的typescript、rxjs、zone.js调到官方兼容版本最后才考虑第三方库的版本。如果第三方库确实不支持目标 Angular 版本要做的不是强行--force安装而是先在项目里确认这个库是否还在维护必要时找到替代品。这里分享一个我自己的排查小技巧把报错信息里的包名、版本号、期望版本号单独复制出来去网上搜你当前 Angular 大版本对应的 dependency 对照表。Angular 官方的 compatibility 文档会列清楚当前版本与 TypeScript、RxJS、Zone.js 的对应关系不要凭感觉猜。报错类型典型信息排查方向npm ERESOLVECould not resolve dependency检查是否有包引用多个版本统一angular/*版本peer deprequires a peer of angular/core^17将 core 升到对应大版本或找替代库TypeScript 版本The Angular Compiler requires TypeScript 5.2调整 TS 到官方兼容版本迁移脚本失败Migration failed: ...查看具体哪一步失败通常需要手工解决后重试4.2 编译错误Ivy 和严格模板检查带来的变化Ivy 编译器对类型检查和模板类型检查更严格升级后经常出现以前能编译、现在编译不过的情况。这不一定是你代码写错了而是编译器不再容忍某些隐式类型。最典型的是模板里访问不存在的属性或者组件输入类型不匹配。Angular 17 默认开启了更严格的模板类型检查strictTemplates以前 View Engine 不检查的地方Ivy 都会报。遇到这类错误先分成两类一类是真实类型问题比如某个BehaviorSubject可能需要显式声明Subjectstring类型老老实实修正另一类是模板写法太“野”比如直接在模板里用any抓数据我会建议把数据访问逻辑挪到组件类里用一个 getter 或方法返回明确类型。这样做升级代码更干净后续维护也省心。另一个编译期常见问题是模块导入遗漏。升级后原来某个BrowserModule或CommonModule的导入路径失效或者某个不需要的模块从第三方包的类型声明里消失了都会引发编译错误。这类错误相对好处理编译器一般会明确指出缺少哪个符号根据提示补上导入即可。但要注意Angular 的 standalone 趋势下依赖注入方式的模块代码正在减少尤其在 Angular 17 以后新建组件默认不再带 NgModule如果你还在这个阶段大量使用 NgModule编译错误会多不少。4.3 运行时问题明明编译过了页面还是白屏编译通过不代表升级成功。我遇到过几次典型的运行时问题。第一个是惰性加载路由在某个旧模块上一直白屏后来发现是loadChildren在版本演进后返回类型变了老写法没有按新规范导出Routes数组或NgModule。第二个是依赖注入问题报NullInjectorError: No provider for HttpClient原因通常是某个库以前自带 provider新版本中 provider 需要由业务方通过provideHttpClient显式提供。第三个是 zone.js 的__zone_symbol__相关报错这类往往源于第三方库或老构建配置里自定义了 zone 行为。排查运行时问题我的建议是先打开浏览器控制台看第一个红色错误处理完第一个再刷新不要同时改多个猜测点。因为运行时错误往往有连锁效应最简单的上前置问题解决了后面一堆“问题”可能自然消失。你如果发现某个错误只在生产构建出现、本地开发时没有还要顺手确认一下是不是enableProdMode相关逻辑在新版里的行为变化或者压缩混淆把某个动态类名改掉了。5. 版本升级之外核心注意事项与长期维护心得版本升级这件事表面上是技术操作实际上真正决定成败的是工作习惯和工程规范。这一节我整理了几个在多个项目里反复验证过的注意事项它们不属于某一次升级的具体步骤但缺了它们升级很容易变成一场漫长的消耗战。5.1 建立升级的发布与回滚机制升级不是把代码合到主干就结束了。这里收敛一个很重要的思路版本升级应该被视为一次带有发布计划的变更而不是单纯的开发任务。我个人会把升级拆成“build 通过 → 测试通过 → 回归 → 灰度发布 → 全量发布”五步。尤其在灰度发布阶段会保留一个可以直接切换回去的旧版本发布产物。为什么强调回滚机制因为 Angular 大版本升级后有些问题在开发环境、测试环境根本测不出来只有真实用户流量进来了才会暴露。比如某个老浏览器上的兼容性问题、或某个在低配机器上才出现的性能退化。如果发布体系里没有快速回滚的开关一旦线上出了严重问题你要么现场改代码要么全体盯着日志猜原因压力会非常大。有一个灰度环境加一键回滚兜底整个升级任务的心理负担会小很多。5.2 把版本升级变成常态化节奏升级成本最低的做法其实是保持每个大版本发布后的一段时间内就完成升级。Angular 官方每年发布两个大版本半年一升的节奏听起来有点频繁但相对一次跨三四个大版本、累计了大量 API 变更再动手反而是最省人力成本的。你可以把升级当成一个固定的技术任务排在里程碑里而不是等某个紧急需求把它逼出来。我在团队里会建议维护一个简单的升级日志每次升级记录从哪个版本到哪个版本、跑了哪几条迁移命令、手工改了几个文件、花了多久、遇到了哪些问题。后续做下一次升级时这份日志就是最实用的参考资料。版本升级的步骤可以被工具自动化但每个项目的手工改动点各有不同经验积累是工具替代不了的。5.3 一定要守住的核心注意事项清单升级前开独立分支保留干净可回滚起点。不要跨多个大版本一次性升级按 8 → 9 → 11 → 13 → 15 → 17 这样的台阶走。用ng update而不是npm install升级 Angular 包确保迁移脚本执行。升级完一个阶段后立刻跑构建和测试不要全部升完才验证。遇到第三方库不兼容优先更新库或替换不要用--force绕开依赖检查。留意 standalone 和新的控制流语法趋势新代码逐步采用新写法。升级后对懒加载、注入、模板检查等高风险区域专门回归一遍。个人习惯上每次升级我都会准备一份“前后对比”笔记记录构建包体积、首屏耗时、核心页面性能在升级前后的变化。升级对业务的价值不只在“框架变新”很多时候能直接带来构建速度和产物体积的优化这些数据拿到会上也是实打实的技术成果。Angular 版本升级是一个可以不断打磨的工程流程。多跑几次之后你会形成自己的节奏和套路工具和命令都会越来越熟练。最重要的是别把升级当成一次性的苦差事而是把它变成项目持续演进过程里的一个常规动作。配置好测试、依赖清理和发布机制后面每一轮版本升级都会比上一轮更轻松。