什么是 Monorepo?
Monorepo(单体仓库)是一种软件工程中的版本控制系统,它将多个项目或模块放在一个单一的仓库中管理,而不是分散在多个独立的仓库中。这种模式在现代软件开发中越来越受欢迎,因为它提供了几个显著的优势:
- 简化的依赖管理:Monorepo 允许跨项目的依赖关系更容易管理,因为所有代码都位于同一仓库中。
- 统一的构建和测试:Monorepo 使得整个代码库的构建和测试更加一致和集中。
- 更好的代码共享:Monorepo 促进了代码和工具的共享,有助于减少重复代码。
- 简化的代码审查:开发者可以在一个上下文中审查跨多个项目的代码变更,这有助于保持代码的一致性和质量。
- 更高效的协作:团队成员可以更容易地协作和理解整个系统的代码结构。
使用 pnpm 创建 TypeScript Monorepo
pnpm
支持 workspaces
功能,允许你在一个仓库中管理多个包。以下是如何使用 pnpm
创建和管理 Monorepo 的基本步骤:
-
初始化 Monorepo
创建一个新的目录,并初始化一个新的
pnpm
工作区:mkdir pnpm-mono cd pnpm-mono pnpm init -y
修改生成 package.json 中的 name 字段为
@ell/monorepo
,@ell
部分是你的公司或组织名称package.json{ "name": "@ell/monorepo", "version": "1.0.0" }
-
配置 workspace
在
pnpm-mono
目录下定义工作区,并使用 tsup 作为 TypeScript 打包构建工具,使用 biome 作为代码格式化工具, 使用 tsx 作为 TypeScript 开发环境运行工具。{ "name": "@ell/monorepo", "version": "1.0.0", "scripts": { "preinstall": "npx only-allow pnpm", "build": "pnpm --filter \"./packages/**\" run build", "clean": "pnpm --filter=@elljs/* run clean", "format": "pnpm biome format --write ." }, "devDependencies": { "@biomejs/biome": "^1.9.4", "rimraf": "^6.0.1", "tsup": "^8.3.5", "tsx": "^4.19.2" } }
.npmrc
中的配置:link-workspace-packages = true
- 当设置为
true
时,这个选项会让pnpm
自动将工作区中的包相互链接。也就是说,当一个包依赖于工作区中的另一个包时,pnpm
会创建一个符号链接(symlink),使得一个包可以直接引用另一个包的本地版本,而不是安装来自远程仓库的版本。这样可以加快开发速度,因为不需要重复下载和安装相同的代码。
prefer-workspace-packages = true
- 这个选项指示
pnpm
在安装依赖时,优先选择工作区中的包而不是远程仓库中的包。如果工作区中存在与远程仓库相同版本的包,pnpm
将使用工作区中的版本。这有助于确保开发和生产环境中使用的包版本一致,并且可以减少对外部仓库的依赖。
recursive-install = true
- 当设置为
true
时,这个选项会让pnpm
在安装依赖时递归地检查所有子工作区(sub-workspaces)的依赖。这意味着pnpm
将不仅安装当前工作区的依赖,还会检查并安装所有子工作区的依赖。这有助于确保整个 monorepo 结构中的所有包都正确安装了它们的依赖。
- 当设置为
-
创建包
在
packages
目录下创建不同的包:cd packages mkdir package-a package-b
package-a 和 package-b 包的 package.json 文件中的 name 字段分别为
@ell/package-a
和@ell/package-b
package-a 中的文件如下:
{ "name": "@ell/package-a", "version": "1.0.0", "type": "module", "source": "./src/index.ts", "types": "./dist/index.d.ts", "sideEffects": false, "exports": { ".": { "import": "./dist/index.js", "require": "./dist/index.cjs", "default": "./dist/index.js" } }, "files": ["dist"], "scripts": { "build": "tsup --config ../../tsup.config.ts", "clean": "rimraf dist && rimraf node_modules" }, "devDependencies": { "typescript": "^5.7.2" } }
package-b 中的文件如下:
{ "name": "@ell/package-b", "version": "1.0.0", "type": "module", "source": "./src/index.ts", "types": "./dist/index.d.ts", "sideEffects": false, "exports": { ".": { "import": "./dist/index.js", "require": "./dist/index.cjs", "default": "./dist/index.js" } }, "files": ["dist"], "scripts": { "build": "tsup --config ../../tsup.config.ts", "clean": "rimraf dist && rimraf node_modules" }, "devDependencies": { "typescript": "^5.7.2" } }
在
package.json
文件中定义了与模块相关的配置,以下是每个字段的详细解释:-
“type”: “module”
- 这个字段指定包支持 ES 模块。它告诉 Node.js 和其他环境,所有的模块都是 ECMAScript 模块,这意味着你可以使用
import
和export
语法。
- 这个字段指定包支持 ES 模块。它告诉 Node.js 和其他环境,所有的模块都是 ECMAScript 模块,这意味着你可以使用
-
“source”: ”./src/index.ts”
- 这个字段指定了 TypeScript 编译器的入口文件。它告诉编译器从
./src/index.ts
文件开始编译。
- 这个字段指定了 TypeScript 编译器的入口文件。它告诉编译器从
-
“types”: ”./dist/index.d.ts”
- 这个字段指定了类型定义文件(
.d.ts
)的位置。当你安装这个包时,TypeScript 会查找这个文件来提供类型信息。这里指定的是./dist/index.d.ts
。
- 这个字段指定了类型定义文件(
-
“sideEffects”: false
- 这个字段告诉打包工具(如 webpack、Rollup)这个包没有任何副作用(即不执行任何代码,除了导出)。这允许打包工具在优化时排除没有被引用的代码,从而减小最终包的大小。
-
“exports”:
- 这个字段定义了模块的导出方式。它允许你指定不同条件下的入口点。
"."
: 表示默认导出和命名导出的配置。"import": "./dist/index.js"
: 指定了当使用import
语法时,应该加载的文件。"require": "./dist/index.cjs"
: 指定了当使用require
语法时,应该加载的文件。这对于 CommonJS 模块兼容性很重要。"default": "./dist/index.js"
: 指定了默认导出的文件。
-
“files”: [“dist”]
- 这个字段定义了当你的包被安装时,哪些文件应该被包含在内。这里指定的是只包含
dist
目录下的文件。
- 这个字段定义了当你的包被安装时,哪些文件应该被包含在内。这里指定的是只包含
这些字段共同定义了包的结构、兼容性和构建行为,使得包可以在不同的环境下正确地被构建和使用。通过精确控制导出和文件包含,你可以确保你的包在不同的模块系统中都能正常工作,并且提供良好的类型支持。
-
-
创建应用
在
apps
目录下创建应用:cd apps mkdir api
安装 package-a 和 package-b:
pnpm add @ell/package-a @ell/package-b
api 中的文件如下:
{ "name": "@ell/api", "version": "1.0.0", "scripts": { "build": "tsc ./src/index.ts", "dev": "tsx ./src/index.ts", "clean": "rimraf dist && rimraf node_modules" }, "dependencies": { "@ell/package-a": "workspace:^", "@ell/package-b": "workspace:^" }, "devDependencies": { "typescript": "^5.7.2" } }
完整的目录结构如下:
- index.ts
- package.json
- biome.json
- tsconfig.json
- index.ts
- package.json
- biome.json
- tsconfig.json
- index.ts
- package.json
- biome.json
- tsconfig.json
- .npmrc
- biome.json
- package.json
- pnpm-workspace.yaml
- tsup.config.ts
-
构建
在根目录执行
pnpm install
命令, 然后执行pnpm build
命令。(base) ➜ pnpm-mono pnpm build > @ell/monorepo@1.0.0 build /Users/roylin/Desktop/ell/pnpm-mono > pnpm --filter "./packages/**" run build Scope: 2 of 4 workspace projects packages/package-a build$ tsup --config ../../tsup.config.ts [7 lines collapsed] │ ESM dist/index.js 86.00 B │ ESM dist/index.js.map 182.00 B │ ESM ⚡️ Build success in 10ms │ CJS dist/index.cjs 567.00 B │ CJS dist/index.cjs.map 285.00 B │ CJS ⚡️ Build success in 10ms │ DTS Build start │ DTS ⚡️ Build success in 335ms │ DTS dist/index.d.cts 48.00 B │ DTS dist/index.d.ts 48.00 B └─ Done in 1.3s packages/package-b build$ tsup --config ../../tsup.config.ts [7 lines collapsed] │ ESM dist/index.js 86.00 B │ ESM dist/index.js.map 182.00 B │ ESM ⚡️ Build success in 10ms │ CJS dist/index.cjs 567.00 B │ CJS dist/index.cjs.map 285.00 B │ CJS ⚡️ Build success in 10ms │ DTS Build start │ DTS ⚡️ Build success in 327ms │ DTS dist/index.d.cts 48.00 B │ DTS dist/index.d.ts 48.00 B └─ Done in 1.3s
-
运行
进入 apps/api 目录执行执行
pnpm dev
运行应用。(base) ➜ api pnpm dev > @ell/api@1.0.0 dev /Users/roylin/Desktop/ell/pnpm-mono/apps/api > tsx ./src/index.ts package-a package-b
源码地址:https://github.com/elljs/pnpm-mono
使用 Maven 创建 Java Monorepo
-
初始化 Monorepo
创建一个Monorepo项目。
mkdir maven-mono cd maven-mono # 在根目录下创建 pom.xml touch pom.xml # 在 api 目录中创建 pom.xml mkdir api && touch api/pom.xml # 在 packagea 目录中创建 pom.xml mkdir packagea && touch packagea/pom.xml # 在 packageb 目录中创建 pom.xml mkdir packageb && touch packageb/pom.xml
根目录中的 pom.xml 文件内容如下:
<project> <!-- 指定 Maven POM 模型的版本 --> <modelVersion>4.0.0</modelVersion> <!-- 项目的基本信息 --> <groupId>com.ell</groupId> <!-- 项目的组 ID,通常用于唯一标识项目或组织 --> <artifactId>maven-monorepo</artifactId> <!-- 项目的 artifact ID,用于在组内部唯一标识项目 --> <version>1.0.0-SNAPSHOT</version> <!-- 项目的版本号,SNAPSHOT 表示这是一个快照版本,可能还在开发中 --> <packaging>pom</packaging> <!-- 打包类型,这里为 POM,表示这是一个父 POM,用于管理子模块 --> <!-- 列出此 POM 文件管理的子模块 --> <modules> <module>packagea</module> <!-- 子模块 packagea 的目录名 --> <module>packageb</module> <!-- 子模块 packageb 的目录名 --> <module>api</module> <!-- 子模块 api 的目录名 --> </modules> <!-- 依赖管理部分,用于定义项目中使用的依赖项的版本,以便在子模块中引用而不需要重复版本号 --> <dependencyManagement> <dependencies> <dependency> <groupId>com.ell</groupId> <!-- 依赖项的组 ID --> <artifactId>monorepo</artifactId> <!-- 依赖项的 artifact ID --> <version>1.0.0</version> <!-- 依赖项的版本号,注意这里应该是一个已发布的稳定版本,而不是 SNAPSHOT,除非你有特殊需求 --> </dependency> </dependencies> </dependencyManagement> </project>
packagea 中的文件,内容如下:
package com.ell.packagea; public class PackageA { public void methodA() { System.out.println("Method A from Package A"); } }
packageb 中的文件,内容如下:
package com.ell.packageb; public class PackageB { public void methodB() { System.out.println("Method B from Package B"); } }
api 中的文件,内容如下:
package com.ell.api; import com.ell.packagea.PackageA; import com.ell.packageb.PackageB; public class Api { public static void main(String[] args) { PackageA packageA = new PackageA(); packageA.methodA(); PackageB packageB = new PackageB(); packageB.methodB(); } }
完整的目录结构如下:
- Api.java
- pom.xml
- PackageA.java
- pom.xml
- PackageB.java
- pom.xml
- pom.xml
-
构建
执行
mvn clean install
命令,这将编译和打包所有项目,并生成相应的 jar 包和 pom 文件。- mvn
这是 Maven 的命令行工具,用于执行 Maven 的各种命令和生命周期阶段。
- clean
clean
是 Maven 的一个生命周期阶段,位于validate
之后。它的主要目的是清理项目之前构建生成的文件,确保从干净的状态开始新的构建。这通常包括删除target
目录(或你配置的任何其他构建输出目录),该目录通常包含编译后的类文件、打包的 JAR 文件、测试报告等。执行
clean
阶段时,Maven 会调用与清理相关的插件目标。例如,maven-clean-plugin
的clean
目标负责删除构建输出目录。- install
install
是 Maven 的另一个生命周期阶段,位于package
之后,deploy
之前。它的主要目的是将项目构建并打包(如 JAR、WAR 等),然后将这个包安装到本地 Maven 仓库中。这样,其他项目就可以作为依赖项来引用它,而无需将其包含在源代码仓库中。执行
install
阶段时,Maven 会首先执行compile
、test
(如果启用了测试)、package
等阶段,以确保项目已经过编译、测试并打包。然后,它会调用与安装相关的插件目标。例如,maven-install-plugin
的install
目标负责将构建的输出安装到本地 Maven 仓库中。- mvn clean install
当你运行
mvn clean install
命令时,Maven 会首先执行clean
阶段来清理之前的构建输出,然后执行install
阶段来构建项目并将其安装到本地 Maven 仓库中。这个命令非常适合于在开发过程中确保你的项目是从干净的状态开始构建的,并且构建的输出被正确地安装到了本地仓库中,以便其他项目可以引用它。 -
使用插件
在 api/pom.xml 中添加一个 maven 插件,用于执行 java 项目的启动命令。
api/pom.xml<project> <modelVersion>4.0.0</modelVersion> <parent> <groupId>com.ell</groupId> <artifactId>maven-monorepo</artifactId> <version>1.0.0-SNAPSHOT</version> <relativePath>../pom.xml</relativePath> </parent> <artifactId>api</artifactId> <dependencies> <dependency> <groupId>com.ell</groupId> <artifactId>packagea</artifactId> <version>1.0.0-SNAPSHOT</version> </dependency> <dependency> <groupId>com.ell</groupId> <artifactId>packageb</artifactId> <version>1.0.0-SNAPSHOT</version> </dependency> </dependencies> <build> <pluginManagement> <plugins> <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>exec-maven-plugin</artifactId> <version>3.0.0</version> <configuration> <mainClass>com.ell.api.Api</mainClass> </configuration> </plugin> </plugins> </pluginManagement> </build> </project>
-
运行项目
mvn -f api/pom.xml exec:java
- -f api/pom.xml
-f
或--file
选项用于指定要使用的 POM 文件的位置。在这个例子中,它指向api/pom.xml
,这意味着 Maven 将会使用位于api
目录下的pom.xml
文件作为项目的对象模型(POM)。这通常用于多模块项目中的子模块,或者当你想要从一个不是当前工作目录的位置运行 Maven 时。- exec:java
这部分指定了要执行的 Maven 插件目标和/或配置。在这个例子中,它使用
exec:java
插件目标,这意味着 Maven 将会执行exec:java
插件,并且使用配置中指定的mainClass
参数来启动 Java 应用程序。(base) ➜ maven-mono mvn -f api/pom.xml exec:java [INFO] Scanning for projects... [INFO] [INFO] ----------------------------< com.ell:api >----------------------------- [INFO] Building api 1.0.0-SNAPSHOT [INFO] from pom.xml [INFO] --------------------------------[ jar ]--------------------------------- [INFO] [INFO] --- exec:3.0.0:java (default-cli) @ api --- Method A from Package A Method B from Package B [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 0.311 s [INFO] Finished at: 2024-11-29T00:24:57+08:00 [INFO] ------------------------------------------------------------------------