引言
对于 Monorepos 架构一直早有耳闻, 但是一直用不上也就懒得去了解, 刚好最近想要基于 prosemirror 写一个自己的富文本编辑器, 基本上是就是参考 tiptap 源码来一步步开发, 一为折腾二为学习。刚好 tiptap 其实就是 Monorepos 架构所以顺便简单研究研究, 故而有了这篇文章。
一、基本概念了解
再开始前先了解几种常见的架构
1.1 单体架构(Monolith)
所谓 Monolith 这个词字面意思是 单块的、整体的, 在软件架构里常用来指 单体架构 模式。
该模式其实就是将所有功能都打包在一个整体里, 作为一个应用进行部署和运行, 它和微服务应该是一个完全相反的一种模式。
- 主要特点就是:
- 单一代码库: 所有功能模块(用户、订单、支付、库存...)都在同一个代码仓库、同一个项目里。
- 单体部署: 打包后只产出一个可执行文件 /
war包 /docker镜像, 直接上线。 - 共享内存与数据库: 所有功能都是部署在一个机器上, 通常共享内存、共用一个数据库。
- 优点
- 开发简单: 没有复杂的服务间通信。
- 部署简单: 一个包就能跑起来, 不用维护一堆微服务。
- 调试方便: 本地起一个服务就能跑全流程。
- 性能好: 模块间是进程内调用, 不用走网络。
- 缺点
- 耦合高: 改一处可能影响全局,难以模块化管理。
- 扩展性差: 无法按模块独立扩容。
- 技术栈受限: 所有功能只能用同一技术栈。
- 发布成本高: 改一行代码也要重新打包发布整个应用。
当然 Monolith 其实和我们要聊的 Monorepos 没啥太大关系, 这里只是顺带了解了解。后面要聊的 multirepo / monorepo 讨论的更多是 代码仓库管理策略。
1.2 多仓库模式(multirepos)
所谓 multirepos 即 Multi-repos 也就是多仓库模式, 说白了就是每个「项目/服务/模块/应用」都是单独放在一个代码仓库里。
当然这些代码仓库如果没有任何关联, 其实也没啥好说点。但是相反有些它们之间可能是有关联的, 甚至有很多业务逻辑都是相通的。比如我们一个项目有 PC 端、后台管理、移动端、小程序、APP 端等等, 甚至有些还有共用的组件库、工具库之类的。
再比如我们上面提到的 prosemirror 其实就是使用 multirepos 架构, 每个功能模块都是以独立仓库的形式存在, 如下图所示:

- 那么
multirepos架构有舍特点呢?
- 一仓一项目: 每个服务、库、
UI组件、工具包都有自己独立的Git仓库。 - 独立版本管理: 每个仓库有自己的版本号、分支、发布流程。
- 强解耦: 一个仓库的变更不直接影响其他仓库。
multirepos架构优点:
- 边界清晰: 不同模块之间独立管理, 没有太多耦合。
- 独立发布: 可以单独更新、发布某个仓库, 而不必影响其他仓库。
- 权限控制简单: 敏感项目可以设置私有仓库, 权限隔离好做。
- 适合多团队合作: 不同仓库(项目)由各自的团队负责, 团队之间互不干扰。
multirepos架构缺点
- 协作成本高: 当某个需求改动需要涉及多个仓库时, 需要多次提
PR、多次发布、如果之间还存在耦合就容易出错。 - 依赖管理麻烦: 仓库之间如果相互依赖, 则比较麻烦没次都需要手动发布版本、升级依赖。
- 工具链碎片化: 每个仓库可能有不同的
lint/build/test配置, 配置之间要做到一致性就比较麻烦。只要有一点调整, 就需要动所有仓库。
1.3 单仓库模式(Monorepos)
而 Monorepo 则是单仓库模式, 顾名思义就是直接将多个项目/服务/模块/包放到同一个仓库进行统一管理, 就好比如我有一个网站, 有前端项目有服务项目, 服务端也是用 JS(Node) 写的, 那我其实就可以将这两个项目放到同一个仓库进行一个管理, 这样的话很多工具函数、脚本、项目配置都是可以复用的。
Monorepos 模式其实就特别一些开源的项目, 上文说到的 tiptap 就是该模式, 一个完整的项目包含了基础的核心模块、还有各种功能扩展模块、同时还需要针对 Rect、Vue、原生 HTML 提供对应的功能包... 同时这些模块之间很多

Monorepos架构特点
- 单个代码仓库: 所有相关项目、包、模块都在一个
Git仓库中进行统一管理。 - 多包结构: 一般使用
packages/、apps/等目录存放多个子项目, 每个子项目可以发布为单独的依赖包。 - 共享依赖与工具链: 可统一使用同一套
lint、build、test、CI等配置。 - 快捷引用: 不同模块之间不需要手动执行
npm link或者发布为npm包, 相互之间就可以直接快速引用。 - 自动化构建与发布: 通常配合工具如
pnpm workspace、lerna、nx、turborepo可以方便快捷的实现依赖管理、构建缓存、按需发布等等。
multirepos架构优点
- 统一管理、提升协作效率: 所有模块在一个仓库内, 统一管理版本、代码规范、
CI/CD流程, 不需要在多个仓库中反复提PR、切换分支。 - 依赖共享, 减少重复安装: 可以通过
workspace(如pnpm/yarn)共享依赖包, 减少磁盘占用、加快安装速度。 - 模块间联动开发方便: 当多个模块有关联时, 可以直接联调, 无需手动发布中间版本, 改动立即生效。
- 一致性更高: 所有子项目共用同一套配置(
eslint、tsconfig、prettier等) 风格统一、维护简单。 - 自动化工具生态完善: 结合
pnpm workspace、lerna、nx、turborepo等工具可轻松实现增量构建、缓存优化、按包发布、版本追踪等高级能力。
multirepos架构缺点
- 仓库体积庞大: 所有模块都在一个仓库中, 代码量和依赖文件如果较多, 仓库就会变得庞大, 首次
clone、安装依赖的成本较高。 - 权限与访问控制困难: 如果不同模块由不同团队进行维护, 那么将这些模块放在一个仓库中在权限划分上就比较麻烦(需用借助额外工具来实现)。
- 构建复杂度提升: 需要配置高效的构建缓存与任务调度系统,否则容易导致全量构建慢。
Git历史和分支管理复杂: 所有改动都在同一仓库中,提交记录庞大,版本回溯或分支策略需要严格规范。
二、Npm Workspaces
npm workspace 是 npm 7 引入的一个新功能, 它允许我们直接在一个单项目中同时管理多个独立的子项目(依赖包), 可以方便的集中管理它们之间的依赖, 减少重复并提升可维护性。
在 Npm 项目中, 我们也正是通过 Npm Workspaces 来实现 Monorepos 架构, 下面我们就简单介绍下 Npm Workspaces 的使用。
2.1 项目初始化
- 首先我们先初始化一个
Npm项目:
sh
npm init -y
初始化完成后, 会在项目跟目录下创建一个 package.json 文件

- 创建子包: 下面我们来创建两个子包
packages/a以及app/b
sh
npm init -y -w packages/a
npm init -y -w app/b
执行上面代码将会:
- 分别在
app和packages目录下初始化两个子包(npm项目) - 同时会在根
package.json中,workspaces配置中添加新的子包 - 同时还会在根目录中, 将子包以软连接的形式安装到
node_modules中

- 手动创建子包: 我们知道了调用
npm init -y -w app/b会做哪些处理, 那么自然, 我们完全也可以自动手动添加子包, 下面我们来新增一个子包packages/c
- 新增目录
packages/c - 在目录
packages/c下, 通过npm init -y初始化一个npm项目 - 根
package.json中,workspaces配置新增配置项packages/c - 最后还需要在根目录下执行
npm install, 目的是为了在node_modules中为每个子包创建新的软连接

2.2 为子包安装依赖包
在上文我们使用 npm init -y -w app/b 添加来子包:
- 该命令中
-w参数用于指定子包, 而-w app/b其实是--workspace=app/b的缩写 npm init -y则是我们要在子包中进行的操作
同理, 如果我们要为子包 app/b 安装依赖 dayjs 就可以直接在项目根目录执行 npm install dayjs -w app/b 即可
sh
npm install dayjs -w app/b
而执行 npm install dayjs -w app/b 会自动完成:
- 在顶层, 也就是根目录安装依赖
dayjs - 同时会更新子包
app/b的package.json, 新增依赖包配置

2.3 使用子包
正如上文所说, 其实当我们执行 npm install 或者通过 npm init -y -w [子包] 初始化子包时, 都会在项目根目录 node_modules 中, 为每个子包创建一个软链接, 如下图所示最右边的箭头表示该依赖包是个软链接

而有了这个软链接, 就可以帮助我们在不同子包中进行相互引用: 如下图所示, 我们在子包 packages/c 中, 直接引用了子包 packages/b 和 app/b

当然这边每个子包的目录名不重要, 重点是每个子包中 package.json 中定义的 name 值, 也就是包名称, 这个包名称我们是可以随意修改的, 只要不重复继续, 包名修改后重新 npm install 即可(更新软链接)

对了, 项目根 package.json 中, workspaces 配置的是子包的路径, 所以只是改子包的名称, 这边是不需要动的。同时这边其实也可以直接使用通配符 *, 如下所示, packages 和 app 目录下的所有项目都将会被作为子包进行加载
diff
{
...
+ "workspaces": [
+ "packages/*",
+ "app/*"
+ ]
}
三、pnpm workspaces
除了使用官方 npm workspaces, 我们还可以使用第三方包管理工具, 比如 yarn 或者 pnpm 它们都实现了各种的一套 workspaces 协议, 下面我们简单介绍下 pnpm workspaces 至于 yarn 就不展开了
3.1 初始化项目
在 pnpm 中初始化相对来说会比较麻烦点, 一切都需要手动操作
- 初始化根项目: 在项目根目录初始化一个 npm 项目
sh
pnpm init
- 创建
pnpm workspace配置文件: 根目录创建配置文件pnpm-workspace.yaml所有和workspace相关的配置都在这边定义

- 手动创建 & 初始化子包: 是的这边子包需要我们手动在对应子包目录下进行创建、初始化(
npm init y)

3.2 添加依赖
- 根目录安装依赖: 对于公用的依赖可以直接在项目根目录中进行安装, 这边可以直接在根目录执行
pnpm add来进行安装, 或者在任意位置(子包、根目录)通过-w参数来安装根依赖包, 这里的-w = workspace root
sh
pnpm add react # 根目录执行
pnpm add react -w # 任意位置执行都行, 会在根目录安装依赖
- 为子包安装依赖: 除了直接在子包内通过执行 pnpm add 来安装项目依赖外, 其实我们还可以使用 --filter 来为子包安装依赖, 通过 --filter 就不限制目录了
sh
pnpm add react # 子包内执行
pnpm add react --filter a # 任意位置执行都行, 通过「--filter a」来为子包「a」安装依赖
3.3 使用子包
在开始前我们需要了解下 workspace: 协议, 该协议是 pnpm 在 monorepo 中用于引用本地 workspace 子包的特殊语法。当我们在 monorepo 中, 一个子包依赖另一个包时, 就可以使用 workspace: 前缀来声明这是一个内部依赖。在 本地开发 时 pnpm 会自动帮我们软链接到本地对应的子包, 而在 发布时 则会自动替换为实际的子依赖包。
如下所示:
- 通过
--filter来为某个子包安装依赖 'c@workspace:*'表示安装workspace:协议的子包c, 需要注意的是这边要加引号''
sh
pnpm add 'c@workspace:*' --filter a

上文用的是 workspace:* 表示使用任意版本, 也就是最新的子包, 自然这边我们也可以限制子包的版本, 规则其实和 npm 依赖包版本号的规则差不多
sh
# 不同的 workspace 版本协议
pnpm --filter pkg-b add 'pkg-a@workspace:*' # 任意版本
pnpm --filter pkg-b add 'pkg-a@workspace:^' # 匹配主版本
pnpm --filter pkg-b add 'pkg-a@workspace:~' # 匹配次版本
pnpm --filter pkg-b add 'pkg-a@workspace:^1.0.0' # 指定版本范围