使用 node 管理器管理 monorepo
不包含工具的使用,一方面因为我没用到过工具,另外一方面看了一下 Lerna,说 Learna 底层还是用到了 yarn 去进行管理,二者并不冲突,所以打算先学习一下基础再说。
顾名思义,monorepo 指的就是 mono(一个)repository,即使用一个 repo 去管理整个项目,也就是代码(code)和资源(assets),其结构大概为:
|- repo
|- |- app-a
|- |- app-b
|- |- lib-a
|- |- lib-b
顺便,如果使用的是 symlink,有一个好处就在于修改 lib-b
中的代码,是不需要重新进行打包的,其他的 package 可以自动获取最新的代码。
构建一个 monorepo
这里虽然会带一点使用 npm 去运行 monorepo 的方法,不过主要还是用 yarn。
基础结构为:
|- packages
| |- module-a
| | |- index.js
| | |- package.json
| |- module-b
| | |- index.js
| | |- package.json
模块中的 package.json 可以使用默认初始化进行生成,即 npm init -y
,index.js 中的代码就是一段 log 输出 a 或者 b。
暴力方法
即将 packages
的名称改为 node_modules
,node 这样会自动寻找 node_modules
目录下的包并运行,如:
当然这个方法不是一个长期有效的方法,尤其是项目会涉及到第三方库时,管理就会变得非常的麻烦。
毕竟大多数情况下,node_modules
中的内容都会被 .gitignore
所忽略,一个 workaround 是重写 .gitignore
文件,指定不需要被 git 忽略的文件,不过依旧,在涉及到使用一些第三方库的前提下,单独在 .gitignore
中列举文件很快就会变得难以管理。
symlink
这一部分代码不变,指令如下:
➜ monorepo git:(main) ✗ cd packages
➜ packages git:(main) ✗ cd module-b
➜ module-b git:(main) ✗ npm link
added 1 package, and audited 3 packages in 458ms
found 0 vulnerabilities
➜ module-b git:(main) ✗ cd ../module-a
➜ module-a git:(main) ✗ npm link module-b
added 2 packages, and audited 5 packages in 408ms
found 0 vulnerabilities
➜ module-a git:(main) ✗ node index.js
b
a
➜ module-a git:(main) ✗
link
指令 npm 和 yarn 通用,这里会将当前的 package 注册为 global module,随后切换到 module-a
中去进行对应的注册,这样就可以在 module-a
中使用 module-b
了。
重新跑了一遍,之前有报错,有点懒得打码了
如果项目需求比较简单,这也不失为一个短期的解决方法。
workspaces
三大管理器其实都有使用 workspace,但是 yarn 出来的最早,所以这里以 yarn 优先说明。
使用 workspaces 最大的优势在于管理器可以自动进行 symlink,省去了手动管理的麻烦。同时 npm 也会尽可能地使用 作用域升(hoisting),使得项目之间可以共用同样的 dependencies,从而提升性能。
此时项目变动如下:
|- packages
| |- module-a
| | |- index.js
| | |- package.json
| |- module-b
| | |- index.js
| | |- package.json
| |- package.json
模块之间的代码没有任何变化,
基础配置
package.json
更新内容如下
{
"name": "monorepo",
"workspaces": {
"packages": ["packages/*"]
},
"private": true
}
"packages/*"
是对 packages 下面进行了模糊匹配,一个个列举出来也是可以的。
关于 private
这个变量,官方文档其实有说明:
Worktrees used to be required to be private (ie list
"private": true
in their package.json). This requirement got removed with the 2.0 release in order to help standalone projects to progressively adopt workspaces (for example by listing their documentation website as a separate workspace).
如果 yarn 的版本为 v2 以上则可以不用添加,运行结果:
➜ monorepo git:(main) ✗ yarn --version
1.22.19
➜ monorepo git:(main) ✗ yarn
yarn install v1.22.19
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.10s.
作用域提升
这个是使用 workspace 的优势之一,如 a 和 b 同时都会使用 相同的包,与其在每个模块中安装对应的 lodash,yarn/npm 会将下载的依赖包放到项目顶部的 node_modules
中,如:
➜ monorepo git:(main) ✗ cd packages/module-a
➜ module-a git:(main) ✗ yarn add lodash
yarn add v1.22.19
[1/4] 🔍 Resolving packages...
[2/4] 🚚 Fetching packages...
[3/4] 🔗 Linking dependencies...
[4/4] 🔨 Building fresh packages...
success Saved lockfile.
success Saved 1 new dependency.
info Direct dependencies
info All dependencies
└─ lodash@4.17.21
✨ Done in 1.97s.
➜ module-a git:(main) ✗ cd ../module-b
➜ module-b git:(main) ✗ yarn add lodash
yarn add v1.22.19
[1/4] 🔍 Resolving packages...
[2/4] 🚚 Fetching packages...
[3/4] 🔗 Linking dependencies...
[4/4] 🔨 Building fresh packages...
success Saved 1 new dependency.
info Direct dependencies
info All dependencies
└─ lodash@4.17.21
✨ Done in 0.44s.
效果如下:
如果二者装的依赖版本不一样,那么其中一个项目会将该依赖安装与当前项目中的 node_modules
中:
➜ module-b git:(main) ✗ yarn add lodash@3
yarn add v1.22.19
[1/4] 🔍 Resolving packages...
[2/4] 🚚 Fetching packages...
[3/4] 🔗 Linking dependencies...
[4/4] 🔨 Building fresh packages...
success Saved lockfile.
success Saved 1 new dependency.
info Direct dependencies
info All dependencies
└─ lodash@3.10.1
✨ Done in 4.50s.
如果想要在当前项目中禁止 hoisting,也可以直接在 package.json 中修改:
{
"name": "monorepo",
"workspaces": {
"packages": ["packages/*"],
"nohoist": []
}
}
一般来说默认不会被 hoist 的项目是一些会与二进制打交道的项目,比如说 react-native、electron 之类的跨平台实现。。
运行脚本
这是一个比较少见的 usecase,不过也肯定会存在。
这里的假设就是,module-b 的作用类似于 cli,需要将整体代码打包让其他的项目去运行这个模块。
这里首先修改 module-b 的 index.js:
#!/usr/bin/env node
// 上面这段代码告知 npm/yarn 下面代码需要直接用 node 去执行
console.log('module b is running');
随后更新 package.json:
{
"name": "module-b",
"bin": "./index.js"
}
随后在 terminal 重新安装一下,更新 symlink:
➜ packages git:(main) ✗ yarn install --force
yarn install v1.22.19
[1/4] 🔍 Resolving packages...
[2/4] 🚚 Fetching packages...
[3/4] 🔗 Linking dependencies...
[4/4] 🔨 Rebuilding all packages...
success Saved lockfile.
✨ Done in 0.21s.
这里必须要用 --force
去强行执行一下更新,否则 yarn 会因为 dependency 没有变动而忽略掉 bin
中的内容。
最后就可以在直接脚本中运行 module-b,如:
{
"name": "monorepo",
"workspaces": {
"packages": ["packages/*"],
"nohoist": []
},
"private": true,
"scripts": {
"start": "module-b"
}
}
➜ packages git:(main) ✗ yarn start
yarn run v1.22.19
$ module-b
module b is running
✨ Done in 1.05s.
如果要重命名 module-b 导出的模块,可以将 bin 中的内容修改成对象:
{
"name": "module-b",
"bin": {
"mdb": "./index.js"
}
}
重新运行一下 yarn install --force
后,就可以以 "scripts": { "start": "mdb" }
的语法去运行 module-b 了。
同时,因为这个脚本是在全剧的 node_modules
下进行的 symlink,因此该项目下所有的包都可以找到并运行这个脚本。
workspace 指令
刚才运行的指令其实都有在切换一些 repo,但是 yarn 有提供 workspace
指令,可以直接在某一个目录中使用 yarn workspace module command
去进行操作,如:
# 一直在根目录 monorepo 下
➜ monorepo git:(main) ✗ yarn workspace module-a add lodash-contrib
yarn workspace v1.22.19
yarn add v1.22.19
[1/4] 🔍 Resolving packages...
[2/4] 🚚 Fetching packages...
[3/4] 🔗 Linking dependencies...
[4/4] 🔨 Building fresh packages...
success Saved lockfile.
success Saved 1 new dependency.
info Direct dependencies
info All dependencies
└─ lodash-contrib@4.1200.1
✨ Done in 2.36s.
✨ Done in 3.30s.
➜
➜ monorepo git:(main) ✗ yarn workspace module-a start
yarn workspace v1.22.19
yarn run v1.22.19
$ mdb
module b is running
✨ Done in 1.00s.
✨ Done in 1.90s.
➜
➜ monorepo git:(main) ✗ yarn workspace module-b build
yarn workspace v1.22.19
yarn run v1.22.19
$ node ./index.js
module b is running
✨ Done in 0.90s.
✨ Done in 1.75s.
其他管理器
大部分的使用是通的,这里会列举一些细微的差别。
npm
需要注意的是,在 npm v7 以前,这个功能是不支持的,可以使用 yarn。
npm v6 应该可以粗略对标为 node v14,我看了下我本地的 14 用的是 6.14.12,node v16 用的是 8.19.2,目前最新的 npm 版本在 v9,应该对标的是 node v18?
想要具体查看当前的 node version 还是需要使用指令查看:
➜ module-a git:(main) ✗ nvm use 14
Now using node v14.16.1 (npm v6.14.12)
➜ module-a git:(main) ✗ npm --version
6.14.12
➜ module-a git:(main) ✗ nvm use 16
Now using node v16.17.1 (npm v8.19.2)
➜ module-a git:(main) ✗ npm --version
8.19.2
根目录下的 package.json
省去其他可以不需要的自动生成属性后如下:
{
"name": "monorepo",
"workspaces": ["packages/module-a", "packages/module-b"]
}
随后在根目录下运行 npm i
:
➜ monorepo git:(main) ✗ npm i
added 2 packages, and audited 5 packages in 531ms
found 0 vulnerabilities
⚠️:虽然官方文档上用的是数组,不过我直接用 yarn 的 package.json,以对象的方式也是可以运行的。
workspace 指令的细微差异
使用 npm 的话,workspace
和 workspaces
是作为一个 flag 存在的:
➜ monorepo git:(main) ✗ npm --workspace=module-a run start
> module-a@1.0.0 start
> mdb
module b is running
相对而言会比 yarn 要麻烦一些。
不过 npm 可以在同时运行所有名称相同的脚本,如:
➜ monorepo git:(main) ✗ npm --workspaces run start
> module-a@1.0.0 start
> mdb
module b is running
npm ERR! Lifecycle script `start` failed with error:
npm ERR! Error: Missing script: "start"
Did you mean one of these?
npm star # Mark your favorite packages
npm stars # View packages marked as favorites
To see a list of scripts, run:
npm run
npm ERR! in workspace: module-b@1.0.0
npm ERR! at location:
为了预防这种错误,npm 也提供了一个 flag 去跳过不存在的指令:
➜ monorepo git:(main) ✗ npm --workspaces --if-present run start
> module-a@1.0.0 start
> mdb
module b is running
注意指令上的细微不同,单独运行一个 module 用的是 workspace
,运行所有的指令使用的是 workspaces
。
⚠️:目前 npm 不支持使用 nohoist
,虽然出现版本冲突的处理方式依旧是一个会 hoist,另一个不会。
pnpm
yarn 和 npm 之间的配置还挺像的,但是 pnpm 的话会要求在 monorepo 的根目录下创建一个 pnpm-workspace.yaml
的文件:
packages:
- packages/*
第二步是要检查版本,现在 a 中引用了 b,但是 pnpm 不像 npm/yarn 那样直接做 symlink,而是通过 virtual store 去进行版本管理,这个时候就要手动维护一下版本的一致性,下面就是 a 中的 package.json:
{
"name": "module-a",
"dependencies": {
"module-b": "workspace:*"
}
}
最后,pnpm 没有 workspace
这个 flag,而是使用 --filter
这个 flag:
➜ monorepo git:(main) ✗ pnpm i
Scope: all 3 workspace projects
Packages: +4
++++
Packages are hard linked from the content-addressable store to the virtual store.
Content-addressable store is at: /Users/Library/pnpm/store/v3
Virtual store is at: node_modules/.pnpm
Progress: resolved 4, reused 4, downloaded 0, added 4, done
➜
➜ monorepo git:(main) ✗ pnpm -F "module-a" run start
> module-a@1.0.0 start /Users/study/monorepo/packages/module-a
> mdb
module b is running
pnpm 的优点在于,如果该脚本不存在,那么 pnpm 会直接忽略而不是报错。另外,因为 pnpm 使用的是 virtual store,因此也不存在 hoisting。
参考
-
npm workspaces
-
yarn Workspaces
-
pnpm Workspace