第9章:大型工程 —— Monorepo、项目引用、测试与性能
约 4 分钟 · 更新于 2026-09-01
第9章:大型工程 —— Monorepo、项目引用、测试与性能
当项目变大,类型安全必须与构建速度、包边界、发布契约和测试体系一起设计。本章建立大型 TypeScript 工程的反馈闭环。
一、为什么使用 Monorepo
Monorepo 将多个包放在同一仓库中:
text
workspace/
├── package.json
├── pnpm-workspace.yaml
├── tsconfig.base.json
└── packages/
├── domain/
├── api-client/
└── web-app/
适用场景:
- 多个应用共享领域模型和工具。
- 需要原子提交跨包变更。
- 希望统一 lint、测试和发布流程。
代价包括构建复杂度、依赖边界治理、缓存和版本策略,不应只为“目录整齐”采用。
二、共享基础配置
json
{
"compilerOptions": {
"target": "ES2022",
"strict": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}
}
各包继承并覆盖与自身环境相关的选项。浏览器包和 Node 包不应盲目共享全部 lib、模块和运行时类型。
三、Project References
包配置:
json
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"composite": true,
"rootDir": "src",
"outDir": "dist"
},
"include": ["src"],
"references": [
{ "path": "../domain" }
]
}
根配置:
json
{
"files": [],
"references": [
{ "path": "packages/domain" },
{ "path": "packages/api-client" },
{ "path": "packages/web-app" }
]
}
构建:
bash
npx tsc --build
npx tsc --build --clean
Project References 明确包依赖图,支持增量构建和声明边界,但要求每个 composite 项目正确声明输入输出。
四、包边界与公开 API
每个包应通过明确入口公开能力:
ts
// packages/domain/src/index.ts
export type { User, Order } from "./model.js";
export { parseUser } from "./validation.js";
避免消费端深入导入内部文件:
ts
// 不推荐
import { helper } from "@workspace/domain/src/internal/helper";
配合 package.json 的 exports 限制入口,减少内部结构变更影响。
五、依赖规则
建议建立并自动检查:
- 应用可以依赖领域和基础设施包。
- 领域包不依赖 UI 和具体框架。
- 包之间不形成循环依赖。
- 类型包不成为任意共享杂物箱。
- 同一第三方依赖的版本策略明确。
“共享类型”本身也是耦合。前后端共享数据库实体,可能把服务端内部字段泄露给客户端;更好的方式是共享稳定协议或 schema。
六、单元测试与类型检查
行为测试示例:
ts
import { describe, expect, it } from "vitest";
import { parsePort } from "./parse-port";
describe("parsePort", () => {
it("接受合法端口", () => {
expect(parsePort("8080")).toEqual({ ok: true, value: 8080 });
});
});
类型测试关注编译契约:
ts
const id: string = getProperty(user, "id");
// @ts-expect-error 不存在的键必须失败
getProperty(user, "missing");
行为测试、类型测试和 tsc --noEmit 解决不同问题。
七、声明文件测试
库发布前至少验证:
- 生成 .d.ts。
- 建立最小消费项目。
- 分别测试 ESM/CJS 或明确只支持其中一种。
- 验证 exports 与 types 指向正确。
- 检查公共声明是否泄露无法解析的内部类型。
八、测试异步代码
测试应控制:
- Promise 是否被等待。
- 计时器是否使用 fake timers 或明确超时。
- 网络是否通过接口注入或 mock server 隔离。
- 失败路径和取消路径是否覆盖。
- 测试结束后监听器、连接和定时器是否清理。
避免只测成功路径和快照。
九、编译性能
常见优化方向:
- Project References 与 incremental。
- 缩小 include,不要把构建产物再次纳入。
- 避免过度复杂的条件/递归类型。
- 减少巨型联合和重复实例化。
- 控制 barrel 文件造成的依赖扩散。
- 使用 --extendedDiagnostics 观察数据。
- 区分编辑器问题、类型检查问题和 bundler 问题。
bash
npx tsc --noEmit --extendedDiagnostics
不要在没有数据时通过关闭严格检查“优化性能”。
十、运行时性能与 TypeScript
类型通常被擦除,所以大部分类型本身不增加运行时成本。但以下设计会产生真实成本:
- 枚举、装饰器和类字段可能输出代码。
- 运行时 schema 校验需要 CPU,但提供边界安全。
- 为满足类型而复制对象会增加内存。
- 过度抽象可能影响 tree shaking 或包体积。
优化仍应基于运行时 profile 和 bundle 分析,而不是看到泛型就担心性能。
十一、CI 反馈链
一个基础流程:
text
安装锁定依赖
→ 格式与 lint
→ tsc --noEmit 或 tsc --build
→ 单元测试
→ 集成测试
→ 构建产物
→ 库消费测试
大型仓库再加入变更检测、远程缓存和按依赖图执行。
十二、版本升级
TypeScript 新版本可能带来:
- 更严格或更准确的推断。
- 声明文件兼容变化。
- 模块解析变化。
- 标准库类型更新。
- 废弃配置。
升级步骤:
- 阅读 release notes。
- 在独立分支升级。
- 跑完整类型检查和测试。
- 区分真实缺陷、声明不兼容与推断变化。
- 不要用全局 skip 或 any 快速压掉问题。
十三、常见误区
- Monorepo 包之间可以任意深层导入。
- Project References 配置存在但构建仍绕过依赖图。
- 前后端直接共享内部数据库实体。
- 只有运行时单测,没有类型契约测试。
- 发布库只测试源码,不测试打包后的消费体验。
- 通过关闭 strict 解决编译慢。
- 巨型高级类型没有性能预算。
- TypeScript 升级时用大量断言掩盖新增错误。
十四、实践练习
- 创建两个 composite 包并配置项目引用。
- 限制包公开入口,禁止深层导入。
- 为泛型工具增加 @ts-expect-error 类型测试。
- 使用 --extendedDiagnostics 记录一次检查基线。
- 建立最小消费项目验证生成的声明文件。
- 为异步 API 测试成功、失败、超时和取消。
十五、总结
- Monorepo 解决跨包协作,也引入边界和构建治理成本。
- Project References 将包依赖图交给 TypeScript 构建模式。
- 公共入口与 exports 应阻止消费端依赖内部结构。
- 行为测试、类型测试和声明消费测试缺一不可。
- 编译性能要通过诊断数据定位,不要牺牲类型规则逃避根因。
- TypeScript 升级属于工程变更,应通过完整反馈链验证。
请继续阅读:第10章:综合实战。
原始资料引用