前端工程化进阶:Monorepos 架构简析(水文)

引言

对于 Monorepos 架构一直早有耳闻, 但是一直用不上也就懒得去了解, 刚好最近想要基于 prosemirror 写一个自己的富文本编辑器, 基本上是就是参考 tiptap 源码来一步步开发, 一为折腾二为学习。刚好 tiptap 其实就是 Monorepos 架构所以顺便简单研究研究, 故而有了这篇文章。

一、基本概念了解

再开始前先了解几种常见的架构

1.1 单体架构(Monolith)

所谓 Monolith 这个词字面意思是 单块的、整体的, 在软件架构里常用来指 单体架构 模式。

该模式其实就是将所有功能都打包在一个整体里, 作为一个应用进行部署和运行, 它和微服务应该是一个完全相反的一种模式。

  1. 主要特点就是:
  • 单一代码库: 所有功能模块(用户、订单、支付、库存...)都在同一个代码仓库、同一个项目里。
  • 单体部署: 打包后只产出一个可执行文件 / war 包 / docker 镜像, 直接上线。
  • 共享内存与数据库: 所有功能都是部署在一个机器上, 通常共享内存、共用一个数据库。
  1. 优点
  • 开发简单: 没有复杂的服务间通信。
  • 部署简单: 一个包就能跑起来, 不用维护一堆微服务。
  • 调试方便: 本地起一个服务就能跑全流程。
  • 性能好: 模块间是进程内调用, 不用走网络。
  1. 缺点
  • 耦合高: 改一处可能影响全局,难以模块化管理。
  • 扩展性差: 无法按模块独立扩容。
  • 技术栈受限: 所有功能只能用同一技术栈。
  • 发布成本高: 改一行代码也要重新打包发布整个应用。

当然 Monolith 其实和我们要聊的 Monorepos 没啥太大关系, 这里只是顺带了解了解。后面要聊的 multirepo / monorepo 讨论的更多是 代码仓库管理策略

1.2 多仓库模式(multirepos)

所谓 multireposMulti-repos 也就是多仓库模式, 说白了就是每个「项目/服务/模块/应用」都是单独放在一个代码仓库里。

当然这些代码仓库如果没有任何关联, 其实也没啥好说点。但是相反有些它们之间可能是有关联的, 甚至有很多业务逻辑都是相通的。比如我们一个项目有 PC 端、后台管理、移动端、小程序、APP 端等等, 甚至有些还有共用的组件库、工具库之类的。

再比如我们上面提到的 prosemirror 其实就是使用 multirepos 架构, 每个功能模块都是以独立仓库的形式存在, 如下图所示:

  1. 那么 multirepos 架构有舍特点呢?
  • 一仓一项目: 每个服务、库、UI 组件、工具包都有自己独立的 Git 仓库。
  • 独立版本管理: 每个仓库有自己的版本号、分支、发布流程。
  • 强解耦: 一个仓库的变更不直接影响其他仓库。
  1. multirepos 架构优点:
  • 边界清晰: 不同模块之间独立管理, 没有太多耦合。
  • 独立发布: 可以单独更新、发布某个仓库, 而不必影响其他仓库。
  • 权限控制简单: 敏感项目可以设置私有仓库, 权限隔离好做。
  • 适合多团队合作: 不同仓库(项目)由各自的团队负责, 团队之间互不干扰。
  1. multirepos 架构缺点
  • 协作成本高: 当某个需求改动需要涉及多个仓库时, 需要多次提 PR、多次发布、如果之间还存在耦合就容易出错。
  • 依赖管理麻烦: 仓库之间如果相互依赖, 则比较麻烦没次都需要手动发布版本、升级依赖。
  • 工具链碎片化: 每个仓库可能有不同的 lint / build / test 配置, 配置之间要做到一致性就比较麻烦。只要有一点调整, 就需要动所有仓库。

1.3 单仓库模式(Monorepos)

Monorepo 则是单仓库模式, 顾名思义就是直接将多个项目/服务/模块/包放到同一个仓库进行统一管理, 就好比如我有一个网站, 有前端项目有服务项目, 服务端也是用 JS(Node) 写的, 那我其实就可以将这两个项目放到同一个仓库进行一个管理, 这样的话很多工具函数、脚本、项目配置都是可以复用的。

Monorepos 模式其实就特别一些开源的项目, 上文说到的 tiptap 就是该模式, 一个完整的项目包含了基础的核心模块、还有各种功能扩展模块、同时还需要针对 RectVue、原生 HTML 提供对应的功能包... 同时这些模块之间很多

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

二、Npm Workspaces

npm workspacenpm 7 引入的一个新功能, 它允许我们直接在一个单项目中同时管理多个独立的子项目(依赖包), 可以方便的集中管理它们之间的依赖, 减少重复并提升可维护性。

Npm 项目中, 我们也正是通过 Npm Workspaces 来实现 Monorepos 架构, 下面我们就简单介绍下 Npm Workspaces 的使用。

2.1 项目初始化

  1. 首先我们先初始化一个 Npm 项目:
sh 复制代码
npm init -y

初始化完成后, 会在项目跟目录下创建一个 package.json 文件

  1. 创建子包: 下面我们来创建两个子包 packages/a 以及 app/b
sh 复制代码
npm init -y -w packages/a
npm init -y -w app/b

执行上面代码将会:

  • 分别在 apppackages 目录下初始化两个子包(npm 项目)
  • 同时会在根 package.json 中, workspaces 配置中添加新的子包
  • 同时还会在根目录中, 将子包以软连接的形式安装到 node_modules
  1. 手动创建子包: 我们知道了调用 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/bpackage.json, 新增依赖包配置

2.3 使用子包

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

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

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

对了, 项目根 package.json 中, workspaces 配置的是子包的路径, 所以只是改子包的名称, 这边是不需要动的。同时这边其实也可以直接使用通配符 *, 如下所示, packagesapp 目录下的所有项目都将会被作为子包进行加载

diff 复制代码
{
  ...
+ "workspaces": [
+   "packages/*",
+   "app/*"
+ ]
}

三、pnpm workspaces

除了使用官方 npm workspaces, 我们还可以使用第三方包管理工具, 比如 yarn 或者 pnpm 它们都实现了各种的一套 workspaces 协议, 下面我们简单介绍下 pnpm workspaces 至于 yarn 就不展开了

3.1 初始化项目

pnpm 中初始化相对来说会比较麻烦点, 一切都需要手动操作

  1. 初始化根项目: 在项目根目录初始化一个 npm 项目
sh 复制代码
pnpm init
  1. 创建 pnpm workspace 配置文件: 根目录创建配置文件 pnpm-workspace.yaml 所有和 workspace 相关的配置都在这边定义
  1. 手动创建 & 初始化子包: 是的这边子包需要我们手动在对应子包目录下进行创建、初始化(npm init y)

3.2 添加依赖

  1. 根目录安装依赖: 对于公用的依赖可以直接在项目根目录中进行安装, 这边可以直接在根目录执行 pnpm add 来进行安装, 或者在任意位置(子包、根目录)通过 -w 参数来安装根依赖包, 这里的 -w = workspace root
sh 复制代码
pnpm add react # 根目录执行
pnpm add react -w # 任意位置执行都行, 会在根目录安装依赖
  1. 为子包安装依赖: 除了直接在子包内通过执行 pnpm add 来安装项目依赖外, 其实我们还可以使用 --filter 来为子包安装依赖, 通过 --filter 就不限制目录了
sh 复制代码
pnpm add react # 子包内执行
pnpm add react --filter a # 任意位置执行都行, 通过「--filter a」来为子包「a」安装依赖

3.3 使用子包

在开始前我们需要了解下 workspace: 协议, 该协议是 pnpmmonorepo 中用于引用本地 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'  # 指定版本范围

四、参考

相关推荐
兆子龙2 小时前
前端必学:完美组件封装的 7 个原则
前端·javascript
兆子龙2 小时前
React 性能坑:别让 AI 踩了,快来添加 rule 吧
前端·javascript
光影少年2 小时前
Vue的生命周期有哪些及执行机制?
前端·vue.js·掘金·金石计划
来碗疙瘩汤2 小时前
Vue 事件绑定完全指南:官方文档未详述的事件大全
前端·javascript·vue.js
天涯学馆2 小时前
从 V8 引擎看 JS 代码是如何一步步变成机器指令的
前端·javascript·面试
Elaine3362 小时前
【通过 Vue 实例劫持突破 Web 编辑器的粘贴限制】
前端·javascript·vue.js·chrome devtools·前端逆向
哔哩哔哩技术2 小时前
从“截图大法”到真实交互:B站专栏视频卡的技术革命
前端
程序员讲BPM工作流2 小时前
npm非全局方式安装小龙虾OpenClaw
前端·npm·node.js
阿成学长_Cain2 小时前
Linux alias 命令详解:从入门到高级用法
linux·前端·chrome