你需要知道的monorepo管理工具
上篇文章我们从多个维度对比了multirepo和monorepo各自的优势,相信这些盘点对你确定使用哪种方式组织当前项目code会很有帮助,本篇文章将对monorepo常用管理工具进行介绍。
一 、 Yarn Workspaces
Workspaces是Yarn从1.0版本开始支持的新特性,使用它能更便捷的管理具有多个项目的仓库,很多知名开源项目都在使用Yarn Workspace,如果vue、react、jest等。
Workspace特性一览:
- 它设置了一个单一的
node_modules
,这样多个项目可以共享依赖,而且存在项目相互依赖时可以引用到最新的版本,这比手动yarn link
要方便和准确的多 - 你的所有项目依赖都将被安装在一起,从而给Yarn更大的空间来更好地优化它们
- Yarn将使用一个单一的
lock file
,而不是为每个项目使用不同的lock file,这意味着更少的冲突和更容易的审查
具体使用方式可查阅官方文档,接下来主要介绍一下常用的Yarn Workspace命令以及如何实现node_modules共享
1. 常用的Yarn Workspace命令
1.1 yarn workspace <workspace_name>
针对特定的 workspace 执行指定的 <command>
,如:
$ yarn workspace project1 add vue --dev // 安装 vue 到 project1
$ yarn workspace project1 remove vue // 从project1 中移除 vue
在 {workspace}/package.json#scripts
中定义的脚本命令,也可以作为 <command>
来执行。
下面是一个利用这个特点创建统一构建命令的例子:
projects/package.json:
// projects/package.json
{
"scripts": {
"build": "yarn workspaces run build"
}
}
project1 | project2/package.json:
{
"scripts": {
"build": "rollup -i index.js -f esm -o dist/bundle.js"
}
}
执行 yarn build 的结果:
$ yarn build
yarn run v1.22.0
$ yarn workspaces run build
> project1
$ rollup -i index.js -f esm -o dist/bundle.js
index.js → dist/bundle.js...
created dist/bundle.js in 70ms
> project2
$ rollup -i index.js -f esm -e project1 -o dist/bundle.js
index.js → dist/bundle.js...
created dist/bundle.js in 80ms
✨ Done in 2.45s.
1.2 yarn workspaces
-
yarn workspaces run
在每个 workspace 下执行 。如:
yarn workspaces run test
将会执行各个 workspace 的 test script。
-
yarn workspaces info [--json]
显示当前各 workspace 之间的依赖关系树。
$ yarn workspaces info
yarn workspaces v1.21.1
{
"project1": {
"location": "project1",
"workspaceDependencies": [],
"mismatchedWorkspaceDependencies": []
},
"project2": {
"location": "project2",
"workspaceDependencies": [
"project1"
],
"mismatchedWorkspaceDependencies": []
}
}
✨ Done in 0.12s.
2. Yarn Workspace 共享 node_modules依赖
下面是一个带有两个子项目的nodejs项目
projects/
|--project1/
| |--package.json
| |--node_modules/
| | |--a/
|--project2
| |--package.json
| |--node_modules/
| | |--a/
| | |--project1/
project1/package.json:
{
"name": "project1",
"version": "1.0.0",
"dependencies": {
"a": "1.0.0"
}
}
project2/package.json:
{
"name": "project2",
"version": "1.0.0",
"dependencies": {
"a": "1.0.0",
"project1": "1.0.0"
}
}
没有使用 Yarn Workspace 前,需要分别在 project1
和 project2
目录下分别执行 yarn|npm install
来安装依赖包到各自的 node_modules
目录下。或者使用 yarn|npm upgrade
来升级依赖的包。这会产生一些问题:
- 如果多个项目存在相同依赖,则会重复下载依赖,不仅耗时还浪费磁盘资源
- 如果项目之间存在相互依赖,但被依赖的项目并没有发布
npm
包,这种情况常见的解决方法是使用npm link
,但是手动维护这样的操作显然很不方便 - 构建方式不统一,如果想构建需要
cd
到各个子项目中执行build
命令
使用Yarn Workspace后这些问题都迎刃而解,只需要在顶底层packages目录中新增如下package.json文件即可:
projects/package.json:
{
"private": true,
"workspaces": ["project1", "project2"] // 也可以使用通配符设置为 ["project*"]
}
在顶层packages目录下执行 yarn install
:
$ cd projects
$ rm -r project1/node_modules
$ rm -r project2/node_modules
$ yarn install
yarn install v1.22.0
info No lockfile found.
[1/4] 🔍 Resolving packages...
[2/4] 🚚 Fetching packages...
[3/4] 🔗 Linking dependencies...
[4/4] 🔨 Building fresh packages...
success Saved lockfile.
✨ Done in 0.56s.
此时查看目录结构如下:
projects/
|--package.json
|--project1/
| |--package.json
|--project2
| |--package.json
|--node_modules/
| |--a/
| |--project1/ -> ./project1/
- projects 是各个子项目的上级目录,术语上称之为
workspace-root
,而 project1 和 project2 术语上称之为workspace
- yarn install 命令既可以在 workspace-root 目录下执行,也可以在任何一个 workspace 目录下执行,效果是一样的
- 如果需要某个特殊的 workspace 不受 Yarn Workspace 管理,只需在此 workspace 目录下添加 .yarnrc 文件,并添加如下内容禁用即可:
workspaces-experimental false
- 在 project1 和 project2 目录下并没有 node_modules 目录(特殊情况下才会有,如当 project1 和 project2 依赖了不同版本的 a 时)
/node_modules/project1
是/project1
的软链接,软链接的名称使用的是/project1/package.json#name
属性的值。- 如果只是修改单个 workspace,可以使用
--focus
参数来快速安装相邻的依赖配置从而避免全部安装一次
3.Yarn Workspace 和 lerna 相比
Yarn Worksapce可作为Lerna的底层工具,一般配和lerna使用效果更佳。具体使用可参考基于lerna和yarn workspace的monorepo工作流
二、 Lerna
Lerna(以多头野兽Hydra的家命名)是一个“用于管理带有多个包的JavaScript项目的工具”。
Lerna的创建是为了解决Babel的多包问题,以优化使用git和npm管理多包仓库的工作流程,它本质上是一种工具和脚本,可以有效地管理和发布许多独立版本的包在一个Git仓库中。
my-lerna-repo/
package.json
packages/
package-1/
package.json
package-2/
package.json
具体基础使用方法可参考官方文档
1. 常用命令
lerna init
初始化项目,如果使用independent模式的话可以使用lerna init -i
,其中 -i 是--ndependent
缩写。初始化得到的目录结构为:
└── lerna-pro/
├── packages/
├── lerna.json
└── package.json
如果代码结构已经存在,则只需要在根目录下创建lerna.json
,并补充相关内容,lerna.json结构和说明如下:
{
"useWorkspaces": true, // 使用 workspaces 配置。此项为 true 的话,将使用 package.json 的 "workspaces",下面的 "packages" 字段将不生效
"version": "0.1.0", // 所有包版本号,独立模式-"independent"
"npmClient": "cnpm", // npm client,可设置为 cnpm、yarn 等
"packages": [ // 包所在目录,可指定多个
"packages/*"
],
"command": { // lerna 命令相关配置
"publish": { // 发布相关
"ignoreChanges": [ // 指定文件或目录的变更,不触发 publish
".gitignore",
"*.log",
"*.md"
]
},
"bootstrap": { // bootstrap 相关
"ignore": "npm-*", // 不受 bootstrap 影响的包
"npmClientArgs": [ // bootstr 执行参数
"--no-package-lock"
]
}
}
}
lerna bootstrap --hoist
为所有项目安装依赖,并链接所有依赖包,类似于npm i,使用--hoist选项后,所有公共的依赖都只会安装在根目录的node_modules目录中去,而不会在每个包目录下的node_modules中都保留各自的依赖包。lerna clean
删除所有项目的node_modules目录lerna run [script]
默认为所有的项目运行npm run [script]
脚本,可以指定项目lerna exec
在每个包运行任意的命令
$ lerna exec -- < command > [..args] # runs the command in all packages
$ lerna exec -- rm -rf ./node_modules
$ lerna exec -- protractor conf.js
lerna exec --scope my-component -- ls -la
lerna changed
列出下次发版lerna publish
要更新的包,原理在运行git add
、git commit
后,内部会运行git diff --name-only v版本号
➜ lerna-repo git:(master) ✗ lerna changed
info cli using local version of lerna
lerna notice cli v3.14.1
lerna info Looking for changed packages since v0.1.4
daybyday #只改过这一个 那下次publish将只上传这一个
lerna success found 1 package ready to publish
lerna publish
版本发布,按提示选择版本号(递增,或自定义),将会执行以下步骤: 1.运行lerna updated来决定哪一个包需要被publish 2.如果有必要,将会更新lerna.json中的version 3.将所有更新过的的包中的package.json的version字段更新 4.将所有更新过的包中的依赖更新 5.为新版本创建一个git commit或tag 6.将包publish到npm上;注意要先用npm adduser登录npm源,否则会失败lerna add <package>[@version] [--dev] [--exact] [--peer]
可以指定为某一个或所有的包安装依赖,依赖可以是外部(npm i 安装的)也可以是内部依赖(packages/下的包,会创建符号链接),例如:
1.lerna add babel , 该命令会在package-1和package-2下安装babel 2.lerna add react --scope=package-1 ,该命令会在package-1下安装react 3.lerna add package-2 --scope=package-1,该命令会在package-1下安装package-2
lerna create <name> [loc]
创建一个lerna管理的包lerna import <path-to-external-repository>
导入本地已经存在的包lerna ls
控制台打印 packages下的包名lerna link
项目包建立软链,类似npm link
2. 两种管理模式
Lerna 对于包的管理,有两种模式:固定模式(fixed/locked
)、独立模式(independent
),默认是fixed模式,babel和vue使用的正是此模式。
- 固定模式(fixed)
所有包是统一的版本号,每次升级,所有包版本统一更新,不管这个包内容改变与否。 lerna 的配置文件 lerna.json 中永远会存在一个确定版本号。现在
{
"version": "0.0.1"
}
- 独立模式(独立模式(independent) 每个包是单独的版本号,每次lerna 触发发布命令,每个包的版本都会单独变化。 lerna 的配置文件 lerna.json 中没有一个确定版本号,而是:
{
"version": "independent"
}
目前最常见的 monorepo 解决方案是 Lerna 和 yarn 的 workspaces 特性,基于lerna和Yarn Workspace的monorepo工作流。由于yarn和lerna在功能上有较多的重叠,我们采用yarn官方推荐的做法,用yarn来处理依赖
问题,用lerna来处理发布问题
。
三 、rush
rush是微软出品的一个专门为monorepo打造的项目管理工具,官方描述:“a scalable monorepo manager for the web”,目的是让在一个仓库中构建和发布多个包变得更加容易。
对rushjs的使用官方文档有较详细介绍,具体评测也可参考rushjs评价,本篇不再展开讲解。
四、Bazel
谷歌推出了Bazel build system,它是一个类似于Make
、Maven
和Gradle
的开源构建和测试工具,使用的是人类可读的高级构建语言。Bazel支持多种语言的项目,并为多种平台构建输出。它支持大型单一仓库中的大型代码库或跨多个仓库的大型代码库和大量用户。
Uber开发者使用Bazel来构建他们的Go monorepo。Uber用Go编写了大部分的后端服务和库,在2018年,这些服务和库都被归纳到一个大型的Go monorepo中,现在有超过10万个文件。Bazel让这个项目得以扩展,缩短了构建时间,并支持其发展。
一个不错的小型开源项目,以Bazel作为演示 Bazel被设计成大规模工作,并支持跨分布式基础设施的增量密封构建,这是大型代码库所必需的。有了Bazel的远程缓存,构建服务器还可以共享它们的构建工件。Bazel缓存所有以前完成的工作,并跟踪对文件内容和构建命令的更改。只有在包或包的依赖关系发生更改时,才构建和测试包。
Bazel可以在Linux、macOS和Windows上运行。Bazel可以从同一个项目为多个平台构建二进制文件和可部署的包,包括桌面、服务器和移动设备。支持许多语言,你可以扩展Bazel来支持任何其他语言或框架。
五、其他
另外值得研究的还有以下工具,在此附上链接,有兴趣可研究。
- Pants Twitter推出了名为Pants的monorepo构建系统
- Buck Fackbook开发的鼓励创建由代码和资源组成的小型可重用模块的构建系统
- Please 用Go编写的跨语言的构建系统,强调高性能、可移植性、可扩展性和正确性。
- NX 一套先进的可扩展的开发工具,适用于monorepos,非常强调现代全栈Web技术。
本文盘点了monorepo常见管理工具,用了较多篇幅来介绍较为主流的lerna + Yarn workspace方案,具体使用还需要在项目中结合具体情况进行组合和取舍。总的来说,百花齐放的解决方案让前端工程化变得更加灵活多样也充满了无限可能。
转载自:https://juejin.cn/post/6952496762569687047