环境要求
- yarn v1.x
- Node.js >= v10.12.0
- lerna 3.x
什么是 monorepo?
monorepo 是指一种把多个项目的源代码放在同一个代码仓库里面管理的一种方法。与 monorepo 相对的是 multirepo,它的思想是按模块分成多个仓库。当前有很多流行的开源项目使用 monorepo 管理代码,如 Babel、Vue3.0。
monorepo 的优缺点
优点:
- 代码复用更容易;
- 精简依赖管理,去除多个包的重复依赖;
- 方便大规模的重构,可以同时重构多个包然后统一发布,避免不同包管理的问题;
- 跨团队合作更容易,跨团队开发时只需提交代码即可合作,而不需要发布到包管理中心;
- 方便统一管理lint、test、build 和 release;
- 统一的地方处理 issue;
- 方便统一生成 ChangeLog。
缺点:
- 丢失版本信息,统一发布版本的 monorepo,会丢失各自项目的版本信息,尽管也可以单独发布版本;
- 缺乏针对包的权限控制(Git),只要需要访问一个项目的,就必须要给开发者授权整个仓库,可能存在安全问题;
- 消耗更多硬件资源:当你只需要开发某个项目时,必须要把整个仓库代码全部下载,会占用更多的网络和本地存储空间。
如何实现 monorepo?
目前比较主流的方案是采用 yarn workspaces + lerna 来实现。
yarn workspaces
yarn workspaces 可以将多个包的 node_modules
整合成一个, 只需要执行 yarn install
就可将所有包的依赖安装。
下面以一个例子来说明:
root/
workspace-a/
package.json
workspace-b/
package.json
node_modules/
package.json
根目录 package.json
文件中声明了 workspaces:
{
"private": true,
"workspaces": ["workspace-a", "workspace-b"]
}
注意,添加 private: true
是防止工作区根目录被发布到 npm,workspaces
是一个数组,其中包含所有“工作空间”的路径,也可支持通配符,如 "workspaces": ["packages/*"]
。
workspace-a
和 workspace-b
的 package. json
分别如下:
workspace-a/package.json:
{
"name": "workspace-a",
"version": "1.0.0",
"dependencies": {
"cross-env": "5.0.5"
}
}
workspace-b/package.json:
{
"name": "workspace-b",
"version": "1.0.0",
"dependencies": {
"cross-env": "5.0.5",
"workspace-a": "1.0.0"
}
}
最后在根目录运行 yarn install
即可安装所有依赖,安装后会有一个类似这样的目录结构:
/package.json
/yarn.lock
/node_modules
/node_modules/cross-env
/node_modules/workspace-a -> /workspace-a
/workspace-a/package.json
/workspace-b/package.json
workspace-b
依赖 workspace-a
,将直接引用当前项目中内部的文件,而不是从 npm 去取。且不会有各自的 node_modules/
,统一在根目录下的 node_modules
。
lerna
A tool for managing JavaScript projects with multiple packages. 一个用来管理有多个包的 JS 项目的工具。
以上是 lerna 的官方介绍。 lerna 用来管理多个包,优化维护多包的工作流,解决多个包互相依赖及发布的问题。目前已被许多著名的开源项目使用,如 babel、create-react-app、mint-ui、react-router。
一个 lerna 项目的结构大致如下:
lerna-repo/
packages/
package-1/
package.json
package-2/
package.json
package.json
lerna.json
packages/ 目录下存放所有的包,lerna.json
是它的配置文件,后面会介绍。
lerna 如何工作
lerna 管理包有2种模式 fixed 和 independent。
Fixed/Locked mode
这是默认的模式,Fixed 模式下所有的包共用一个版本号,这个版本号保存在 lerna.json
的 version
字段里面。所以当你运行 lerna publish
,修改过的包会自动更新 package.json
里的 version
并发布到 npm,未修改的包则不会更新。还有一个问题值得注意,当 lerna.json
的 version
做了主版本号的改动时,所有的包都会更新版本并发布。
注意:如果你的主版本号是 0,所有的修改都会当成是 breaking,表示做了不向下兼容的大改动,这会导致所有包的版本都会更新。
Independent mode
lerna init --independent
用来初始化一个 independent 模式的项目,你也可以手动修改 lerna.json
里的 version
为 independent
使用该模式。
independent 模式允许每个包自行更新版本号,当你运行 lerna publish
时,需要逐个选择修改过的包的版本。
lerna 常用命令
lerna init
创建一个新的 lerna 仓库或更新已有仓库为新版本的 lerna,其中的选项 --independent/-i
用来生成 independent
模式的项目。
lerna bootstrap
此命令会做以下几个事情:
- npm install 为所有的包安装依赖。
- 为互相依赖的包创建软链接。
- 在所有 bootstrap 包(不包括
command.bootstrap.ignore
中忽略的包)中执行npm run prepublish
(如果传了参数--ignore-prepublish
将跳过此步骤)。 - 在所有 bootstrap 包(不包括
command.bootstrap.ignore
中忽略的包)中执行npm run prepare
。
lerna publish
发布所有修改过的包,会在终端提示(prompt)选择一个新版本,并会更新所有改动到 Git 和 npm.
lerna run [script]
在所有包中执行特定的 npm script。
lerna ls
列出当前仓库中的所有公共包(public packages),private: true
的包不会列出。
lerna.json
lerna.json
内容大致如下
{
"version": "1.1.3",
"npmClient": "npm",
"command": {
"publish": {
"ignoreChanges": ["ignored-file", "*.md"],
"message": "chore(release): publish",
"registry": "https://npm.pkg.github.com"
},
"bootstrap": {
"ignore": "component-*",
"npmClientArgs": ["--no-package-lock"]
}
},
"packages": ["packages/*"]
}
version
: 当前仓库的版本。npmClient
: 使用的 npm 客户端,默认是 "npm",可选值还有 "yarn"。command.publish.ignoreChanges
: 是个数组,在这个数组里面的文件变动,不会触发版本更新。command.publish.message
: 自定义发布新版本时的 git commit 信息。command.publish.registry
: 设置私有仓库,默认是发布到npmjs.org
。command.bootstrap.ignore
: 设置在这里的目录将不会参与lerna bootstrap
。command.bootstrap.npmClientArgs
: 执行lerna bootstrap
时会将此数组的所有值当作参数传给npm install
。command.bootstrap.scope
: 限制lerna bootstrap
在哪些包起作用。packages
: 用以指明所有包的路径。
项目实践
搭建
开启 yarn workspaces:
yarn config set workspaces-experimental true
创建项目:
mkdir lerna-demo && cd lerna-demo
yarn init
全局安装 lerna:
npm install --global lerna
lerna 初始化:
lerna init
就会在当前目录下生成以下文件:
lerna-demo/
packages/
package.json
lerna.json
配置根目录下的 lerna.json
使用 yarn 客户端并使用 workspaces:
// lerna.json
{
"packages": [
"packages/*"
],
"version": "0.0.0",
+ "npmClient": "yarn",
+ "useWorkspaces": true
}
package.json
设置 private
为 true
,防止根目录被发布到 npm,还要设置 workspaces 目录:
// package.json
{
"name": "lerna-demo",
+ "private": true,
+ "workspaces": [
+ "packages/*"
+ ]
}
新增包
新增包需要在 packages/
下创建目录:
cd packages
mkdir wdm-lerna-demo-core && cd wdm-lerna-demo-core
使用 yarn 初始化:
yarn init
新包的初始化版本应当是
0.0.0
,因为是用 lerna 更新版本,发布时会在此基础上新增。
同样操作再创建包 wdm-lerna-demo-ui,最后目录结构大致如下:
lerna-demo/
packages/
wdm-lerna-demo-core/
package.json
wdm-lerna-demo-ui/
package.json
package.json
lerna.json
安装公共依赖
如果是所有的包公用的依赖,可以像这样安装:
lerna add lodash
执行完上面命令会在所有包的 dependencies
中加入依赖,如果是 dev 的公用依赖,最好是加到根目录的 devDependencies
:
yarn add eslint --dev -W
-W
选项显式指明在 workspace 的根目录执行,避免在根目录误操作 yarn。
安装本地包依赖
此示例中我们设定 ui
依赖 core
:
lerna add wdm-lerna-demo-core --scope=wdm-lerna-demo-ui
执行完后 wdm-lerna-demo-ui
会增加新的 dependencies
:
// package.json
{
"name": "wdm-lerna-demo-ui",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"dependencies": {
"wdm-lerna-demo-core": "^1.0.0"
}
}
然后就可以在 ui
中使用 core
:
import myCore from 'wdm-lerna-demo-core'
这里是通过软链接到
wdm-lerna-demo-core/
目录,并不是使用从 npm 下载的包。所以,在core
中的修改可以实时地在ui
中反映。
移除依赖
如果你想要移除一个被所有包依赖的公共包,可以这样操作:
lerna exec -- yarn remove lodash
# lerna exec -- yarn remove lodash --network-timeout=1000000 # 如果提示网络有问题用此命令
lerna exec -- <command> [..args]
表示在所有包中执行该 command.
发布到 npm
首先检查是否已登录到 npm:
# 打印出用户名表示已登录
npm whoami
如果未登录,则执行以下命令按提示操作:
npm login
登录后即可发布:
lerna publish
这里的 lerna publish
会做以下几件事情:
- 让你选择如何更新版本,是 major、minor 或是 beta。
- 更新版本到有改动的包,即修改
package.json
的version
。 - 如果有某些包依赖刚才更新的包,自动更新
dependencies
的版本号。 - 把改动提交到 git 并生成以版本号命名的 git commit 和 tag。
- 发布刚才改动的并且是 public 的包到 npm。
至此,一个 lerna 项目搭建完成,此处源代码见 lerna-demo。