Angular cli-hello-world-lazy 集成测试:如何验证懒加载分包与 ngDevMode 的构建期移除
2026/9/8 20:55:59 网站建设 项目流程

Angular cli-hello-world-lazy 集成测试:如何验证懒加载分包与 ngDevMode 的构建期移除

【免费下载链接】angularDeliver web apps with confidence 🚀项目地址: https://gitcode.com/GitHub_Trending/an/angular

cli-hello-world-lazy是 Angular 仓库integration/目录下的一组 CLI 集成测试,核心目标有两个:一是验证当应用存在懒加载模块时,生产构建的 bundle 体积是否符合预期基线;二是验证全局变量ngDevMode及其在 ng_dev_mode.ts 中的字符串引用在 production 构建后被正确 tree-shaking 移除。读完本文,你可以掌握这套"构建产物静态扫描 + 体积基线比对"的回归防护机制的实现方式,并了解如何复用到自己项目的 CI 流程中。

测试意图:两个必须跨 chunk 成立的构建保证

原始文档 README.md 对该测试的定位是:

This test checks bundle sizes when there is a lazy module. It also checks if thengDevModeglobal variable and string references inpackages/core/src/util/ng_dev_mode.tsare correctly removed. This test contains a lazy route to ensurengDevModeremoval happens even across chunks, and a payload size check to ensure extra code is not retained accidentally.

拆解开来,它守护着两条容易在编译器/打包器改动中被悄悄破坏的保证:

  1. ngDevMode必须从产物中消失ngDevMode是 Angular 的开发模式全局开关,仅应在运行时"按需存在"。生产构建中,如果ngDevMode的引用没有被移除,说明 dev-mode 相关的检查代码可能被错误保留(体积泄漏),或者用户无法再通过window.ngDevMode = false等手段干预框架行为。
  2. 懒加载 chunk 必须真正"懒"。仅仅检查主 chunk 是不够的——ngDevMode的引用同样可能出现在路由懒加载生成的独立 chunk 里。因此该测试特意内置了一条懒路由,确保"移除"逻辑对跨 chunk 的产物同样生效。
  3. payload 体积有基线约束。通过 size.json 记录的体积基线,防止"意外保留的多余代码"(例如本应被摇掉的 dev 分支)让产物悄悄膨胀。

最小应用骨架:一条懒路由 + 独立组件

测试应用本身是一个刻意保持最小的 standalone 应用,全部源码位于 integration/cli-hello-world-lazy/src 下。

入口 main.ts 使用bootstrapApplication引导根组件,并通过provideRouter(appRoutes)注册路由:

import {bootstrapApplication, provideProtractorTestingSupport} from '@angular/platform-browser'; import {provideRouter} from '@angular/router'; import {AppComponent} from './app/app.component'; import {appRoutes} from './app/app.routes'; bootstrapApplication(AppComponent, { providers: [provideRouter(appRoutes), provideProtractorTestingSupport()], }).catch(console.error);

关键在于 app.routes.ts 中定义的唯一一条路由,它使用loadChildren配合动态import()指向独立的懒路由文件:

export const appRoutes: Routes = [ { path: 'lazy', loadChildren: () => import('./lazy/lazy.routes').then((routes) => routes.lazyRoutes), }, ];

而 lazy.routes.ts 只包含一个极简组件:

export const lazyRoutes: Routes = [{path: '', component: LazyComponent}];

LazyComponent 的模板只有一行<p>lazy works!</p>。根组件 AppComponent 则通过RouterOutlet提供渲染出口。这个骨架的意义在于:import('./lazy/lazy.routes')这条动态导入是打包器切分 chunk 的唯一依据——构建后它会生成一个独立于main.js的懒加载 chunk,正是检查脚本需要扫描的对象。

构建配置:为什么 production 配置是检查的前提

angular.json 使用了新版@angular/build:application构建器,其配置对两个检查目标都做了针对性设计:

配置项默认(dev)production 配置作用
optimizationfalsetrue开启 terser 压缩与 dead code elimination,这是ngDevMode被移除的前提
aottrue全程 AOT 编译
namedChunkstruetrue用模块名而非数字编号命名 chunk,产物可读、可定位
outputHashingnonenone主入口文件名稳定,便于检查脚本与体积基线按固定文件名比对
sourceMapfalse减小产物,且避免 map 文件干扰.js扫描
extractLicensestrue与真实生产发布对齐
polyfills["zone.js"]使用 zone.js 调度

production 配置还声明了两组 budgets:

"budgets": [ {"type": "initial", "maximumWarning": "2mb", "maximumError": "5mb"}, {"type": "anyComponentStyle", "maximumWarning": "6kb", "maximumError": "10kb"} ]

这些预算在ng build --configuration production阶段即构成第一道体积防线:initial chunk 超过 2mb 警告、5mb 直接报错。而真正的"精确基线"检查则由 size.json 承担,当前记录的体积基线为:

{ "dist/main.js": 108611, "dist/polyfills.js": 34169, "dist/lazy.routes-[hash].js": 361 }

可以看到懒加载 chunk(lazy.routes-[hash].js)被单独列名且仅约 361 字节——这正体现了"懒代码不混入初始 payload"的预期:初始main.js与懒 chunk 的体积各自独立受控。

ngDevMode 移除检查:一个扫描 dist 的 Node 脚本

检查逻辑全部集中在 check-output-for-ngdevmode.js,全文如下:

const fs = require('fs'); const path = require('path'); const distPath = './dist/'; const ngDevModeVariable = 'ngDevMode'; const filesWithNgDevMode = fs .readdirSync(distPath) .filter((p) => p.endsWith('.js')) .filter((p) => fs.readFileSync(path.join(distPath, p), 'utf-8').includes(ngDevModeVariable)); if (filesWithNgDevMode.length > 0) { throw new Error( `Found '${ngDevMode}' referenced in ${filesWithNgDevMode}. These references should be tree-shaken away!`, ); } else { console.log(`No '${ngDevMode}' references found in ${distPath}`); }

脚本的思路非常直接但足够严格:递归无关,逐个文件、逐字节。它列出dist/目录下所有.js文件(即同时覆盖main.js与懒加载 chunk),对每个文件做子串匹配ngDevMode;只要任何一个文件命中,就抛出错误并列出命中文件名,使pnpm test失败。这个"字符串级"检查比"变量级"检查更强——它连压缩后残留的字符串字面量都能捕获。

这个检查之所以必要且可行,可以从 ng_dev_mode.ts 的源码实现得到印证。框架通过declare global声明了ngDevMode全局量,其值可以是null | NgDevModePerfCounters(还附带 hydration 性能计数器),并可能为false。初始化入口是:

export function initNgDevMode(): boolean { if (typeof ngDevMode === 'undefined' || ngDevMode) { if (typeof ngDevMode !== 'object' || Object.keys(ngDevMode).length === 0) { ngDevModeResetPerfCounters(); } return typeof ngDevMode !== 'undefined' && !!ngDevMode; } return false; }

(参见 ng_dev_mode.ts#L80-L92)

值得注意的是 ng_dev_mode.ts#L49-L52 中的这段注释与写法:

// Make sure to refer to ngDevMode as ['ngDevMode'] for closure. ... global['ngDevMode'] = false;

框架源码刻意用global['ngDevMode']的括号取值方式而非global.ngDevMode,目的就是配合编译器(closure)在 production 优化时把对全局的探测彻底擦除。换言之,"产物里不应再出现ngDevMode字符串"是框架源码写法、optimization: true的 dead code elimination、以及本检查脚本三者共同约定并守护的契约。该测试中的懒路由则进一步保证这份契约在每一个chunk 上都成立——这正是 README 中 "even across chunks" 的含义。

测试执行链:pnpm 脚本与 Bazel 编排

在 package.json 中,整条测试链被压缩为两个脚本:

"build": "ng build --configuration production", "test": "pnpm build && node check-output-for-ngdevmode.js"

即:先用 production 配置执行真实构建(optimization: true),再运行产物扫描脚本。任何一步失败(构建超预算、产物中出现ngDevMode)都会使test退出非零。

该目录下的 BUILD.bazel 只有一行:

ng_integration_test( name = "test", )

具体的行为由 integration/index.bzl 中的ng_integration_test宏统一提供。从该宏的源码结构看,它会自动:

  • native.glob收集本目录全部源文件(排除node_modules)作为测试输入;
  • 默认执行命令为pnpm install+pnpm run test,并用tool_mappings注入 pnpm 与 node 工具链;
  • 把仓库内所有INTEGRATION_PACKAGES(即packages/下各 Angular 包)以本地构建的 tgz 归档覆盖进测试的node_modules——这意味着该集成测试验证的是当前分支刚构建出来的 Angular 框架代码,而非 npm 上的已发布版本;
  • 为测试打上integrationno-sandbox标签(因为它可能启动宿主应用),超时设为long(15 分钟),体量标记为enormous

这套机制的可复用价值

cli-hello-world-lazy虽然代码量极小,但它示范了一种对前端框架项目非常有价值的回归防护范式,三个构件均可独立借鉴:

  1. 字符串级产物扫描:用一个几十行的 Node 脚本断言"某些标识符/字符串绝不能出现在 dist 中",比单元测试更早发现构建管线回归(对应 check-output-for-ngdevmode.js);
  2. 体积基线文件:用 size.json 之类的基线把"产物不能意外变大"变成可 diff 的显式断言,与budgets的粗粒度警告形成两级防护;
  3. 最小应用 + 真实构建路径:不复用大型 demo,而是维护一个几十行的最小应用(standalone 根组件、一条loadChildren懒路由),走真实的ng build --configuration production流程,从而精确聚焦某一类构建行为——这里是"懒加载分包"与"dev 全局变量移除"。

需要注意适用前提:该检查脚本依赖outputHashing: none下稳定的文件名约定与dist/的扁平目录结构;若你的项目启用了内容哈希或嵌套输出目录,需要先调整扫描路径的枚举方式。另外,该集成测试在 Bazel 环境下运行时会用仓库内构建的 Angular 包覆盖 npm 依赖,因此它验证的是"当前源码构建出的框架"的行为,这一前提在移植到其他仓库时应替换为锁定具体依赖版本。

【免费下载链接】angularDeliver web apps with confidence 🚀项目地址: https://gitcode.com/GitHub_Trending/an/angular

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询