
项目排期终于给到了 Angular V21 升级说实话接到这个任务的第一反应不是兴奋是心虚。这个系列写到第十八期眼看着 Angular 从模块化一路走向信号化每次升级都像一次外科手术V21 这刀要切在哪里如果心里没数光靠一路ng update回车大概率要跪。这篇我不聊 PPT 上的新特性只讲从 V20 往 V21 搬家的实操思路、踩过的坑、以及出现问题时的排查路径。适合手头正在做迁移预算、又不想把生产环境搞得鸡飞狗跳的开发者。1. 为什么值得升级V21 的关键变化1.1 升级不是追新是解决旧债很多团队对升级的第一反应是“能跑就不动”。但说句实在话Angular 的版本迭代节奏这些年已经非常稳定每个大版本不是给你加几个 API 完事而是在做技术债务的清理。V21 这个版本最明显的信号是旧的响应式体系逐渐退场信号Signal成为状态管理的主干模板控制流语法全面接管*ngIf、*ngFor构建管线进一步精简开发服务器和构建缓存都有实质性的性能提升。我这次升级的项目是公司内部的某跨平台后台系统模块数量接近两百模板中旧结构指令的用量非常大直接依赖的第三方库有二十多个。之所以选在这个时间点升级是因为项目里积压了两个久拖未决的性能问题一个是列表页大数据量渲染卡顿另一个是状态变更时变更检测的无效刷新太多。V21 的信号与按需派发机制加上新的控制流语法对可变动检查的友好度正好能解决这两类问题。升级不是给自己找事是在用短期切换成本换后续两年的维护舒适度。1.2 破坏性变更清单先行升级前必须把变更清单拉出来不然后面每一步都是雷。V21 相对 V20 的破坏性变更大致可以归成四类第一类是废弃 API 的移除尤其是angular/common里一些长期标废弃的管道和指令会在编译期直接报错第二类是控制流语法的默认化旧模板如果还没迁移会收到大量弃用告警某些严格模式下直接失败第三类是 TypeScript 版本基线提高类型定义更严格之前能蒙混过关的any写法开始报错第四类是运行环境要求变化Node.js 版本低于某个阈值的会直接无法启动构建。我的建议是别凭记忆猜直接在官方 changelog 里拉出 V20 到 V21 的 breaking changes 清单然后逐条对照自己的项目代码做标记。这一步很枯燥但能省掉后面一半的定位时间。实际操作时可以用一个简单的表格把每条变更对应到项目里的模块标记影响面是“核心代码”还是“测试代码”。目的是让团队心里有数哪些改动是要单独排期的哪些可以顺手改完。2. 升级前的准备与依赖体检2.1 版本兼容性矩阵与依赖盘点在敲任何命令之前先把环境版本理清楚。Angular 的依赖树很敏感angular/core、angular/common、angular/compiler这些核心包必须同版本哪怕差一个 patch生产构建也可能出稀奇古怪的报错。我通常会维护一份版本兼容性清单列在这个项目里。V21 升级的基础要求我用表格整理给大家参考这是基于本次实操环境的经验值具体以官方文档为准项目最低要求推荐配置Node.js20.x 以上22.x LTSTypeScript5.6 以上5.8 左右RxJS7.8 以上7.8.xZone.js0.14.x 或迁移到 zoneless按实际路由复杂度决定Angular CLI21.x最新 patch依赖盘点这一步很容易被忽视但它往往是升级失败的“头号种子”。我这次打开package.json逐个检查所有angular/*依赖确保没有用latest这种浮动版本因为浮动版本在升级瞬间可能把你带到不同的 minor 版本上锁定你的环境版本能保证升级可控。然后用npm ls angular/core检查是否有重复安装的情况如果出现多个版本嵌套先通过overrides或升级中间层依赖来收敛否则后续升级的报错信息会非常混乱。2.2 用官方迁移工具做预检Angular 官方提供的升级工具链是ng update但在执行之前我强烈建议先跑一次ng update angular/core --from 20 --to 21 --dry-run。这个命令不会真正改动文件只会输出一份迁移影响报告展示哪些包会被更新、哪些源文件会被改写、哪些命令会被执行。这是整场升级里性价比最高的一次“预演”。我这次跑完 dry-run 的输出让我心里踏实了不少迁移器会自动处理模板中旧控制流指令到新语法的转换但同时也提示有些自定义指令和管道需要手动检查。还有一个重要信息是V21 的迁移工具会把providedIn的写法统一规范化某些测试文件的注入方式也要跟着变。拿到这份报告后不要急着真正更新先在版本控制里建一个备份分支确认当前工作区是干净的再继续下一步。2.3 锁文件与分支策略升级前的重要性排序里锁文件一定要排在前列。使用npm的话就是package-lock.json使用yarn就是yarn.lock这些文件记录了当前依赖树的确切状态。我见过不少项目升级失败后想回滚结果发现锁文件已经跟着乱了回滚之后依赖树依旧残缺。我的做法是先把锁文件哈希记录下来或者直接在版本控制里打一个升级前 tag。然后新建一个专门的分支比如chore/upgrade-ng21所有升级改动都在这条分支上完成。好处是升级过程会被完整保留即使中途想放弃也可以干净地切回主分支。一旦真开始ng update锁文件会被自动重写这是正常现象不要慌只要你的原始分支在就随时有后悔药。3. 从 V20 到 V21 的分步实操3.1 分步执行 ng update 命令执行ng update不是一条命令一键到底正确的姿势是分步走。先更新全局 Angular CLI再更新项目里的 CLI 相关包然后依次更新核心包、工具链包、相关生态包。我在这次升级里按下面这个顺序执行每一步之间都停下来跑一次构建或者测试# 全局 CLI 更新如果有全局依赖 npm install -g angular/cli21 # 项目本地 CLI 更新 ng update angular/cli21 --from 20 --to 21 --migrate-only # 核心框架更新 ng update angular/core21 --from 20 --to 21 # 其他相关包 ng update angular/material21 angular/cdk21为什么要有这个顺序因为 Angular CLI 是升级过程的执行引擎引擎不先动后面的迁移器跑不起来。核心包更新的时候会提示是否运行代码迁移器这里建议选择y比如模板控制流迁移、独立组件迁移它们能自动处理大部分机械性的重复修改。我遇到过有人直接执行ng update angular/core21结果 CLI 还是旧的迁移器无法驱动报错说“需要先更新 angular/cli”。这种问题本质上就是顺序错了。升级讲的是依赖链CLI 依赖 compilercompiler 依赖 corecore 依赖 TypeScript所以每一步之后都要用npm ls和ng version确认版本状态没跑偏。3.2 处理破坏性变更的典型场景升级过程中最耗时间的就是破坏性变更。我这里举两个这次项目里处理得比较典型的场景可以给大家参考。第一个是模板控制流迁移。虽然迁移器能自动把*ngIf改为if把*ngFor改为for但还是有漏网之鱼。例如*ngFor里用了index变量迁移器可能把它变成普通变量但如果你在循环里嵌套了*ngIf这层关系可能需要人工调整。迁移后的代码大致长这样!-- 迁移前 -- div *ngForlet item of items; let i index; trackBy: trackByFn span *ngIfitem.visible{{ i }}: {{ item.name }}/span /div !-- 迁移后 -- for (item of items; track item.id) { if (item.visible) { span{{ $index }}: {{ item.name }}/span } }注意trackBy的处理。新语法里track直接接受表达式不用再写完整的函数名但如果你的trackBy函数里有关键逻辑需要迁移器帮它转换成track item.id之类的形式转换后一定要复查一遍因为如果你的业务需要按复合字段跟踪简单转换可能会丢掉逻辑。第二个是模块导入路径的变化。V21 对公共 API 做了收敛不少深层路径的导入被收进主入口。升级后构建报错说找不到angular/common/http/testing的某个导出这种一般是路径改了。我的排查方式是查官方 API 列表把旧的深层路径替换成新的主入口导入。这类改动虽然机械但涉及面广适合让迁移器配合搜索替换一起处理然后靠 TypeScript 编译错误一项项消灭。3.3 信号状态的渐进式接入升级到 V21 不等于要把所有状态管理都改成信号模式但如果你想让升级后的性能收益落到实处信号化是绕不开的。我这次的做法不是重写而是渐进式接入先挑一个高频更新、旧变更检测压力最大的列表页来试点把原来的BehaviorSubject状态替换为signal和computed。下面是一个简化的对比左侧是旧写法右侧是信号写法// 旧写法 private itemsSubject new BehaviorSubjectItem[]([]); readonly items$ this.itemsSubject.asObservable(); loadItems() { this.http.getItem[](/api/items) .subscribe(data this.itemsSubject.next(data)); }// 信号写法 private itemsSignal signalItem[]([]); readonly items itemsSignal.asReadonly(); loadItems() { this.http.getItem[](/api/items) .subscribe(data itemsSignal.set(data)); }模板中旧写法要配合async管道信号写法直接就可以在模板中读取。这里的关键点是如果组件里还有别的响应式依赖不要立刻把async管道删掉先让信号和旧响应式共存观察变更检测和渲染时间的变化确认稳定之后再逐步切换。我见过激进迁移的团队一夜之间把几十个组件改成信号结果下拉刷新、表单联动各种隐性 bug 一起爆发。信号化是长期工程升级当天能切一两个核心场景已经够让团队体验到收益了。4. 升级过程中的常见问题排查实录4.1 构建阶段的两类高频报错每次大版本升级构建报错都是最多的问题。第一类是 TypeScript 类型错误特别是一些第三方库的类型定义没有跟上 V21 的类型收紧报错信息往往显示在node_modules内部看似与你无关实际是你的代码引用了被移除的导出。解决方式不是去改第三方库而是在你的业务代码里添加类型收窄或者显式类型标注。第二类是编译器模板错误常见的是for循环里要求必须指定track表达式旧代码里如果之前没有trackBy迁移器可能会生成一个简单的track $index或track item。如果元素是基本类型比如字符串数组直接track $index就够了如果是对象数组建议直接用唯一标识字段否则列表变更时可能出现渲染异常。有时候这个错误不会立刻爆而是在某些操作后列表排序乱了后台检索数据时才发现。4.2 运行时白屏与路由问题升级后如果页面直接白屏优先检查浏览器控制台和网络面板。我这次遇到的白屏是因为懒加载路由的模块拆分路径变了V21 默认的 chunk 命名规则和输出目录有调整导致生产环境中旧的 chunk 文件名被缓存构建后部署到服务器上浏览器还在请求旧资源名。解决方式很简单服务器配置里关闭对*.js的强缓存或者部署时清一次 CDN 缓存。开发环境下如果白屏那多半是 Zone.js 被移除或替换导致的事件触发异常。V21 把 zoneless 模式进一步前置如果你在升级前已经启用了provideZonelessChangeDetection升级后要检查所有第三方库对 Zone 的隐性依赖。判断方法是在控制台搜索NgZone相关的告警如果第三方组件库还在内部调用NgZone.onMicrotaskEmpty就需要用兼容模式过渡或者升级该组件库的版本。4.3 第三方库版本冲突在升级第三方库时最容易出现的是 peer dependency 冲突。比如某个图表库声明支持angular/core ^20.0.0而你升级到了 21npm install会直接报错。这时候不要急着加--force或--legacy-peer-deps强闯先查这个库是否有适配 V21 的版本如果有直接更新库本身如果没有就要评估代码里对它的依赖有多深。我这次遇到一个老旧的工具库作者已经停止维护peer dependency 还锁在 Angular 20。最终方案是把库的代码用本地封装的方式抽进来移除对node_modules中该库的依赖然后用 V21 重新编译。这个过程比较费精力但比强闯依赖关系、在运行时产生隐蔽 bug 要安全得多。如果你的项目里有类似的长期不维护库建议在升级前就列一个“黑名单”优先处理。4.4 包体积与性能回退升级后如果发现首屏包体积不降反升先别慌。V21 在默认配置里增加了更细粒度的构建缓存第一次构建会比以前慢一些这是缓存冷启动第二次构建就会恢复正常。但如果你对比过构建产物确实发现 main bundle 变大通常是新的控制流语法在编译时使用了额外的模板运行时或者是独立组件迁移把原本共享的模块代码拆进了各自的路由块里。我的排查工具是source-map-explorer直接看产物体积分布。如果发现某个共享模块被多个懒加载块重复打包检查angular/compiler的配置和optimization选项必要时使用inject配合路由懒加载的配置把重复的代码抽到公共 chunk。V21 对独立组件的支持更完善理论上经过合理的模块拆分包体会比 V20 更小。如果你反而变大了多半是迁移器把某些组件标记为独立但依赖没有正确共享这种问题需要结合路由代码逐一确认。5. 升级后的收尾与维护建议5.1 上线前的回归清单升级完成、构建通过不代表事情结束了。在我这次的经验里真正的问题往往在回归测试阶段暴露。我为自己总结了一个上线前回归清单分享给大家参考核心业务链路登录、授权、路由守卫、超时跳转全流程走一遍。列表页重点测大数据量分页、排序、过滤观察是否出现卡顿或数据错乱。表单页双向绑定、校验提示、异步校验确认响应式表单的事件触发正常。懒加载模块逐个访问对应路由确认 chunk 加载正常没有 404。错误上报确认全局错误处理器还能捕获到模板错误和异步错误。回归时用 Chrome DevTools 的 Performance 面板记录几个关键页面的渲染时长和升级前的同场景数据对比。如果某个页面的性能明显下降优先看是不是变更检测范围变大了V21 下可以使用信号来缩小更新范围但前提是你真的切了信号模式。5.2 我踩过的几个坑最后分享几个这次升级中我亲自踩过、差点把我劝退的坑。第一个是ng update中途终端断网导致依赖半更新状态。修复方式是把node_modules整个删掉重新用锁文件安装再继续升级命令。所以升级前一定要确认网络稳定最好用本地缓存仓库。第二个是迁移器把模板里*ngIf改成if后原来用ng-template包裹的else分支会被重构成else但如果原来的else模板里又引用了外层循环变量重构后作用域可能不对需要手动调整。这个问题不报错但跑起来会出现未定义变量非常隐蔽。第三个是测试代码里的fakeAsync与tick。V21 由于变更检测调度的调整依赖fakeAsync的时序测试可能会失败需要把tick次数或异步处理方式重新校准。如果你的项目测试量很大这部分我建议放在升级后的第二天再做因为核心功能回归比测试修复优先级更高。Angular V21 升级不是一次性的命令执行而是一次对项目代码健康度的全面体检。你会在这个过程中发现很多以前被隐藏的坏味道也会逐步理解 Angular 官方为什么会做这样的设计演进。我个人在实际操作中的体会是分步走、勤验证、少用蛮力强改比什么都重要。升级之后回头再看那些繁琐的迁移工作其实是重新梳理项目结构的好机会。