MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

TypeScript Monorepo架构配置全攻略

2023-02-014.5k 阅读

什么是 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.jsontsconfig.json
  • tools:存放一些工具脚本和配置文件,例如构建脚本、ESLint 配置等。这些工具可以被各个模块复用,统一开发流程。

依赖管理

在 Monorepo 中,依赖管理至关重要。Yarn Workspaces 和 Lerna 在依赖管理上有一些不同的方式,但都旨在避免依赖重复安装,提高安装效率。

Yarn Workspaces 依赖管理

当使用 Yarn Workspaces 时,所有模块的依赖会被安装到根目录的 node_modules 中。例如,在 module - onepackage.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 - twopackage.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 - onenode_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 - onesrc/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 进行部署:

  1. 安装 PM2:yarn global add pm2
  2. 在项目目录下创建 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'
      }
    }
  ]
};
  1. 运行 pm2 start ecosystem.config.js 启动服务。

通过以上全面的攻略,你应该能够熟练配置和管理 TypeScript Monorepo 架构,充分发挥其优势,提高项目开发效率和代码质量。