创建 monorepo

由来

传统的项目被称为单仓库巨石应用,因为它只有一个代码仓库。随着时间和业务的迭代,这个代码库变得越来越复杂,最终像一块巨石一样,导致开发和构建的效率因其“重量”而下降。

为了提高效率,团队开始将这块巨石“敲碎”,将不同的业务碎片存储在多个代码仓库中,这样就形成了多仓库多模块应用。每个模块可以独立开发和构建,从而提升了整体效率。

然而,随着时间的推移,曾经的碎片也会重新聚合成新的“巨石”。虽然可以再次将其拆分,但这样会显著提高管理成本,因为碎片数量过多,且它们可能再次增长为巨石。此外,大量仓库的管理也会导致开发和构建效率的下降。

为了解决这一问题,出现了单仓库多模块应用的概念,将所有模块存储在一个仓库中。这种方式结合了单仓库的便捷性和多模块的灵活性,开发团队不再需要在多个仓库间切换,同时可以根据不同业务需求独立开发和构建。

但单仓库多模块应用也并非没有缺点。首先,由于所有模块集中在一个仓库中,对权限管理、代码审查和测试流程的要求会更加严格。这需要团队制定清晰的规范,以确保各模块之间的协作顺畅,并降低潜在的管理复杂性。

因此,虽然单仓库多模块应用在灵活性和效率上提供了优势,但也需要认真考量如何平衡管理复杂性与开发效率,以确保项目的长期可持续性。

创建

这里使用的是 lerna + pnpm 的组合。

  • Lerna:Lerna是一种用于管理单仓库中多个包的工具,擅长处理多模块项目的依赖管理和发布流程。它支持模块间的共享依赖和独立版本控制,使项目的开发和构建流程更加灵活。
  • pnpm:相比其他包管理工具,pnpm能更高效地管理依赖。它通过硬链接实现依赖共享,减少磁盘占用并加快安装速度。此外,pnpm还具有严格的模块隔离特性,可以避免模块间不必要的依赖冲突。

初始化项目

首先,创建新的项目目录并进入其中:

mkdir NewMonoRepo
cd NewMonoRepo

接下来,初始化一个新的 pnpm项目:

pnpm init

然后,安装 lerna作为开发依赖:

pnpm add lerna -D

如果这一步失败,确保先创建 pnpm-workspace.yaml文件,然后运行以下命令初始化 lerna

npx lerna init

创建工作区

在项目根目录下新建 pnpm-workspace.yaml 文件,定义工作区的模块位置:

`packages:
  # 模块存储于 NewMonoRepo/packages 下
  - "packages/*"`

然后配置 lerna.json 文件,增加以下选项以指定使用 pnpm

`{
  "npmClient": "pnpm",
  "version": "independent"
}`

创建子模块

packages 目录下创建子模块。可以手动创建文件夹,或使用命令创建模块:

mkdir packages/module-a
cd packages/module-a
pnpm init

# 也可以直接使用 lerna create <PackageName> 创建包

为每个模块配置各自的 package.json文件。

安装依赖和管理模块

前面提到了,可以手动创建包,也可以通过 lerna create <package> 创建,但是一般还是通过手动,或自行编写脚本,因为 lerna create <package> 生成的包的模板可能并不是我们需要的。(我翻了 lerna issues,好像是不支持自定义模板,作者的意思大概是这个应该交给其他工具实现)。

以下是一个简单的创建脚本。

const fs = require("fs")
const path = require("path")
const process = require("process")

const projectName = process.argv[/** node position, file position, */ 2 /** create project name */]
const packageJSON = {
  name: `projectName`,
  version: "0.0.0",
  description: '',
  author: "inksha",
  homepage: "",
  license: "MIT",
  main: "src/index.ts",
  publishConfig: {
    registry: "https://registry.npmmirror.com",
  },
  repository: {
    type: "git",
    directory: `packages/${projectName}`,
  },
  scripts: {
    dev: "ts-node ./src/index.ts",
    check: "biome check --fix",
    lint: "biome lint --fix",
    format: "biome format --fix",
  },
  bugs: {},
}

const tsconfig = { extends: "../../tsconfig.json" }

const dir = `packages/${projectName}`

const mainFileContent = `export const ${projectName} = () => "hello world"`

const testFileContent = `import { ${projectName} } from '../src'
describe("test ${projectName}", () => {
  test("hello world", () => {
    expect(${projectName}()).toBe("hello world")
  })
})`

fs.mkdirSync(dir)
fs.mkdirSync(path.join(dir, "src"))
fs.mkdirSync(path.join(dir, "test"))

fs.writeFileSync(path.join(dir, "package.json"), JSON.stringify(packageJSON, undefined, 2), "utf-8")
fs.writeFileSync(path.join(dir, "tsconfig.json"), JSON.stringify(tsconfig, undefined, 2), "utf-8")
fs.writeFileSync(path.join(dir, "src", "index.ts"), mainFileContent, "utf-8")
fs.writeFileSync(path.join(dir, "test", "index.test.ts"), testFileContent, "utf-8")

将其配置到根目录的 package.json 中。

{
  "scripts": {
    "new": "node ./create.js" // 这个存放的位置可以自己自定义
  }
}

之后执行 pnpm new test 就可以创建 packages/test 这个包了。

安装依赖

依赖有两种安装方式,一个是安装在子包,一种则是安装在项目根目录。

一般共用依赖都安装在项目根目录下,比如 typescript, @types/node, @types/jest, jest 等等。

通过 pnpm add typescript jest @types/node @types/jest -w -D 即可安装在根目录。

而安装在子包内的则需要切换目录到子包内,正常运行 pnpm add 即可。

如果需要安装其他子包,则使用 pnpm add <子包名称> --workspace 安装。

package.json 中会出现:

{
  "dependencies": {
    "common": "workspace:^", // workspace 将在发布时被替换为具体版本
  }
}

开发

就是正常开发,只是之前是单仓库单项目,这次是单仓库多项目而已。

运行任务

可以通过 lerna, 也可以通过 pnpm 执行。

通过 pnpm 就是 pnpm --filter <匹配包名,支持通配符> <执行命令>

通过 lerna 就是 lerna run <执行命令> 。 默认是对所有包执行(前提是有这个命令), 可以通过过滤器选项 (lerna run --help 查看) 精确匹配包。

构建

构建其实没有什么太大变化,具体需要根据使用工具来变。

比如 tsc ,在包目录下增加 tsconfig.json 引用根目录配置。然后执行 tsc -v -b packages 就能将所有子包打包到根目录的 dist 目录下 (如果没有其他配置的话)。


创建 monorepo
http://localhost:8080/archives/chuang-jian-monorepo
作者
inksha
发布于
2024年11月01日
许可协议