Angular懒加载路由实战:从原理到企业级避坑指南

📅 2026/6/22 3:00:49
Angular懒加载路由实战:从原理到企业级避坑指南
1. 项目概述为什么 Angular 的懒加载路由不是“锦上添花”而是“生死线”你刚接手一个中型 Angular 企业后台系统首页加载时间 4.2 秒FMP首次内容绘制指标在 Lighthouse 里红得刺眼。打开 DevTools 的 Network 面板一眼扫过去main.js2.8MB、vendor.js3.1MB、polyfills.js1.2MB——三个文件加起来快 7MB全在首屏就一股脑儿砸给浏览器。用户点开“客户管理”模块前得先下载完“库存报表”“财务对账”“权限审计”所有模块的代码。这不是优化这是自残。这就是没用懒加载路由的真实代价。Angular 的懒加载Lazy Loading根本不是什么高级技巧它是现代单页应用SPA的生存底线。它让RouterModule不再把整个应用的路由配置一次性编译进主包而是按需动态加载模块——用户点“订单中心”才去拉orders.module.ts及其依赖点“商品库”才加载products.module.ts。核心逻辑就一条把 7MB 的初始包拆成 300KB 的壳 若干个 200–500KB 的功能模块包由路由触发加载。关键词Angular、Lazy Loading、Routes、loadChildren、Angular CLI全部指向同一个实操闭环用 Angular CLI 创建模块 → 在app-routing.module.ts中配置loadChildren→ 编译时自动切分代码块 → 运行时按需 fetch。而网络热词里反复出现的vue2 routes后加载、vue2 routes远程加载恰恰反向印证了这个问题的普适性——Vue 2 时代靠import()动态导入实现类似效果但 Angular 把这套机制深度集成进路由系统和 CLI 工具链原生支持、零配置、强类型、可预测。我去年帮一家做医疗 SaaS 的客户重构路由把 12 个业务模块全部懒加载后首屏 JS 体积从 6.9MB 降到 1.1MBTTFB首字节时间不变的前提下FCP最大内容绘制从 3.8s 缩短到 0.9s用户跳出率直降 37%。这不是理论是压在生产环境上的真实水位线。2. 核心设计思路为什么必须用loadChildren而不是component背后的编译器真相2.1 懒加载的本质不是“延迟渲染”而是“延迟编译”很多新手误以为懒加载就是“等用户点进来再渲染组件”这是致命误解。真正的懒加载发生在AOTAhead-of-Time编译阶段而非运行时。Angular CLI 在执行ng build --prod时会扫描所有loadChildren配置项识别出哪些模块被标记为异步加载然后启动 Webpack 的代码分割Code Splitting机制将这些模块及其所有依赖组件、服务、管道、指令单独打包成独立的 chunk 文件比如orders-1a2b3c4d.js、reports-5e6f7g8h.js。当用户导航到/orders时Angular Router 才会动态import()这个 chunk执行其中的模块定义最后实例化组件。而如果用component直接配置路由比如{ path: orders, component: OrdersComponent }OrdersComponent及其整个依赖树OrdersService、OrderTableComponent、CurrencyPipe等会被强制打入main.js因为 AOT 编译器需要在构建时就确定所有静态引用关系。此时OrdersComponent是“已知的、确定的、必须存在的”无法被分割出去。提示loadChildren的值必须是一个返回PromiseNgModuleFactory的函数Angular 8 后推荐使用() import(./orders/orders.module).then(m m.OrdersModule)这种基于import()的动态导入语法它明确告诉 Webpack“这个模块可以独立打包”。2.2loadChildren的两种写法从 Angular 7 到 Angular 17 的演进陷阱Angular 7 引入loadChildren字符串写法./orders/orders.module#OrdersModule但这种写法在 Angular 8 中已被废弃且存在严重隐患类型不安全字符串./orders/orders.module#OrdersModule完全绕过 TypeScript 编译检查。如果OrdersModule类名拼错或路径写成./order/orders.module少了个s编译器不会报错只有运行时import()失败才抛Error: Cannot find module调试成本极高。IDE 支持差VS Code 无法跳转到该模块无法进行重命名重构Rename Refactor一旦模块改名所有字符串引用全得手动改极易遗漏。Tree-shaking 失效Webpack 无法静态分析字符串路径可能保留未使用的模块代码。所以必须用函数式写法// ✅ 正确TypeScript 可检查、IDE 可跳转、Webpack 可分析 { path: orders, loadChildren: () import(./orders/orders.module).then(m m.OrdersModule) } // ❌ 错误已废弃、无类型检查、调试地狱 { path: orders, loadChildren: ./orders/orders.module#OrdersModule }我踩过最深的坑是在一个 Angular 12 项目里混用两种写法主路由用函数式但某个子路由忘了改还留着字符串写法。上线后一切正常直到某天 CI/CD 流水线升级了 Webpack 版本那个字符串路由突然 404监控告警炸了。查了 3 小时才发现是废弃语法被新 Webpack 彻底移除。从此我的团队立下铁规git grep loadChildren.*# -n成为每次 PR 的必检命令。2.3 为什么不能把懒加载逻辑塞进component——模块边界与依赖注入的硬约束有人会想“既然OrdersComponent是个组件我直接在app-routing.module.ts里配component: OrdersComponent然后在OrdersComponent的ngOnInit里手动import(./orders.service)行不行” 答案是技术上可行但工程上自杀。原因有三服务注入失效OrdersService如果在OrdersModule的providers数组中声明它只对该模块内的组件有效。若OrdersComponent被直接放在AppModule下即非懒加载它就无法获得OrdersModule提供的OrdersService实例只能注入AppModule的全局服务导致业务逻辑错乱。样式隔离崩溃OrdersComponent的styleUrls或styles依赖OrdersModule中引入的第三方 UI 库如angular/material的主题 CSS。如果模块未加载这些样式根本不会注入 DOM组件会显示为裸 HTML。变更检测失序懒加载模块有自己的NgModuleRef和独立的ChangeDetectorRef实例。手动import()组件类但不加载其所属模块Angular 的变更检测机制无法正确挂载async管道、OnPush策略等全部失效。懒加载的最小单元是NgModule不是Component。这是 Angular 设计哲学的硬性边界——模块是依赖注入、组件注册、样式作用域、变更检测的原子容器。试图绕过它等于在沙上建塔。3. 实操全流程从零创建一个可验证的懒加载路由含 CLI 命令、配置细节与编译产物分析3.1 第一步用 Angular CLI 创建懒加载模块带路由别手写orders.module.tsCLI 会帮你生成标准结构包括路由模块、声明组件、导出模块——这一步省掉 80% 的配置错误# 在项目根目录执行 ng generate module orders --route orders --module app.module这条命令做了五件事创建src/app/orders/orders.module.ts空的NgModuledeclarations为空创建src/app/orders/orders-routing.module.ts包含{ path: , component: OrdersComponent }的子路由创建src/app/orders/orders.component.ts及配套 HTML/CSS/Spec 文件修改src/app/app-routing.module.ts自动添加一条路由{ path: orders, loadChildren: () import(./orders/orders.module).then(m m.OrdersModule) }修改src/app/app.module.ts不将OrdersComponent加入declarations也不导入OrdersModule—— 这是关键懒加载模块绝不能在根模块中声明。注意--route orders参数指定了子路由路径为orders所以最终访问地址是/orders而非/orders/orders。CLI 会自动在orders-routing.module.ts中设置path: 确保子路由相对路径正确。3.2 第二步填充业务逻辑并验证路由跳转现在OrdersComponent是空的我们加点真实内容来验证// src/app/orders/orders.component.ts import { Component, OnInit } from angular/core; Component({ selector: app-orders, template: h2订单中心 (v{{ version }})/h2 p当前时间{{ now | date:yyyy-MM-dd HH:mm:ss }}/p button (click)refresh()刷新时间/button , styles: [h2 { color: #1976d2; }] }) export class OrdersComponent implements OnInit { version 1.0.0; now new Date(); ngOnInit(): void { console.log([OrdersModule] 已加载); } refresh(): void { this.now new Date(); } }启动开发服务器ng serve打开浏览器访问http://localhost:4200/orders。此时观察 Network 面板首次访问/orders时会看到一个新请求orders-1a2b3c4d.jshash 值因项目而异大小约 120KB含OrdersComponent、OrdersModule、CommonModule等控制台输出[OrdersModule] 已加载再次点击浏览器后退回到/然后重新点/ordersNetwork 面板不再发起orders-*.js请求已缓存如果你清空浏览器缓存再访问/首页Network 面板里绝对看不到orders-*.js—— 它只在/orders路径下才加载。这就是懒加载生效的铁证模块代码与路由路径严格绑定无请求、无加载、无内存占用。3.3 第三步构建生产包并分析代码分割结果执行生产构建ng build --configuration production。构建完成后进入dist/your-app-name/目录查看生成的 JS 文件ls -lh dist/your-app-name/*.js # 输出示例 # 124K main.1a2b3c4d.js # 主应用包不含 OrdersModule # 3.1M vendor.5e6f7g8h.js # 第三方库Angular Core、RxJS 等 # 120K orders-9i0j1k2l.js # 懒加载模块 1 # 210K reports-m3n4o5p6.js # 懒加载模块 2 # 85K runtime.7q8r9s0t.js # Webpack 运行时用source-map-explorer工具可视化分析需先安装npm install -g source-map-explorersource-map-explorer dist/your-app-name/main.1a2b3c4d.js你会看到一张清晰的依赖图main.js里只有AppModule、CoreModule、SharedModule的代码OrdersComponent、OrdersService等节点完全消失——它们被归入orders-*.js的独立 chunk 中。实操心得我习惯在angular.json的build.options.statsJson设为true构建后生成stats.json再用 webpack-bundle-analyzer 可视化分析。比source-map-explorer更直观能直接看到每个 chunk 的组成和大小占比。3.4 第四步处理常见需求——预加载、错误处理与加载状态反馈懒加载不是“放任不管”生产环境必须处理三大现实问题预加载Preloading平衡速度与体验默认情况下懒加载模块只在用户点击时才加载可能造成点击后白屏等待。Angular 提供PreloadAllModules策略在空闲时NavigationEnd事件后自动预加载所有懒加载模块// app-routing.module.ts import { PreloadAllModules, RouterModule, Routes } from angular/router; const routes: Routes [ { path: , redirectTo: /dashboard, pathMatch: full }, { path: dashboard, loadChildren: () import(./dashboard/dashboard.module).then(m m.DashboardModule) }, { path: orders, loadChildren: () import(./orders/orders.module).then(m m.OrdersModule) }, // ... 其他路由 ]; NgModule({ imports: [ RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules // ⚡ 开启预加载 }) ], exports: [RouterModule] }) export class AppRoutingModule { }预加载不是“全量加载”而是利用浏览器空闲时间用户阅读首页内容时后台静默下载orders-*.js、reports-*.js。实测数据在 4G 网络下预加载可将/orders首次点击的加载延迟从 800ms 降至 120ms纯内存加载。但要注意预加载会增加首页的总下载量如果用户只用 20% 的功能预加载反而浪费带宽。我的建议是对高频路径如/dashboard,/profile用PreloadAllModules对低频路径如/admin/settings保持按需加载。加载状态与错误处理给用户确定性反馈用户点击/orders后如果网络慢必须显示加载中状态如果模块加载失败404、500、网络中断必须友好提示。Angular Router 提供Router.events监听RouteConfigLoadStart/RouteConfigLoadEnd事件// app.component.ts import { Component, OnInit, OnDestroy } from angular/core; import { Router, Event, RouteConfigLoadStart, RouteConfigLoadEnd, NavigationError } from angular/router; import { Subscription } from rxjs; Component({ selector: app-root, template: app-header/app-header div *ngIfloading classloading-overlay span正在加载模块.../span /div router-outlet/router-outlet , styles: [ .loading-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(255,255,255,0.8); display: flex; align-items: center; justify-content: center; z-index: 1000; } ] }) export class AppComponent implements OnInit, OnDestroy { loading false; private routerSub: Subscription; constructor(private router: Router) {} ngOnInit(): void { this.routerSub this.router.events.subscribe((event: Event) { if (event instanceof RouteConfigLoadStart) { this.loading true; } else if (event instanceof RouteConfigLoadEnd || event instanceof NavigationError) { this.loading false; } }); } ngOnDestroy(): void { if (this.routerSub) this.routerSub.unsubscribe(); } }这段代码会在任何懒加载模块开始加载时显示半透明遮罩层加载完成或失败后自动隐藏。NavigationError事件还能捕获具体错误else if (event instanceof NavigationError) { console.error(路由加载失败:, event.error); alert(模块加载失败请检查网络或稍后重试。错误码${event.error.status}); }注意RouteConfigLoadStart/End仅针对loadChildren触发component路由不会触发。这是判断懒加载是否生效的另一个监控点。4. 深度避坑指南那些官方文档不会写的 7 个致命陷阱与实战对策4.1 陷阱一forRoot()与forChild()混用导致的路由冲突新手常犯错误在懒加载模块的OrdersRoutingModule中错误地调用RouterModule.forRoot(routes)// ❌ 错误OrdersRoutingModule.ts import { NgModule } from angular/core; import { RouterModule, Routes } from angular/router; import { OrdersComponent } from ./orders.component; const routes: Routes [{ path: , component: OrdersComponent }]; NgModule({ imports: [RouterModule.forRoot(routes)], // ⚠️ 错这里应该是 forChild exports: [RouterModule] }) export class OrdersRoutingModule { }forRoot()会注册全局的Router服务实例并重置整个路由配置。当OrdersModule被懒加载时它会覆盖AppRoutingModule中的forRoot()配置导致/dashboard、/profile等所有路由失效只剩/orders可用。正确写法// ✅ 正确OrdersRoutingModule.ts import { NgModule } from angular/core; import { RouterModule, Routes } from angular/router; import { OrdersComponent } from ./orders.component; const routes: Routes [{ path: , component: OrdersComponent }]; NgModule({ imports: [RouterModule.forChild(routes)], // ✅ 必须用 forChild exports: [RouterModule] }) export class OrdersRoutingModule { }forChild()只将子路由追加到父路由配置中不创建新Router实例。这是 Angular 路由模块化的基石规则。4.2 陷阱二懒加载模块中重复提供服务引发状态污染假设OrdersService是一个管理订单列表的可变服务// orders.service.ts Injectable({ providedIn: root // ⚠️ 危险全局单例 }) export class OrdersService { private list: Order[] []; getList() { return this.list; } add(order: Order) { this.list.push(order); } }如果OrdersService的providedIn: root它就是一个全局单例。当用户从/orders跳到/reports另一个懒加载模块再返回/ordersOrdersService.list里的数据还在——这看似合理但如果ReportsModule也用了同名服务或不同模块需要隔离状态就会出问题。对策将服务提供范围限定在模块内// orders.module.ts import { NgModule } from angular/core; import { CommonModule } from angular/common; import { OrdersRoutingModule } from ./orders-routing.module; import { OrdersComponent } from ./orders.component; import { OrdersService } from ./orders.service; NgModule({ declarations: [OrdersComponent], imports: [CommonModule, OrdersRoutingModule], providers: [OrdersService] // ✅ 在模块 providers 中提供非 root }) export class OrdersModule { }此时OrdersService的生命周期与OrdersModule绑定模块加载时创建实例模块卸载时销毁实例Angular 9 默认启用onDestroy生命周期钩子。用户离开/orders后服务实例被 GC 回收状态彻底清空。4.3 陷阱三CanLoad守卫的执行时机与权限校验盲区CanLoad守卫用于在模块加载前拦截常用于权限控制// auth.guard.ts Injectable({ providedIn: root }) export class AuthGuard implements CanLoad { constructor(private authService: AuthService, private router: Router) {} canLoad(route: Route): boolean { if (this.authService.isLoggedIn()) { return true; } else { this.router.navigate([/login]); return false; } } } // app-routing.module.ts { path: admin, loadChildren: () import(./admin/admin.module).then(m m.AdminModule), canLoad: [AuthGuard] // ✅ 配置 canLoad }但canLoad有一个致命盲区它只在模块首次加载时执行一次。用户登录后访问/admincanLoad返回true模块加载成功之后用户在/admin页面内操作触发登出此时AuthService.isLoggedIn()变为false但用户仍在/admin页面——canLoad不会再次触发因为模块已加载路由只是在模块内部切换。对策canLoad只做“准入检查”真正的权限校验必须结合CanActivate守卫// admin-routing.module.ts const routes: Routes [ { path: , component: AdminDashboardComponent, canActivate: [AuthGuard] // ✅ 每次进入组件都校验 }, { path: users, component: UserListComponent, canActivate: [AuthGuard] } ];CanLoad防止未授权用户下载敏感模块代码CanActivate防止已加载模块内的非法访问二者缺一不可。4.4 陷阱四import()路径错误导致的构建成功但运行时 404这是最隐蔽的坑。假设你的模块路径是src/app/features/orders/orders.module.ts但你在路由中写成// ❌ 错误路径多了一层 features loadChildren: () import(./features/orders/orders.module).then(m m.OrdersModule)Angular CLI 构建时不会报错因为import()是运行时解析。但ng build生成的orders-*.js文件名是基于import()字符串路径计算的。如果路径写错Webpack 会生成一个不存在的 chunk运行时import()失败报Error: Cannot find module ./features/orders/orders.module。排查方法查看dist/your-app-name/目录下实际生成的orders-*.js文件名对比import()字符串路径确认是否完全匹配注意./开头表示相对路径/开头表示绝对路径使用 VS Code 的“Go to Definition”CtrlClick功能点击import(./xxx)看能否正确跳转到模块文件。终极保险在tsconfig.json的compilerOptions.baseUrl设为src然后统一用绝对路径// tsconfig.json { compilerOptions: { baseUrl: src } }// ✅ 绝对路径不易出错 loadChildren: () import(app/features/orders/orders.module).then(m m.OrdersModule)4.5 陷阱五resolve数据预加载与懒加载的时序冲突resolve守卫用于在组件激活前获取数据// orders.resolver.ts Injectable({ providedIn: root }) export class OrdersResolver implements ResolveOrder[] { constructor(private service: OrdersService) {} resolve(route: ActivatedRouteSnapshot): ObservableOrder[] { return this.service.getOrders(); // HTTP 请求 } } // orders-routing.module.ts const routes: Routes [ { path: , component: OrdersComponent, resolve: { orders: OrdersResolver } // ✅ 配置 resolve } ];问题来了resolve的执行依赖OrdersService而OrdersService在OrdersModule中提供。但resolve守卫的实例化发生在OrdersModule加载之前Angular Router 需要在模块加载前就准备好OrdersResolver实例以便在导航时调用resolve()方法。解决方案将OrdersResolver提升到AppModule中提供并通过Injector获取懒加载模块的服务// app.module.ts import { OrdersResolver } from ./orders/orders.resolver; NgModule({ // ... providers: [OrdersResolver] // ✅ 在根模块提供 resolver }) export class AppModule { } // orders.resolver.ts Injectable({ providedIn: root }) export class OrdersResolver implements ResolveOrder[] { constructor( private injector: Injector, // ⚡ 注入 injector private http: HttpClient ) {} resolve(route: ActivatedRouteSnapshot): ObservableOrder[] { // 在 resolve 时动态获取 OrdersService模块已加载 const ordersService this.injector.get(OrdersService); return ordersService.getOrders(); } }这样OrdersResolver是全局单例但getOrders()调用时OrdersService已被OrdersModule加载injector.get()能正确返回实例。4.6 陷阱六ng update升级后loadChildren报Cannot find module的版本兼容问题Angular 14 升级到 Angular 15 时部分项目出现loadChildren报错Error: Cannot find module ./orders/orders.module根本原因是 Angular 15 的angular-devkit/build-angular默认启用了esbuild作为构建器而esbuild对import()动态路径的解析规则与 Webpack 不同。esbuild要求import()的路径必须是静态字符串字面量不能包含变量或表达式。如果你的代码中有// ❌ Angular 15 esbuild 下报错 const moduleName ./orders/orders.module; loadChildren: () import(moduleName).then(m m.OrdersModule)对策严格使用静态字符串// ✅ 正确永远用静态字符串 loadChildren: () import(./orders/orders.module).then(m m.OrdersModule)或者在angular.json中强制回退到 Webpack 构建器不推荐放弃 esbuild 的速度优势// angular.json architect: { build: { builder: angular-devkit/build-angular:browser, options: { builder: angular-devkit/build-angular:browser-esbuild, // 改为 browser // ... } } }4.7 陷阱七微前端场景下子应用懒加载的跨域与资源路径错乱当 Angular 应用作为微前端子应用如 qiankun、single-spa嵌入主应用时懒加载模块的orders-*.js请求路径可能出错。例如主应用地址是https://main.com子应用挂载在https://main.com/subapp但orders-*.js却向https://main.com/orders-*.js发起请求404而非https://main.com/subapp/orders-*.js。这是因为 Angular CLI 默认将baseHref设为/Webpack 的publicPath也是/。解决方案是构建时指定baseHrefng build --base-href /subapp/ --deploy-url /subapp/--base-href /subapp/设置base href/subapp/影响所有相对路径--deploy-url /subapp/设置 Webpack 的publicPath让orders-*.js的请求 URL 变为/subapp/orders-*.js。在微前端场景下这是必须的构建参数否则懒加载必然失败。5. 进阶实战从单模块懒加载到企业级路由架构设计5.1 场景一嵌套路由与多级懒加载——电商后台的典型结构一个真实的电商后台路由往往有多层嵌套/ # 主应用Dashboard ├── /products # 商品管理懒加载 │ ├── /list # 商品列表子路由 │ ├── /create # 新建商品子路由 │ └── /edit/:id # 编辑商品子路由 ├── /orders # 订单管理懒加载 │ ├── /list # 订单列表子路由 │ └── /detail/:id # 订单详情子路由 └── /customers # 客户管理懒加载实现方式是两级懒加载AppRoutingModule懒加载ProductsModuleProductsModule再懒加载其子模块如ProductListModule// app-routing.module.ts { path: products, loadChildren: () import(./products/products.module).then(m m.ProductsModule) } // products/products.module.ts import { NgModule } from angular/core; import { CommonModule } from angular/common; import { ProductsRoutingModule } from ./products-routing.module; import { ProductsComponent } from ./products.component; NgModule({ declarations: [ProductsComponent], imports: [ CommonModule, ProductsRoutingModule // ✅ 子路由模块 ] }) export class ProductsModule { } // products/products-routing.module.ts const routes: Routes [ { path: , component: ProductsComponent, children: [ { path: list, loadChildren: () import(./product-list/product-list.module).then(m m.ProductListModule) }, { path: create, loadChildren: () import(./product-create/product-create.module).then(m m.ProductCreateModule) } ] } ];这种结构让product-list.module.ts可以独立打包product-list-*.js进一步细化代码分割粒度。实测将商品列表页从ProductsModule中拆出后ProductsModule包体积从 420KB 降至 180KBproduct-list-*.js为 210KB用户首次访问/products时更快后续点/products/list也只需加载 210KB。5.2 场景二按角色动态加载模块——SaaS 多租户权限体系SaaS 平台中不同租户客户看到的功能模块不同。A 客户购买了“报表模块”B 客户没有购买就不该加载reports.module.ts。传统做法是所有模块都懒加载再用*ngIf控制菜单显示。但这样reports-*.js仍会被预加载或用户偶然触发加载浪费资源。动态路由方案在AppRoutingModule初始化时根据用户权限 API 返回的数据动态构建routes数组// app-routing.module.ts import { NgModule, APP_INITIALIZER } from angular/core; import { RouterModule, Routes } from angular/router; import { AuthService } from ./auth.service; export function initApp(authService: AuthService) { return () authService.loadUserPermissions(); // 返回 Promise } // 动态路由数组初始为空 let dynamicRoutes: Routes []; NgModule({ imports: [ RouterModule.forRoot(dynamicRoutes, { // ... 配置 }) ], exports: [RouterModule], providers: [ { provide: APP_INITIALIZER, useFactory: initApp, deps: [AuthService], multi: true } ] }) export class AppRoutingModule { constructor( private router: Router, private authService: AuthService ) { // 在构造函数中权限数据已加载完成 const permissions this.authService.getPermissions(); const routes: Routes [ { path: , redirectTo: /dashboard, pathMatch: full } ]; if (permissions.includes(products)) { routes.push({ path: products, loadChildren: () import(./products/products.module).then(m m.ProductsModule) }); } if (permissions.includes(reports)) { routes.push({ path: reports, loadChildren: () import(./reports/reports.module).then(m m.ReportsModule) }); } // ✅ 动态重置路由配置 this.router.resetConfig(routes); } }APP_INITIALIZER确保权限加载完成后再初始化路由router.resetConfig()动态更新路由表。这样B 客户的浏览器里永远不会出现reports-*.js的请求真正实现“按需交付”。5.3 场景三离线优先策略——Service Worker 与懒加载模块的协同PWA渐进式 Web 应用要求离线可用。Angular 的angular/pwa提供ng add angular/pwa命令但它默认只缓存index.html、main.js、vendor.js等主包不缓存懒加载模块的orders-*.js。要让/orders在离线时也能访问必须在ngsw-config.json中显式声明{ navigationUrls: [ { positive: true, regex: ^\\/.*$ } ], assetGroups: [ { name: app, installMode: prefetch, resources: { files: [ /favicon.ico, /index.html, /*.css, /*.js ] } }, { name: lazy-modules, installMode: lazy, updateMode: prefetch, resources: { files: [ /orders-*.js, /reports-*.js, /customers-*.js ] } } ] }installMode: lazy这些文件不在安装 Service Worker 时下载而是按需缓存updateMode: prefetch