Monorepo管理

学习目标

  • 理解Monorepo的基本概念和优势
  • 掌握使用Nx管理NestJS Monorepo的方法
  • 学习使用Lerna管理多包项目
  • 理解工作空间结构和组织方式
  • 掌握依赖共享和代码共享的实现
  • 学习Monorepo中的构建、测试和部署策略
  • 了解Monorepo的最佳实践和常见问题解决方案

核心知识点

Monorepo基础

Monorepo的基本概念和优势:

  • Monorepo定义:将多个项目存储在单个代码仓库中的软件开发策略
  • 优势
    • 简化依赖管理
    • 促进代码共享和重用
    • 统一的构建和测试流程
    • 简化跨项目更改
    • 提高代码质量和一致性
  • 挑战
    • 仓库体积增大
    • 构建时间延长
    • 需要专门的工具支持
    • 权限管理复杂

Nx工作空间

Nx是一个强大的Monorepo管理工具:

  • Nx核心概念
    • 工作空间(Workspace):包含多个项目的代码库
    • 项目(Project):工作空间中的单个应用或库
    • 库(Library):可重用的代码模块
    • 生成器(Generator):用于创建和修改代码
    • 执行器(Executor):用于运行任务(如构建、测试)
  • Nx特性
    • 智能缓存
    • 依赖图分析
    • 代码生成
    • 任务编排
    • 插件系统

Lerna管理

Lerna是专门用于管理JavaScript多包仓库的工具:

  • Lerna核心概念
    • 包(Package):仓库中的单个npm包
    • 版本管理:统一管理所有包的版本
    • 发布流程:协调多个包的发布
  • Lerna模式
    • Fixed模式:所有包使用相同的版本号
    • Independent模式:每个包有独立的版本号
  • Lerna命令
    • lerna bootstrap:安装依赖
    • lerna run:运行脚本
    • lerna version:更新版本
    • lerna publish:发布包

工作空间结构

Monorepo工作空间的合理结构:

  • 应用目录:存放可独立部署的应用
  • 库目录:存放可重用的代码库
  • 配置目录:存放工作空间级别的配置
  • 工具目录:存放构建和开发工具
  • 文档目录:存放项目文档

依赖管理

Monorepo中的依赖管理策略:

  • 根级依赖:所有项目共享的依赖
  • 项目级依赖:特定项目需要的依赖
  • peerDependencies:对等依赖
  • 依赖版本一致性:确保所有项目使用相同版本的依赖
  • 依赖分析:识别和管理依赖关系

代码共享

Monorepo中的代码共享实现:

  • 共享库:创建可重用的代码库
  • 类型定义:共享TypeScript类型
  • 工具函数:共享通用工具
  • 配置文件:共享配置
  • UI组件:共享前端组件

实践案例

使用Nx创建NestJS Monorepo

步骤1:初始化Nx工作空间

# 使用Nx CLI创建工作空间
npx create-nx-workspace@latest

# 选择工作空间类型
# 选择 "nest" 选项创建NestJS工作空间

# 输入工作空间名称
# 例如:nestjs-monorepo

# 选择包管理器
# 例如:npm, yarn, pnpm

步骤2:查看工作空间结构

nestjs-monorepo/
  ├── apps/                  # 应用目录
  │   └── api/               # 主API应用
  │       ├── src/           # 应用源代码
  │       ├── jest.config.js # 测试配置
  │       ├── tsconfig.json  # TypeScript配置
  │       └── tsconfig.app.json
  ├── libs/                  # 库目录
  │   └── api/               # API相关库
  │       └── interface/     # 接口定义库
  ├── tools/                 # 工具目录
  ├── nx.json               # Nx配置
  ├── package.json          # 根包配置
  ├── tsconfig.json         # 根TypeScript配置
  ├── tsconfig.base.json    # 基础TypeScript配置
  └── .gitignore            # Git忽略配置

步骤3:创建新应用

# 创建新的NestJS应用
nx generate @nrwl/nest:app user-service

# 创建新的Express应用
nx generate @nrwl/express:app admin-panel

# 创建新的React应用
nx generate @nrwl/react:app frontend

步骤4:创建共享库

# 创建数据访问库
nx generate @nrwl/nest:lib data-access --directory=shared

# 创建工具库
nx generate @nrwl/nest:lib utils --directory=shared

# 创建类型定义库
nx generate @nrwl/nest:lib types --directory=shared

# 创建认证库
nx generate @nrwl/nest:lib auth --directory=features

步骤5:运行任务

# 构建单个应用
nx build api

# 测试单个应用
nx test api

# 运行单个应用
nx serve api

# 构建所有应用
nx run-many --target=build --all

# 测试所有应用
nx run-many --target=test --all

# 只构建受影响的项目
nx affected:build

# 只测试受影响的项目
nx affected:test

步骤6:分析依赖图

# 生成依赖图
nx graph

# 生成依赖图并打开浏览器
nx dep-graph

使用Lerna管理多包项目

步骤1:初始化Lerna仓库

# 创建仓库目录
mkdir nestjs-lerna-repo
cd nestjs-lerna-repo

# 初始化Git仓库
git init

# 初始化Lerna
npx lerna init

# 配置Lerna为independent模式(可选)
npx lerna init --independent

步骤2:查看Lerna仓库结构

nestjs-lerna-repo/
  ├── packages/             # 包目录
  ├── lerna.json           # Lerna配置
  ├── package.json          # 根包配置
  └── .gitignore            # Git忽略配置

步骤3:创建包

# 创建核心包
mkdir -p packages/core
cd packages/core
npm init -y

# 创建认证包
mkdir -p packages/auth
cd packages/auth
npm init -y

# 创建用户包
mkdir -p packages/user
cd packages/user
npm init -y

# 创建API包
mkdir -p packages/api
cd packages/api
npm init -y

步骤4:配置包依赖

// packages/api/package.json
{
  "name": "@nestjs-lerna/api",
  "version": "1.0.0",
  "description": "API package",
  "dependencies": {
    "@nestjs-lerna/core": "^1.0.0",
    "@nestjs-lerna/auth": "^1.0.0",
    "@nestjs-lerna/user": "^1.0.0",
    "@nestjs/common": "^9.0.0",
    "@nestjs/core": "^9.0.0"
  }
}

步骤5:安装依赖

# 安装依赖
npx lerna bootstrap

# 使用hoist模式安装依赖(将共享依赖提升到根目录)
npx lerna bootstrap --hoist

# 使用npm workspaces
npx lerna bootstrap --use-workspaces

步骤6:运行命令

# 运行所有包的测试
npx lerna run test

# 运行特定包的测试
npx lerna run test --scope=@nestjs-lerna/api

# 运行所有包的构建
npx lerna run build

# 运行特定包的构建
npx lerna run build --scope=@nestjs-lerna/api

步骤7:版本管理和发布

# 查看更改
npx lerna changed

# 更新版本
npx lerna version

# 发布包
npx lerna publish

# 发布特定版本
npx lerna publish 1.0.1

# 发布预发布版本
npx lerna publish --canary

工作空间配置和优化

步骤1:配置Nx工作空间

// nx.json
{
  "$schema": "./node_modules/nx/schemas/nx-schema.json",
  "affected": {
    "defaultBase": "main"
  },
  "cli": {
    "packageManager": "npm",
    "defaultCollection": "@nrwl/nest"
  },
  "tasksRunnerOptions": {
    "default": {
      "runner": "nx-cloud",
      "options": {
        "cacheableOperations": ["build", "test", "lint"],
        "parallel": 3,
        "canRunInBackground": true
      }
    }
  },
  "targetDefaults": {
    "build": {
      "dependsOn": ["^build"],
      "inputs": ["production", "^production"]
    },
    "test": {
      "inputs": ["default", "^production"]
    },
    "lint": {
      "inputs": ["default", "^default"]
    }
  },
  "namedInputs": {
    "default": ["{projectRoot}/**/*", "sharedGlobals"],
    "production": ["default", "!{projectRoot}/**/*.spec.ts"]
  }
}

步骤2:配置TypeScript

// tsconfig.base.json
{
  "compileOnSave": false,
  "compilerOptions": {
    "rootDir": ".",
    "sourceMap": true,
    "declaration": false,
    "moduleResolution": "node",
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "importHelpers": true,
    "target": "es2015",
    "module": "esnext",
    "lib": ["es2017", "dom"],
    "skipLibCheck": true,
    "skipDefaultLibCheck": true,
    "baseUrl": ".",
    "paths": {
      "@nestjs-monorepo/shared/*": ["libs/shared/*/src"],
      "@nestjs-monorepo/features/*": ["libs/features/*/src"]
    }
  },
  "exclude": ["node_modules", "tmp"]
}

步骤3:配置ESLint

// .eslintrc.json
{
  "root": true,
  "ignorePatterns": ["**/*"],
  "plugins": ["@nrwl/nx"],
  "overrides": [
    {
      "files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
      "parserOptions": {
        "project": ["tsconfig.base.json"]
      },
      "rules": {
        "@nrwl/nx/enforce-module-boundaries": ["error", {
          "enforceBuildableLibDependency": true,
          "allow": [],
          "depConstraints": [
            {
              "sourceTag": "app",
              "onlyDependOnLibsWithTags": ["feature", "data-access", "util"]
            },
            {
              "sourceTag": "feature",
              "onlyDependOnLibsWithTags": ["data-access", "util"]
            },
            {
              "sourceTag": "data-access",
              "onlyDependOnLibsWithTags": ["util"]
            }
          ]
        }]
      }
    }
  ]
}

步骤4:配置Prettier

// .prettierrc
{
  "singleQuote": true,
  "trailingComma": "all",
  "printWidth": 120,
  "tabWidth": 2,
  "semi": true
}

代码示例

共享库示例

// libs/shared/utils/src/lib/logger/logger.service.ts
import { Injectable, Logger as NestLogger } from '@nestjs/common';

@Injectable()
export class LoggerService {
  private logger: NestLogger;

  constructor(context?: string) {
    this.logger = new NestLogger(context);
  }

  log(message: string, context?: string) {
    this.logger.log(message, context);
  }

  error(message: string, trace?: string, context?: string) {
    this.logger.error(message, trace, context);
  }

  warn(message: string, context?: string) {
    this.logger.warn(message, context);
  }

  debug(message: string, context?: string) {
    this.logger.debug(message, context);
  }

  verbose(message: string, context?: string) {
    this.logger.verbose(message, context);
  }
}

// libs/shared/utils/src/lib/logger/logger.module.ts
import { Module, Global } from '@nestjs/common';
import { LoggerService } from './logger.service';

@Global()
@Module({
  providers: [LoggerService],
  exports: [LoggerService],
})
export class LoggerModule {}

// libs/shared/utils/src/index.ts
export * from './lib/logger/logger.service';
export * from './lib/logger/logger.module';

数据访问库示例

// libs/shared/data-access/src/lib/database/database.module.ts
import { Module, Global } from '@nestjs/common';
import { TypeOrmModule, TypeOrmModuleOptions } from '@nestjs/typeorm';
import { ConfigService } from '@nestjs/config';

@Global()
@Module({
  imports: [
    TypeOrmModule.forRootAsync({
      useFactory: (configService: ConfigService) => ({
        type: 'postgres',
        host: configService.get('DB_HOST'),
        port: configService.get('DB_PORT'),
        username: configService.get('DB_USERNAME'),
        password: configService.get('DB_PASSWORD'),
        database: configService.get('DB_NAME'),
        entities: [__dirname + '/**/*.entity{.ts,.js}'],
        synchronize: configService.get('DB_SYNCHRONIZE') === 'true',
        logging: configService.get('DB_LOGGING') === 'true',
      }),
      inject: [ConfigService],
    }),
  ],
  exports: [TypeOrmModule],
})
export class DatabaseModule {}

// libs/shared/data-access/src/index.ts
export * from './lib/database/database.module';

特性库示例

// libs/features/auth/src/lib/auth.service.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { compare } from 'bcrypt';
import { UserService } from '@nestjs-monorepo/features/user';

@Injectable()
export class AuthService {
  constructor(
    private readonly userService: UserService,
    private readonly jwtService: JwtService,
  ) {}

  async validateUser(email: string, password: string) {
    const user = await this.userService.findByEmail(email);
    if (!user) {
      throw new UnauthorizedException('Invalid credentials');
    }

    const isPasswordValid = await compare(password, user.password);
    if (!isPasswordValid) {
      throw new UnauthorizedException('Invalid credentials');
    }

    return user;
  }

  async login(user: any) {
    const payload = { sub: user.id, email: user.email };
    return {
      access_token: this.jwtService.sign(payload),
    };
  }
}

// libs/features/auth/src/lib/auth.module.ts
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { ConfigService } from '@nestjs/config';
import { AuthService } from './auth.service';
import { JwtStrategy } from './strategies/jwt.strategy';
import { LocalStrategy } from './strategies/local.strategy';
import { UserModule } from '@nestjs-monorepo/features/user';

@Module({
  imports: [
    UserModule,
    PassportModule,
    JwtModule.registerAsync({
      useFactory: (configService: ConfigService) => ({
        secret: configService.get('JWT_SECRET'),
        signOptions: {
          expiresIn: configService.get('JWT_EXPIRES_IN'),
        },
      }),
      inject: [ConfigService],
    }),
  ],
  providers: [AuthService, LocalStrategy, JwtStrategy],
  exports: [AuthService],
})
export class AuthModule {}

// libs/features/auth/src/index.ts
export * from './lib/auth.service';
export * from './lib/auth.module';

应用使用共享库示例

// apps/api/src/app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { LoggerModule } from '@nestjs-monorepo/shared/utils';
import { DatabaseModule } from '@nestjs-monorepo/shared/data-access';
import { AuthModule } from '@nestjs-monorepo/features/auth';
import { UserModule } from '@nestjs-monorepo/features/user';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
    }),
    LoggerModule,
    DatabaseModule,
    AuthModule,
    UserModule,
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

// apps/api/src/app.service.ts
import { Injectable } from '@nestjs/common';
import { LoggerService } from '@nestjs-monorepo/shared/utils';
import { AuthService } from '@nestjs-monorepo/features/auth';

@Injectable()
export class AppService {
  constructor(
    private readonly logger: LoggerService,
    private readonly authService: AuthService,
  ) {}

  getHello(): string {
    this.logger.log('Hello endpoint called');
    return 'Hello World!';
  }
}

Lerna包示例

// packages/core/src/index.ts
export * from './lib/core.module';
export * from './lib/core.service';

// packages/core/src/lib/core.service.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class CoreService {
  getCoreMessage(): string {
    return 'Core service message';
  }
}

// packages/core/src/lib/core.module.ts
import { Module, Global } from '@nestjs/common';
import { CoreService } from './core.service';

@Global()
@Module({
  providers: [CoreService],
  exports: [CoreService],
})
export class CoreModule {}

// packages/api/src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3000);
}
bootstrap();

// packages/api/src/app.module.ts
import { Module } from '@nestjs/common';
import { CoreModule } from '@nestjs-lerna/core';
import { AuthModule } from '@nestjs-lerna/auth';
import { UserModule } from '@nestjs-lerna/user';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [CoreModule, AuthModule, UserModule],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

常见问题与解决方案

1. 如何处理Monorepo中的依赖冲突?

解决方案

  • 使用根级依赖管理共享依赖
  • 配置resolutions字段强制依赖版本
  • 使用npm dedupeyarn dedupe整理依赖
  • 定期更新依赖到兼容版本
  • 使用依赖分析工具识别冲突

2. 如何优化Monorepo的构建时间?

解决方案

  • 使用Nx的智能缓存
  • 并行执行构建任务
  • 只构建受影响的项目
  • 优化CI/CD配置
  • 使用增量构建
  • 考虑使用分布式构建系统

3. 如何管理Monorepo中的环境变量?

解决方案

  • 使用根级.env文件管理共享变量
  • 为每个环境创建特定的.env文件
  • 使用dotenv@nestjs/config加载变量
  • 加密存储敏感变量
  • 使用CI/CD变量管理生产环境变量

4. 如何处理Monorepo中的权限管理?

解决方案

  • 使用Git分支保护规则
  • 实现代码审查流程
  • 使用精细化的访问控制
  • 分离敏感项目到独立仓库
  • 建立清晰的贡献指南

5. 如何迁移现有项目到Monorepo?

解决方案

  • 评估项目间的依赖关系
  • 设计合理的工作空间结构
  • 逐步迁移项目,先迁移依赖较少的项目
  • 更新导入路径和依赖引用
  • 测试构建和部署流程
  • 制定回滚计划

互动问答

  1. 什么是Monorepo?它与多仓库(Multirepo)有什么区别?

    Monorepo是将多个项目存储在单个代码仓库中的软件开发策略。

    区别

    • Monorepo:单个仓库包含多个项目,简化依赖管理和代码共享
    • Multirepo:每个项目有独立的仓库,隔离性更好,但依赖管理复杂
    • Monorepo优势:代码共享、统一构建、简化跨项目更改
    • Multirepo优势:更小的仓库体积、更简单的权限管理、更快的克隆速度
  2. Nx和Lerna有什么区别?什么时候应该使用它们?

    Nx

    • 功能丰富的Monorepo管理工具
    • 提供智能缓存、依赖图分析、代码生成等功能
    • 适用于大型企业级项目
    • 支持多种框架和语言

    Lerna

    • 专门用于管理JavaScript多包仓库
    • 专注于版本管理和发布流程
    • 适用于npm包的管理
    • 配置简单,学习曲线平缓

    选择建议

    • 大型NestJS项目:使用Nx
    • 多包npm库:使用Lerna
    • 复杂的前端+后端项目:使用Nx
    • 简单的多包项目:使用Lerna
  3. 如何设计Monorepo的工作空间结构?

    设计工作空间结构的最佳实践:

    • 按类型组织:将应用和库分开存放
    • 按功能分组:将相关的库放在同一目录下
    • 使用一致的命名:遵循统一的命名规范
    • 保持扁平结构:避免过深的目录嵌套
    • 合理划分边界:明确项目间的依赖关系
    • 考虑可扩展性:预留未来项目的空间
  4. 如何实现Monorepo中的代码共享?

    实现代码共享的方法:

    • 创建共享库:将可重用代码提取到独立的库中
    • 定义清晰的API:为共享库提供明确的接口
    • 使用TypeScript路径别名:简化导入路径
    • 版本管理:确保共享库的版本一致性
    • 文档化:为共享库提供详细的文档
    • 测试:确保共享库的质量和稳定性
  5. Monorepo的最佳实践有哪些?

    Monorepo的最佳实践:

    • 使用专门的工具:如Nx或Lerna
    • 自动化流程:使用CI/CD自动化构建、测试和部署
    • 设置合理的缓存:减少构建时间
    • 建立代码规范:确保代码质量和一致性
    • 定期清理:移除未使用的代码和依赖
    • 监控性能:关注构建时间和仓库大小
    • 培训团队:确保团队成员理解Monorepo工作流程

总结

Monorepo管理是现代NestJS项目开发中的重要策略,通过本教程的学习,你应该能够:

  1. 理解Monorepo的基本概念和优势
  2. 使用Nx创建和管理NestJS Monorepo
  3. 使用Lerna管理多包项目
  4. 设计合理的工作空间结构
  5. 实现依赖共享和代码共享
  6. 优化Monorepo的构建和测试流程
  7. 解决Monorepo中的常见问题

通过合理使用Monorepo管理策略,你可以显著提高项目的开发效率,促进代码重用,简化依赖管理,并确保代码质量和一致性。选择适合你项目规模和团队需求的Monorepo工具,并遵循最佳实践,可以让你的NestJS项目开发更加高效和可维护。

« 上一篇 CLI插件开发 下一篇 » 错误处理策略