当你的项目多得像一盘散沙,是时候请出 Monorepo 这位“收纳大师”了!
1 你的“开发桌面”是否也乱成了一锅粥?
想象一下这个场景:
你的桌面上摊着好几个项目——用Go写的后端、用Python跑的 AI 模型、iOS和Android两个App、一个React前端,外加微信小程序和公众号……
每个项目都是一个独立的文件夹(Git 仓库),每次想做个涉及前后端的修改,你都得像个DJ一样,在好几个窗口之间疯狂“切碟”。时间久了,不仅心累,还容易出错。
如果你对这个场景感同身受,那么恭喜你,你遇到了一个“甜蜜的烦恼”,而Monorepo就是帮你解决这个烦恼的“收纳魔法”。本文将带你从零开始,使用Nx+pnpm这对黄金搭档,亲手打造一个能容纳Go、React等多语言项目的Monorepo工作区。
2 Monorepo:管理“项目动物园”的神器
把所有项目都放进一个仓库(Monorepo),听起来有点疯狂,但它带来的好处会让你大呼“真香”:
-
上帝视角,一目了然 再也不用在几十个浏览窗口或IDE窗口间反复横跳了。所有代码都在一个地方,整个技术版图清清楚楚,管理起来心情都变好了。
-
代码共享,如丝般顺滑
- 前后端“心灵相通”:后端 Go 定义了一个数据结构(DTO),前端 React 想用?在 Monorepo 里,你只需创建一个
libs/types共享库,前后端直接引用就行。告别了过去需要发 npm 包或 Go module,然后两边再小心翼翼地更新版本的繁琐流程。 - 跨平台“组件复用”:未来小程序和 App 想共享一套工具函数或业务逻辑?在 Monorepo 架构下,这种共享是天生的优势,而非后期的“技术改造”。
- 前后端“心灵相通”:后端 Go 定义了一个数据结构(DTO),前端 React 想用?在 Monorepo 里,你只需创建一个
-
提交与重构,指哪打哪
- 原子化提交 (Atomic Commits):想象你改了一个后端 API,需要同时更新 Go 服务端、React 前端和 Android App。在 Monorepo 里,这一切可以在一个 commit 中完成!这意味着你的代码库在任何一个历史节点都是一致且可运行的,彻底告别了“A 仓库更新了,B 仓库还没跟上,项目挂了”的尴尬。
- “地毯式”重构:想给某个核心函数改个名?在 IDE 里直接全局搜索替换,从后端到前端,一键搞定,安全又高效。
-
CI/CD,精准而高效
你可以配置一套统一的构建部署流水线。更酷的是,借助
Nx这样的智能工具,当你只改了 React 前端的代码时,CI/CD 会只构建和部署前端应用,后端、App 等项目纹丝不动,大大节省了时间和计算资源。
3 “神兵利器”:Nx + PNPM
- Nx: 我们的“项目总管家”,它能理解项目之间的依赖关系,实现智能的构建、测试和缓存。
- PNPM: 高效的包管理器,它的
workspace特性是实现 Monorepo 的天然盟友。
给总管家发“上岗证”
在动手之前,先在你的电脑上全局安装Nx。这就像在管理仓库前先安装Git一样。
pnpm add --global nx
安装gh
注意我使用nx+pnpm,需要提前安装 gh,否则会出现问题。
brew install gh
sudo apt install gh -y
4 实战演练:从零到一,打造你的多语言Monorepo
4.1 开辟鸿蒙,创建工作空间
我将使用create-nx-workspace脚手架来初始化项目。它很聪明,会自动检测到你安装了pnpm。
# `just-do-it` 是你的项目名,可以随心所欲地替换
# `--preset=apps` 表示创建一个空的、适合添加多个独立应用的 workspace
# `--pm=pnpm` 明确指定包管理器
npx create-nx-workspace@latest just-do-it --preset=apps --pm=pnpm
整个过程行云流水,完成后进入just-do-it目录,你的“地基”就已经打好了:
just-do-it/
├── node_modules/ # 依赖库存放地
├── nx.json # Nx 的核心配置文件
├── package.json # 项目的“身份证”
├── pnpm-lock.yaml # 锁定依赖版本
└── README.md
温馨提示:一些旧教程可能会说此时会自动创建apps目录和pnpm-workspace.yaml文件,新版本并不会。别担心,新版本下我们自己动手,丰衣足食!
4.2 Git远程地址从HTTPS切换到SSH
如果在GitHub推送代码时觉得 HTTPS 方式太慢或需要频繁输入密码,可以切换到更高效的SSH方式。
# 查看你当前的远程地址
git remote -v
# 将 origin 的地址替换成你的 SSH 地址
git remote set-url origin git@github.com:YourUsername/YourRepoName.git
# 例如:
git remote set-url origin git@github.com:finnley/just-do-it.git
4.3 功能分区(可选)
在根目录下,创建两个核心目录:
mkdir apps libs
apps/: “成品展示区”。这里存放所有可以独立部署的应用,比如你的 React 前端、Go 后端服务、Android App 等。libs/: “共享工具箱”。这里存放所有可复用的代码库,比如共享的工具函数、公共 UI 组件、通用类型定义等。
配置 pnpm Workspace。在项目根目录下,确保你有一个 pnpm-workspace.yaml 文件,告诉 pnpm 它的管辖范围。如果没有,就创建一个:
# pnpm-workspace.yaml
packages:
- 'apps/*'
- 'libs/*'
4.4 让第一个“居民”——Go后端服务入驻
1、创建Go项目的“家”
由于后端可能采用微服务架构,我们先在apps里为所有后端服务建一个总目录backend,如果没有创建apps目录,可以直接在根目录下新建backend目录。
mkdir backend && cd backend
# 初始化 Go Workspace,让多个 Go 微服务能和谐共处
go work init
此时会在 backend 目录下生成一个名为 go.work 文件,内容如下:
go 1.25.6
2、创建第一个微服务 ecmp
# 创建项目目录
mkdir ecmp && cd ecmp
# 初始化 Go Module
go mod init einscat.com/ecmp
3、登记服务到Go Workspace
# 回到上层backend目录
cd ..
go work use ./ecmp
此时go.work文件内容更新如下:
go 1.25.6
use ./ecmp
现在,backend目录下的go.work文件记录了ecmp的存在,方便统一管理。
4、启动一个简单的Gin服务
为了验证,我们新建 ecmp/main.go 文件,并于其中写入一个“Hello World”级别的服务:
package main
import (
"log"
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
// Create a Gin router with default middleware (logger and recovery)
r := gin.Default()
// Define a simple GET endpoint
r.GET("/ping", func(c *gin.Context) {
// Return JSON response
c.JSON(http.StatusOK, gin.H{
"message": "pong",
})
})
// Start server on port 8080 (default)
// Server will listen on 0.0.0.0:8080 (localhost:8080 on Windows)
if err := r.Run(); err != nil {
log.Fatalf("failed to run server: %v", err)
}
}
5、Go项目在Monorepo里“上户口”
现在Go项目已经存在了,但我们的“总管家”Nx还不知道,所以我们需要为Go项目创建一个“身份证”——project.json 文件。
在backend/ecmp目录下新建project.json文件:
{
"name": "ecmp",
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
"root": "backend/ecmp",
"projectType": "application",
"tags": ["scope:backend", "lang:go"],
"targets": {
"build": {
"executor": "nx:run-commands",
"options": {
"command": "cd {projectRoot} && go build -o ../../dist/ecmp ."
},
"outputs": ["{workspaceRoot}/dist/ecmp"]
},
"serve": {
"executor": "nx:run-commands",
"options": {
"command": "cd {projectRoot} && go run ."
}
},
"test": {
"executor": "nx:run-commands",
"options": {
"command": "cd {projectRoot} && go test ./..."
}
}
}
}
这张“身份证”告诉了Nx关于ecmp的一切:它的名字、位置、以及如何构建(build)、**运行(serve)和测试(test)**它。outputs 字段尤其重要,它告诉 Nx 构建产物的位置,以便启用缓存。
可选:将 backend/ecmp 注册到 pnpm-workspace.yaml,让pnpm帮助管理依赖,这一步当然也可以在最初创建项目的时候添加进去。
# pnpm-workspace.yaml
packages:
- 'apps/*'
- 'apps/backend/*'
- 'libs/*'
6、验明正身
运行nx show projects,看看Nx是否已经认出了我们的新成员:
➜ just-do-it git:(main) ✗ nx show projects
ecmp
成功!或者,用更酷的方式,打开项目依赖图看看:
nx graph
7、启动项目,感受Monorepo的魔力
点火,启动!回到项目根目录,用Nx的命令来启动我们的Go服务:
nx serve ecmp
看到Gin服务的启动日志了吗?恭喜,你的第一个非JS项目已成功融入Monorepo!
4.5 如法炮制,添加 React 前端项目
1、手动创建Vite + React项目
使用vite新建一个fe-ecmp的前端项目:
cd just-do-it
mkdir -p frontend/web && cd frontend/web
# 根据提示操作,项目名称输入 fe-ecmp,选择 React 和 TypeScript
pnpm create vite@latest
操作完成后,frontend/web 目录下会多出一个 fe-ecmp 项目文件夹。
注意:如果之前选择了创建apps目录,所有的目录都需要基于apps目录,比如“/apps/frontend/web”
2、为前端项目“上户口”
同样,在frontend/web/fe-ecmp目录下新建project.json文件:
{
"$schema": "../../../../node_modules/nx/schemas/project-schema.json",
"name": "fe-ecmp",
"root": "frontend/web/fe-ecmp",
"sourceRoot": "frontend/web/fe-ecmp/src",
"projectType": "application",
"tags": ["scope:frontend", "framework:react"],
"targets": {
"serve": {
"executor": "nx:run-commands",
"options": {
"command": "pnpm dev",
"cwd": "{projectRoot}"
}
},
"build": {
"executor": "nx:run-commands",
"options": {
"command": "pnpm build",
"cwd": "{projectRoot}"
},
"outputs": ["{projectRoot}/dist"]
},
"preview": {
"executor": "nx:run-commands",
"options": {
"command": "pnpm preview",
"cwd": "{projectRoot}"
}
},
"lint": {
"executor": "nx:run-commands",
"options": {
"command": "pnpm lint",
"cwd": "{projectRoot}"
}
}
}
}
这里的配置和Go项目类似,只是把targets里的命令换成了pnpm dev、pnpm build等前端专用命令。
3、统一安装所有依赖
将fe-ecmp注册到 pnpm-workspace.yaml,让pnpm帮助管理依赖,这一步当然也可以在最初创建项目的时候添加进去,如果之前没有没有穿件该文件这里也可以创建,根据实际情况修改apps目录与package选项。
# pnpm-workspace.yaml
packages:
- 'apps/*'
- 'apps/backend/*'
- 'apps/frontend/web/*'
- 'libs/*'
或者
# pnpm-workspace.yaml
packages:
- 'frontend/web/*'
回到Monorepo的根目录,运行pnpm install。pnpm会自动扫描所有apps/*和libs/*下的package.json,并将所有依赖项安装到根目录的node_modules中。
# 确保在 just-do-it/ 根目录下
pnpm install
这里切记如果目录过深,pnpm-workspace.yaml 需要配置到项目目录,否则前端无法执行下面命令。
4、用Nx统一指挥前端项目
现在,我就可以像指挥官一样,在根目录用同样简洁的Nx命令来操作前端项目了:
# 启动开发服务器
nx serve fe-ecmp
# 构建生产包
nx build fe-ecmp
# 检查代码格式
nx lint fe-ecmp
5、为前端指定项目安装依赖
这个操作需要在Monorepo的根目录下执行,而不是进入到前端项目的子目录中。场景步骤如下:
假设我要为名为fe-ecmp的前端项目安装axios这个库:
首先打开终端,并确保我位于Monorepo的根目录(即just-do-it目录下),执行以下命令:
pnpm add axios --filter fe-ecmp
- pnpm add
: 这是 pnpm 添加依赖的标准命令。 - –filter
: 这是关键部分。它告诉 pnpm,这个命令只针对名为 web-frontend 的这个项目生效。项目名称是在其 package.json 文件里的 “name” 字段定义的。
如果我想安装一个开发依赖(devDependency),可以添加-D标志:
pnpm add typescript -D --filter fe-ecmp
为什么这样做?
在 pnpm 工作空间(workspace)中:
- 集中管理:所有项目的依赖最终都会被安装到根目录的 node_modules 文件夹中,并通过符号链接(symlinks)的方式供各个项目使用,这极大地节省了磁盘空间。
- 精确操作:使用 –filter 标志可以确保依赖被正确地添加到 apps/web-frontend/package.json 文件中,而不是错误地加到根目录的 package.json 或其他项目中。
- 维护一致性:执行命令后,pnpm 会自动更新根目录下的 pnpm-lock.yaml 文件,以保证整个 Monorepo 中所有依赖版本的一致性和锁定。
恭喜你!你已经成功掌握了搭建和管理一个多语言 Monorepo 的核心技能。现在,去把你那盘散沙般的项目都收纳进这个强大的工作区吧!
4.6(可选)将已有的Git仓库导入Monorepo
如果我想把一个已存在的独立项目(比如 Observatory)迁移到Monorepo的backend目录下,git subtree是个不错的选择。
# 1. (可选) 添加一个远程别名,方便操作
git remote add backend_repo_local /path/to/your/existing/Observatory
# 2. 使用 subtree 将对方的 main 分支添加到指定前缀目录下
git subtree add --prefix=apps/backend-core backend_repo_local main
# 3. (可选) 移除远程别名
git remote remove backend_repo_local
4 更新日志
- 2026-01-25:移除apps目录,默认创建的monorepo项目指定–preset=apps参数也不会默认创建指定目录