TypeScript Monorepo架构配置全攻略
什么是 Monorepo
在深入探讨 TypeScript Monorepo 架构配置之前,我们先来理解一下什么是 Monorepo。Monorepo,即单一仓库,是一种将多个项目或模块存放在同一个代码仓库中的管理方式。与之相对的是 Polyrepo,即多仓库,每个项目或模块有自己独立的代码仓库。
采用 Monorepo 有诸多优势。首先,它便于代码共享。多个模块之间可以轻松地复用代码,无需繁琐的发布和安装流程。例如,一个工具函数库在多个业务模块中都可能用到,在 Monorepo 中,只需在仓库内引用即可。其次,有利于统一开发流程和工具链。所有项目都遵循相同的编码规范、测试框架和构建工具,减少了因不同项目配置差异带来的问题。再者,对依赖管理更友好。所有项目的依赖都在一个仓库中管理,避免了不同项目依赖同一库但版本不一致的冲突。
然而,Monorepo 也并非十全十美。仓库规模可能变得庞大,管理成本随之增加。大型团队协作时,不同模块的开发进度和需求可能相互影响。
为什么选择 TypeScript 用于 Monorepo
TypeScript 是 JavaScript 的超集,它为 JavaScript 添加了静态类型系统。在 Monorepo 环境下,TypeScript 的优势尤为突出。
强类型检查有助于在开发阶段发现更多错误。在多人协作开发多个模块的 Monorepo 中,明确的类型定义可以避免因数据类型不匹配导致的运行时错误。例如,一个模块提供的 API 接口,通过 TypeScript 的类型定义可以清晰地告知其他模块调用者参数类型和返回值类型,减少错误调用。
TypeScript 对面向对象编程、模块系统的良好支持,契合 Monorepo 中多模块的组织和管理。它的模块语法与 JavaScript 的 ES6 模块语法相似,但更具严谨性,使得模块之间的依赖关系更清晰。
同时,TypeScript 拥有庞大的社区和丰富的工具生态。诸如 ESLint、Prettier 等工具对 TypeScript 有良好的支持,能够帮助我们保持代码风格统一,提高代码质量。
初始化 TypeScript Monorepo
选择项目管理工具
首先,我们需要选择一个合适的项目管理工具来初始化我们的 Monorepo。常见的有 Yarn Workspaces 和 Lerna。
Yarn Workspaces:Yarn 是一个快速、可靠、安全的依赖管理工具。Yarn Workspaces 允许我们在一个根目录下管理多个包(package)。它通过在根目录的 package.json
文件中配置 workspaces
字段来指定各个包的位置。例如:
{
"private": true,
"workspaces": [
"packages/*"
],
"scripts": {
"start": "echo \"This is the root script\""
}
}
上述配置表示 packages
目录下的所有子目录都是一个独立的包。
Lerna:Lerna 是一个用于管理多包仓库的工具,它提供了诸如版本管理、发布等高级功能。Lerna 也支持使用 Yarn Workspaces 来管理依赖。初始化 Lerna 项目很简单,运行 npx lerna init
即可。Lerna 会在根目录生成 lerna.json
文件,用于配置 Lerna 的相关选项。例如:
{
"packages": [
"packages/*"
],
"version": "0.0.1"
}
此配置指定了包的位置和初始版本。
初始化 TypeScript 配置
在根目录下,我们需要初始化 TypeScript 配置文件 tsconfig.json
。这个配置文件会影响整个 Monorepo 中 TypeScript 的编译行为。以下是一个基本的 tsconfig.json
示例:
{
"compilerOptions": {
"target": "ES6",
"module": "commonjs",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}
}
target
:指定编译后的 JavaScript 版本,这里设置为ES6
。module
:指定模块系统,commonjs
适用于大多数 Node.js 环境。outDir
:指定编译后的输出目录。rootDir
:指定源码目录。strict
:开启严格类型检查,有助于发现更多类型错误。esModuleInterop
:允许从 CommonJS 模块中导入默认导出,方便与旧有 JavaScript 代码集成。skipLibCheck
:跳过声明文件的类型检查,加快编译速度。forceConsistentCasingInFileNames
:确保文件名大小写一致,避免在不同操作系统上因文件名大小写问题导致的错误。
如果 Monorepo 中的不同模块有不同的 TypeScript 编译需求,可以在各个模块的子目录下再创建 tsconfig.json
文件,这些子配置文件会继承根目录的 tsconfig.json
配置,并可以根据需要进行覆盖和扩展。例如,某个模块需要使用不同的 target
版本:
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"target": "ES5"
}
}
目录结构设计
一个良好的 Monorepo 目录结构有助于清晰地组织各个模块,提高开发效率。以下是一种常见的目录结构示例:
monorepo-root/
├── packages/
│ ├── module-one/
│ │ ├── src/
│ │ │ ├── index.ts
│ │ │ └── utils.ts
│ │ ├── test/
│ │ │ └── index.test.ts
│ │ ├── package.json
│ │ └── tsconfig.json
│ ├── module-two/
│ │ ├── src/
│ │ │ ├── main.ts
│ │ │ └── models.ts
│ │ ├── test/
│ │ │ └── main.test.ts
│ │ ├── package.json
│ │ └── tsconfig.json
├── tools/
│ ├── scripts/
│ │ └── build.js
│ └── config/
│ └── eslint.config.js
├── package.json
└── tsconfig.json
- packages:存放各个独立的模块。每个模块都有自己的
src
源码目录、test
测试目录、package.json
和tsconfig.json
。 - tools:存放一些工具脚本和配置文件,例如构建脚本、ESLint 配置等。这些工具可以被各个模块复用,统一开发流程。
依赖管理
在 Monorepo 中,依赖管理至关重要。Yarn Workspaces 和 Lerna 在依赖管理上有一些不同的方式,但都旨在避免依赖重复安装,提高安装效率。
Yarn Workspaces 依赖管理
当使用 Yarn Workspaces 时,所有模块的依赖会被安装到根目录的 node_modules
中。例如,在 module - one
的 package.json
中添加 lodash
依赖:
{
"name": "module - one",
"version": "1.0.0",
"dependencies": {
"lodash": "^4.17.21"
}
}
运行 yarn install
后,lodash
会被安装到根目录的 node_modules
中,而不是 module - one
目录下。这样做的好处是避免了每个模块重复安装相同的依赖,节省磁盘空间,提高安装速度。
同时,模块之间也可以相互引用。如果 module - two
需要引用 module - one
,可以在 module - two
的 package.json
中添加依赖:
{
"name": "module - two",
"version": "1.0.0",
"dependencies": {
"module - one": "workspace:^1.0.0"
}
}
这里使用 workspace:
协议来引用同一 Monorepo 中的其他模块。
Lerna 依赖管理
Lerna 同样支持 Yarn Workspaces 的依赖管理方式。此外,Lerna 还提供了一些额外的命令来管理依赖。例如,lerna bootstrap
命令可以安装所有模块的依赖,并链接相互依赖的模块。如果 module - one
依赖 module - two
,运行 lerna bootstrap
会自动在 module - one
的 node_modules
中创建一个符号链接指向 module - two
的源码目录,方便开发和调试。
lerna add
命令可以在指定模块中添加依赖。例如,要在 module - one
中添加 axios
依赖,可以运行 lerna add axios --scope=module - one
。
构建和脚本管理
构建工具选择
在 TypeScript Monorepo 中,常见的构建工具包括 Rollup、Webpack 和 TSC(TypeScript 编译器)本身。
Rollup:Rollup 是一个专注于 ES6 模块的打包工具,它生成的代码体积小、性能高。在 Monorepo 中,对于一些库类型的模块,Rollup 是一个不错的选择。例如,我们有一个 utils
模块,希望将其打包成一个 ES6 模块供其他项目使用,可以这样配置 Rollup:
import typescript from '@rollup/plugin - typescript';
export default {
input: 'packages/utils/src/index.ts',
output: {
file: 'packages/utils/dist/index.js',
format: 'esm'
},
plugins: [typescript()]
};
上述配置指定了入口文件、输出文件和使用 TypeScript 插件进行编译。
Webpack:Webpack 是一个功能强大的前端构建工具,支持多种模块类型和加载器。对于前端应用模块,Webpack 可以处理诸如 CSS、图片等各种资源。例如,在一个 React 应用模块中,Webpack 配置如下:
const path = require('path');
const TsconfigPathsPlugin = require('tsconfig - paths - webpack - plugin');
module.exports = {
entry: './packages/react - app/src/index.tsx',
output: {
path: path.resolve(__dirname, 'packages/react - app/dist'),
filename: 'bundle.js'
},
resolve: {
extensions: ['.ts', '.tsx', '.js'],
plugins: [new TsconfigPathsPlugin()]
},
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts - loader',
exclude: /node_modules/
}
]
}
};
这里配置了入口文件、输出路径、解析规则和 TypeScript 加载器。
TSC:直接使用 TSC 进行构建也是一种简单有效的方式,特别是对于一些不需要复杂打包功能的模块。在模块的 package.json
中添加构建脚本:
{
"scripts": {
"build": "tsc"
}
}
然后在模块的 tsconfig.json
中配置好输出路径等选项,运行 yarn build
即可进行编译。
脚本管理
在根目录的 package.json
中,可以定义一些全局的脚本,方便对整个 Monorepo 进行操作。例如:
{
"scripts": {
"build": "lerna run build",
"test": "lerna run test",
"lint": "lerna run lint"
}
}
这里通过 lerna run
命令在每个模块中执行相应的脚本。每个模块的 package.json
中也可以定义自己的脚本,如:
{
"scripts": {
"build": "tsc",
"test": "jest",
"lint": "eslint src"
}
}
这样,在根目录运行 yarn build
就会依次在每个模块中执行 tsc
命令进行构建。
代码共享与引用
在 Monorepo 中,不同模块之间经常需要共享代码。TypeScript 提供了多种方式来实现代码共享。
模块导出与导入
最常见的方式是通过 TypeScript 的模块导出和导入语法。例如,在 module - one
的 src/utils.ts
中定义一个函数:
export function add(a: number, b: number): number {
return a + b;
}
然后在 module - two
中引用这个函数:
import { add } from'module - one/src/utils';
const result = add(2, 3);
console.log(result);
这里假设 module - two
已经按照前面所述的依赖管理方式配置好了对 module - one
的引用。
创建共享库模块
如果有一些通用的工具函数、类型定义等需要被多个模块共享,可以创建一个专门的共享库模块。例如,在 packages/shared
目录下创建共享代码:
packages/
├── shared/
│ ├── src/
│ │ ├── types.ts
│ │ └── utils.ts
│ ├── package.json
│ └── tsconfig.json
在 utils.ts
中定义共享函数:
export function formatDate(date: Date): string {
return date.toISOString();
}
在其他模块中引用:
import { formatDate } from'shared/src/utils';
const now = new Date();
const formattedDate = formatDate(now);
console.log(formattedDate);
测试策略
在 Monorepo 中,测试策略的制定尤为重要,以确保每个模块的质量和模块之间的集成。
单元测试
对于每个模块,通常会使用单元测试框架来测试单个函数、类等。在 TypeScript 项目中,Jest 是一个广泛使用的单元测试框架。在模块的 package.json
中添加 Jest 相关脚本:
{
"scripts": {
"test": "jest"
},
"devDependencies": {
"@types/jest": "^29.5.4",
"jest": "^29.6.1",
"ts - jest": "^29.1.1"
}
}
然后在模块的 src
目录下创建测试文件,例如 utils.test.ts
:
import { add } from './utils';
test('add function should work correctly', () => {
const result = add(2, 3);
expect(result).toBe(5);
});
运行 yarn test
即可执行单元测试。
集成测试
除了单元测试,还需要进行集成测试,以验证模块之间的交互是否正常。例如,如果 module - one
调用 module - two
的 API,需要编写集成测试来确保调用的正确性。可以使用测试框架如 Cypress 或 Jest 的集成测试功能。
以 Jest 为例,假设 module - one
中有一个函数调用 module - two
的 API:
// module - one/src/consumer.ts
import { apiCall } from'module - two/src/api';
export async function consumeApi() {
const result = await apiCall();
return result;
}
在 module - one
的测试文件中编写集成测试:
import { consumeApi } from './consumer';
test('consumeApi should return correct result', async () => {
const result = await consumeApi();
expect(result).toBeDefined();
});
持续集成与部署
持续集成(CI)
持续集成是确保代码质量的重要环节。常见的 CI 平台有 GitHub Actions、GitLab CI/CD 等。
以 GitHub Actions 为例,在根目录创建 .github/workflows
目录,并在其中创建一个 build - test.yml
文件:
name: Build and Test
on:
push:
branches:
- main
jobs:
build - test:
runs - on: ubuntu - latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Node.js
uses: actions/setup - node@v3
with:
node - version: '18'
- name: Install dependencies
run: yarn install
- name: Build
run: yarn build
- name: Test
run: yarn test
上述配置表示在 main
分支有推送时,在最新的 Ubuntu 环境中安装 Node.js,安装依赖,进行构建和测试。
部署
部署方式因项目类型而异。对于前端应用模块,可以部署到云服务提供商如 AWS S3、Netlify 等。对于后端服务模块,可以部署到服务器上,使用 PM2 等进程管理工具。
例如,对于一个 Node.js 后端服务模块,使用 PM2 进行部署:
- 安装 PM2:
yarn global add pm2
- 在项目目录下创建
ecosystem.config.js
文件:
module.exports = {
apps: [
{
name: 'backend - service',
script: 'dist/index.js',
instances: 1,
autorestart: true,
watch: false,
max_memory_restart: '1G',
env: {
NODE_ENV: 'production'
}
}
]
};
- 运行
pm2 start ecosystem.config.js
启动服务。
通过以上全面的攻略,你应该能够熟练配置和管理 TypeScript Monorepo 架构,充分发挥其优势,提高项目开发效率和代码质量。