Monorepo架构: 项目管理工具介绍、需求分析与技术选型
概述
-
如何实现 monorepo,以及在项目中如何管理多个包,在进行具体项目开发前,有必要强调一个重要思维 — 全局观
-
即看待技术方案时,要从需求角度出发,综合考量该方案能否长远满足项目或团队需求
-
为什么要有全局观呢?如果直接拿一个项目,用特定工具创建工作区间并将其作为 monorepo 项目使用,虽直观易感受,但当技术更新时,这种思维方式就会落后
-
若站在更高视角了解新技术、新方向,先梳理相关工具,明确哪些工具可满足需求,虽前期会花费一些时间,但后续不论遇到何种工具,都能清楚其用途
-
这种方法可称作比较学习法,能举一反三、触类旁通,学会多种技术,横向比较了三类与 monorepo 相关的应用场景工具
-
建立全局观后,后续在进行包管理和构建相关的技术选型时,就能更有针对性
-
目前与 monorepo 相关的工具可分为三类,前两类尤为重要,需带着问题研究
- 包管理与版本控制:例如 NPM、Lerna 等工具
- 构建和测试优化:需要带着疑问去探究其含义
- 组件化流程化开发:这是前端常涉及的内容
-
解决包管理问题后,更重要的是形成自动化流水化,并提高其效率
-
构建和测试优化环节便是在包管理基础上加入缓存、服务端优化等,以提升自动化流水线和整体开发效率
包管理与版本控制
- 下面先从包管理和版本控制部分开始,重点介绍三个方案:
- Learner:用于管理多个应用源代码的工具,很多项目曾使用该工具,但官方已切换至 pnpm
- pnpm 和 Yarn Workspaces:二者类似,都可在同一工作区间管理多个 package 的依赖项。Pnpm Space 和 Yarn Workspaces 能在每个项目中独立使用 package.json 管理对应依赖
构建和测试优化相关的工具
-
NX:功能全面,具有缓存增量、并行构建、分布式构建等特点,还包含云平台相关功能,涉及构建、发布及云平台工具功能。其维护团队与 Leana 团队有一定关联,可与 Leana 5.1 以上版本集成,二者是互补关系
-
Turborepo:近年来流行的工具,底层用 Rush 编写。具有性能优化(采用高效缓存和任务调度策略)、依赖管理(智能处理依赖关系)、可扩展性(能处理不同规模项目的构建测试)、集成支持(与现有工具框架无缝集成)、远程缓存(提供服务端远程缓存加速构建)等特点,可视为 NX 的子集
-
Rush:微软出品,专为 monorepo 项目打造,主要解决两个问题:
- 幽灵依赖:指在 package.json 文件中未明确列出,但安装时必须安装的包,可能导致项目在不同环境运行结果不同
- 依赖包多副本问题:即同一个依赖包的多个版本同时存在于项目中,使依赖关系复杂,易引发引用混乱。Rush 还支持并行构建、插件系统和项目发布
组件化和模块化开发相关工具
-
Bit:使用 PNPM 管理项目依赖,是商业化工具。其颗粒度小到组件级别,可将 React 等框架中的组件或函数单独封装发布,具有灵活、便捷、可插拔、无需过多维护等优点,适用于多种场景,支持多种项目类型。
-
Layer- pack:是Webpack Plugin, 主要管理导入的 Glow 选项,聚焦于 Webpack 生态,可继承 Webpack 相关配置,适用于已有 Webpack 项目且想采用特定架构的情况
基于 Yarn Workspace 的 Monorepo 项目实践指南
1 ) 环境准备
- 先从包管理相关工具开始, 首先,查看本地环境,我使用的是 Node 的 LTS 版本,然后全局安装 yarn
- 推荐安装 nrm 并使用它切换到淘宝源。我已经切换成功,可以通过
npm config get registry
查看,现在已经切换到淘宝源了。将淘宝源复制,然后使用yarn config set registry
设置为淘宝源,这样使用 yarn 工具安装依赖项时就会使用淘宝源,加速安装过程,npm 则不用管
2 ) 创建项目目录与初始化
- 创建一个空目录,使用
mkdir yarnworkspacedemo
命令,也可以右键创建 - 我直接使用命令行创建,接下来使用
yarn init -y
初始化项目 - 初始化完成后,用 VSCode 打开
package.json
文件
3 ) 设置 yarn workspace 项目
- 要设置一个 yarn workspace 的项目(即命名空间项目)来管理模块,需要修改
package.json
文件,在其中设置workspaces
属性,并指定目录。 - 接着创建一个
packages
目录,在其下面创建对应的模块,例如moduleA
和moduleB
,这两个模块实际上就是两个 npm 包。 - 在
packages/moduleA
和packages/moduleB
目录下,分别使用yarn init -y
初始化package.json
文件,这样每个模块就有独立的文件来记录其相关依赖。
4 ) 添加依赖项
- 在子模块中添加依赖:可以
cd
到子模块目录下,使用yarn add
命令添加对应的依赖 - 在根目录为所有模块添加依赖:直接在根目录使用
yarn add lodash -w
命令,这里的-w
表示为所有工作区添加依赖。但要注意,根目录的package.json
中需要设置private: true
和workspaces
属性项。执行该命令后,会更新根目录的package.json
文件,但子目录中不会显示依赖,但在子模块中调用依赖的方法时可以正常使用,这得益于 Node 的包管理机制,它会向上查找父级目录中的依赖 - 在特定子模块中添加依赖:使用
yarn workspace moduleA add <dependency>
命令,可以限定在某个模块中添加依赖项,此时只会更新该子模块的package.json
文件
6 ) 处理依赖版本冲突
- 如果在子目录中指定了某个依赖的版本,而根目录中又指定了不同的版本,yarn 会在子目录中单独安装一份指定版本的依赖。
- 例如,在子目录中添加
lodash
版本为 3,在根目录中添加lodash
版本为 4,子目录中会单独创建一个node_modules
目录来存放版本为 3 的lodash
,而根目录的node_modules
中存放版本为 4 的lodash
。
7 ) 跨模块使用
-
若要在
moduleB
中使用moduleA
导出的方法,先在moduleA
中导出方法,如使用 CommonJS 规范:const range = (x, y) => { // 方法实现 return [x, y]; }; module.exports = range;
-
然后在
moduleB
中使用yarn workspace moduleB add moduleA
添加依赖, 但可能会遇到版本号问题,这是因为 npm 上可能有同名的包。解决方法有两种:- 修改包名:给包名添加命名空间,如
@yournamespace/moduleA
,确保名称独一无二。 - 使用文件引用:在
package.json
中使用文件路径引用,如"./packages/moduleA"
- 修改包名:给包名添加命名空间,如
8 ) 创建新模块及处理未发布模块引用
-
创建一个新模块
moduleC
,在packages/moduleC
目录下使用yarn init -y
初始化,并设置包名时使用@命名空间/包名
的格式,包名建议使用小写英文加中短横线,且不以点号或下横线开头。 -
若要在
moduleB
中安装还未发布的moduleC
,不能直接使用yarn workspace moduleB add moduleC
,会报错。正确做法是先cd
到根目录,使用yarn install
命令在node_modules
下创建软链接。执行后若没有成功链接,可以清除缓存,删除node_modules
目录,再次执行yarn install
。此时在moduleB
中可以使用require('moduleC')
引用moduleC
,但moduleB
的package.json
中不会更新moduleC
的依赖信息。若要更新,需要手动在moduleB
的package.json
的dependencies
部分添加moduleC
的具体版本。
9 ) 注意事项
- 文件协议引用的问题:如果使用文件协议引用模块(如
file:./packages/moduleA
),模块会被视为外部依赖,而不是工作区的一部分。每次更新模块代码后,需要清除node_modules
缓存,重新执行yarn install
,比较麻烦。 - 使用 yarn workspace 的要点总结:
- 在项目根目录的
package.json
中配置workspaces
属性,添加多个文件目录交给 yarn 管理。 - 管理模块可以使用
yarn workspace <模块名> <命令>
的方式,也可以直接cd
到该目录使用常用命令,yarn 会自动管理多个不同包下同名依赖的不同版本号。若与根目录中的依赖冲突,会在该包下创建node_modules
目录并下载一份新的依赖。 - 模块未发布时使用 yarn 关联本地依赖,可能会下载到别人创建的同名包,解决方法是命名一个独特的名称。跨模块使用时,可以使用
module:*
方式配合yarn install
让包在整个工作区中关联,但还需执行yarn install
在工作区目录的node_modules
中创建对应的软链接。 - 包名最好使用
@命名空间/包名
的结构,采用小写英文加中短横线的方式命名,避免以点号或下横线开头
- 在项目根目录的
PMPM Workspace使用教程、依赖包管理操作
1 ) 创建PNPM Workspace
- 创建空目录:首先创建一个空的目录,我在这里创建了一个名为“PNPMworkspacedemo”的目录。
- 打开workspace:使用workspace将其打开。
- 初始化package.json文件:在终端工具中,使用
pnpm init -y
初始化一个package.json
文件。 - 创建pnpm-workspace.yaml文件:在该目录中创建一个名为
pnpm-workspace.yaml
的文件,在文件里添加一个字段packages
,换行后接上文件的目录。例如,packages/
表示只要是packages
目录下面的包,相当于moderator
项目,就是受PNPM workspace管理的模块。你也可以添加其他目录,比如folder1
、folder2
等(若根目录中没有这些目录,可以将其注释掉)。这样,PNPM的workspace就创建好了。
2 ) 创建模块并初始化
- 创建
module-a
和module-b
两个模块,和之前一样,在这两个目录中初始化package.json
文件。打开终端工具,执行以下命令: cd packages/module-a
然后pnpm init -y
cd packages/module-b
然后pnpm init -y
3 ) 安装依赖包
- 在根目录安装依赖:在根目录执行
pnpm install lodash
命令,会提示需要有pnpm-workspace.yaml
文件。若文件名字写错,修改后再次安装,需要加上-w
参数,这样依赖项会安装在根目录,module-a
和module-b
可以引用根目录中的模块,但这些模块不会打包到module-a
或module-b
中。 - 在指定模块安装特定版本依赖:若要在
module-a
中安装特定版本的lodash
,可以使用--filter
参数,例如pnpm install lodash@3 --filter=module-a
,执行后在module-a
的package.json
里会多出一个lodash
的依赖项。也可以使用pnpm install lodash@2 --filter module-a
这种格式。 - 模块间依赖安装:在
module-b
中引用module-a
,可以使用pnpm add module-a
命令。安装后,node_modules
下面的依赖包会有一个小箭头,表示这些文件是在全局引用的,存放在PNPM的缓存目录中,这样可以节省磁盘空间。
4 ) 查看和设置PNPM缓存目录
- 查看缓存目录:使用
pnpm store path
命令可以查看本地的PNPM store位置,使用文件管理工具打开该目录,里面存放着很多下载的包和文件。 - 设置缓存路径:使用
pnpm config set store-dir ~/.pnpm-store
命令可以设置缓存路径,设置成功后,后续安装依赖包时,若提示需要重新rebuild store
,按照提示操作即可。也可以使用pnpm config delete store-dir
命令将缓存路径恢复到默认情况。
5 ) 模块代码编写与测试
- 在
module-a
和module-b
中编写代码进行测试。例如,在module-a
中引入lodash
:const _ = require('lodash'); const range = _.range; module.exports = range;
- 在
module-b
中引用module-a
:const range = require('module-a'); console.log(range(1, 10));
- 保存代码后,若要执行代码,可以使用不同的命令:
- 执行所有模块的脚本:PNPM提供了
-r
命令,例如pnpm -r exec node index.js
,可以循环执行各个模块中的index.js
文件。在多个模块需要运行测试脚本的场景中,就可以使用pnpm -r exec
加上测试脚本的执行命令,让PNPM循环执行每个包中的测试脚本。 - 执行特定模块的脚本:若要执行
module-a
中的start
脚本,可以使用pnpm --filter=module-a run start
(建议写filter
时带上引号,避免出现问题)。
- 执行所有模块的脚本:PNPM提供了
6 ) 简化命令设置
对于觉得每次输入pnpm
相关命令麻烦的同学,可以根据不同操作系统进行设置:
- Mac或Linux系统:可以借助如
zsh
一类的终端工具,设置常量。例如,设置alias pnpmrs='pnpm --filter=module-a'
,使用前先执行source ~/.zshrc
让设置生效。也可以进一步简化,如alias p=pnpm
,alias pr='pnpm -r exec'
。若需要传参,可以编写函数,例如:pa() { pnpm "$1" --filter="$2" }
- Windows系统:在
PowerShell
中,配置存放在profile.ps1
文件里。可以使用Set-Alias
命令设置别名,例如Set-Alias p pnpm
,Set-Alias pr 'pnpm -r exec'
。设置完成后,保存并重新加载配置文件即可使用简化后的命令。
PNPM与Yarn对比
- Yarn的优缺点
- 优点:相对成熟和稳定,较早支持monorepo方式,社区支持相对广泛,支持并行下载。
- 缺点:空间占用比PNPM大,某些情况下安装性能不如PNPM,配置命令没有PNPM方便。配置workspace时,需要在
package.json
里写包名,相对麻烦。
- PNPM的优缺点
- 优点:具有高效的存储方式,性能比Yarn优越,有更严格的包命名策略。
- 缺点:社区支持和资源没有Yarn完善,可能会遇到与项目或工具的兼容性问题,例如低版本的Node。