创建 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
目录下 (如果没有其他配置的话)。