Lerna 多 package 源代码管理方案

Lerna 多 package 源代码管理方案

说明

Lerna是一个用于管理包含多个软件包(package) 的 JavaScript 源代码管理方案

参考

  • Lerna 中文官网
  • Yarn wrokspace
  • lerna+yarn workspace+monorepo项目的最佳实践
  • Lerna 配置详解
  • lerna 指令总览 中文

1. 为什么要将项目拆分成多个 package ?

  1. 当项目越来越大,拆分成多个 package 有利于降低软件的复杂度
  2. 作为工具类、或者组件类项目,有助于使用者按照需要引用合适的 package (按需加载,减少打包体积)

2. 多 package 的代码架构怎么维护? 多仓库还是单仓库?

首先,既然分了多个 package 简单的方式就是每个 package 由各自的 代码仓库管理,这种方式在 package 数量较少时比较方便

  1. 但是一旦 package 数量变多,每个 package 都有各自的 node_modules 安装时需要依次安装,不但耗时而且安装的内容容易重复且占用过多空间;
  2. 由于拆到了多个代码仓库,issue 会比较分散,但 package 之间又有关联关系,这样就会导致问题难以统一处理
  3. 公共模块 package 一旦更新,所有其他 package 都要手动更新一遍版本

单仓库,开发时多个 package 的代码放到一个文件夹下,通过 npm 将这些文件夹在的模块安装成 local module 来使用;这样 子 package 就会作为一个模块引入到了父项目中,开发时就如同在一个项目开发一样

  1. 这样做与 多仓库相比有一个缺陷:多仓库可以依据不同仓库进行不同 package 的开发及发布权限控制,但是单仓库的话只能整体做一个权限控制

Monorepo 是针对单仓库的流行解决方案,这套方案旨在兼顾模块化的优势前提下提供简单的源代码管理方法,monorepo 的主要两个实现 Yarn workspaces 和 lerna

3. NPM 模块中的 scope package

我们使用第三方包时经常能看到类似如下命名的包,这些包都会被下载到 node_modules下 名为 @babel 的目录下边,这种使用方式称为 scope package 它是一种把相关模块组织到一起的方法

"@babel/core": "^7.10.4",
"@babel/preset-env": "^7.10.4",
"@babel/preset-flow": "^7.10.4",
"@babel/preset-react": "^7.10.4",
"@babel/preset-typescript": "^7.10.4",

scope package 的安装与普通的 package 安装一样,使用时需要带上 scope 即 import xxx from '@babel/core';

scope package 的发布也与普通模块类似,它们的主要区别是

  • 包的 package.json 中 name 字段为 scope 的格式 "name": "@mjz-scope/package-name"
  • 首次发布时追加 npm publish –access public (因为 scope package 默认时受限制的,如果没有私有的 npm 账户会发布不出去,所以这里将其设置为 public 即可)

scope package 与我们的 “源代码管理方案” (即:单仓库还是多仓库)并们有太大关系,因为无论怎样组织源代码,最后的发布的包还是通过 npm publish 发布到 registory 中,但是如果我们使用了 scope package 的方案,那么我们的“源代码” 与 “npm package” 在组织结构上就会更一致,使用相关性会更强一些。

哪些项目在用?

  • lerna : Babel 、material-ui、create-react-app
  • yarn workspace: React
  • 类似实现方案: Vue、Element-ui

Lerna 项目从 0-1 搭建

到底是使用 Yarn workspace 还是使用 lerna, 答案是两个一起使用,主要原因是两者各有优势

  1. Yarn workspace 在依赖管理上做的更好,可以智能的优化依赖关心的安装
  2. Lerna 在版控制方面更好

因此最终的方案是,lerna 只负责创建 package 以及 package 的版本控制,开发过程中涉及到 安装依赖或者删除依赖等都使用 yarn 的命令(尽量不要混用)

包管理结果:

  • 依赖版本的 lock 文件只存在于更目录的 yarn.lock 中,子项目中不会有
  • 所有依赖都优先安装到根目录的 node_modules 下,除非不同的子项目中需要安装不同的版本
  • 如果不同依赖版本各自有多个 package 使用,那么最多引用的会放到根目录 node_modules 下
  • 清空 所有 node_modules 后,重新 yarn install 时就会重新计算依赖的安装位置,达到最优解

常用命令

  • lerna create [packageName] 创建一个 package
  • yarn workspace [packageName] [add|remove] lodash 给某个 package 中安装或删除依赖
  • yarn workspaces run [add|remove] xxx 给所有 package 中安装或删除依赖,会安装到每个子 package 的 package.json
  • yarn add -W -D typescript 给 root 安装依赖,会安装到 根目录 package.json
  • lerna clean && rm -rf ./node_modules 清除安装的依赖

最终的目录结构

.
├── README.md
├── lerna.json
├── node_modules
│   ├── @mjz-test
│   ├── lodash
├── package.json
├── packages
│   ├── lerna-package-1
│   ├── lerna-package-2
│   ├── lerna-package-3
│   └── lerna-package-4
└── yarn.lock

0. 确保项目链接到 Git

因为 lerna 会根据 git 动态发布,所以先要确保项目中有 git

git init lerna-test && cd lerna-test
git add README.md
git commit -m "first commit"
git branch -M main
git remote add origin [email protected]:mjzhang1993/lerna-test.git
git push -u origin main

1. 初始化项目,转变项目为一个 Lerna 仓库

# 全局安装 lerna
npm install lerna -g
# 全局安装 yarn 
brew install yarn # 如果使用 nvm 等 node 版本管理工具则需要追加 --without-node 
# 初始化项目,将仓库转变为 lerna 仓库
yarn init
lerna init

可以看到,项目中新增了 lerna.json package.json 以及 packages 空文件夹;
更改 package.json 追加 private: true 这一项,这表示当前这个根目录所在的区域是不应该被 npm 发布的; 追加 workspaces: [] 开启 yarn 的 workspaces 模式

{
  "name": "lerna-test",
  "version": "1.0.0",
  "private": true, 
  "workspaces": [ // 同时这里也要设置 workspaces (yarn)
    "packages/*"
  ],
  "devDependencies": {
    "lerna": "^3.22.1"
  }
}

更改 lerna.json

{
  "packages": [
    "packages/*"
  ],
  "version": "0.0.0",
  "npmClient": "yarn",
  "useWorkspaces": true,
}

2. packages 目录下创建三个 package

为了测试及演示,创建以下三个 package

在创建之前我们要先确定,创建的三个 package 要发布到那个 registry(镜像仓库) 中

# 如果目标的 registry 就是 npm 的官方镜像则不需要设置
yarn config set registry https://registry.npm.xxx.com
# 登录到 镜像仓库
yarn login # 或者执行 npm adduser --registry=https://registry.npm.xxx.com
1. 创建 lerna-package-1
# 创建一个带 scope 的 npm package
lerna create @mjz-test/lerna-package-1

命令执行后可以在 packages 目录下看到一个 名为 lerna-package-1 的 目录就创建好了,这个目录对应的 package.name 则为 @mjz-test/lerna-package-1,接下来我们给这个模块添加依赖

yarn workspace @mjz-test/lerna-package-1 add lodash

执行后可以看到,lodash 被安装到了 根目录的 /node_modules 下边,但是 lodash 的安装记录被记录到了 /packages/lerna-package-1/package.json

2. 同样的方式创建 lerna-package-2
lerna create @mjz-test/lerna-package-2

然后我们给 lerna-package-2 添加 lodash 依赖,

yarn workspace @mjz-test/lerna-package-2 add lodash

结果不会有新的 lodash 被安装,但是 /packages/lerna-package-2/package.json 中同样会被记录 lodash 的安装,也就是说,多个package 使用同一个 依赖不会被重复下载,但是会被各自package 记录

下面我们给 lerna-package-2 添加 lerna-package-1 作为依赖

# 注意:被安装的内部依赖一定要加版本号,否则,yarn 会到 registry 获取对应的包
yarn workspace @mjz-test/lerna-package-2 add @mjz-test/[email protected]

以上操作后 /packages/lerna-package-2/package.json 中增加了对 @mjz-test/lerna-package-1 的依赖,而在 root 层的 node_modules 中 @mjz-test/lerna-package-1 是通过软连接的方式连接到了 packages/ 下对应的包中的

2. 同样的方式创建 lerna-package-3
lerna create @mjz-test/lerna-package-3

然后我们给 lerna-package-2 添加 lodash 另外一个版本的依赖

yarn workspace @mjz-test/lerna-package-3 add [email protected]

执行后 [email protected] 由于只有 @mjz-test/lerna-package-3 使用,其他两个 package 使用的都是 lodash@4 ,因此它被安装到了 /packages/lerna-package-2/node_modules/ 下边

3. 项目构建

由于当前三个 package 之间有引用关系,例如 @mjz-test/lerna-package-2 引用 @mjz-test/lerna-package-1, 这个时候如果先构建了 @mjz-test/lerna-package-2 就会出错,lerna 提供了一个可以按照依赖顺序打包的能力,我们可以尝试在 @mjz-test/lerna-package-1 中把 @mjz-test/lerna-package-3 作为依赖(为了更清楚的展示)

yarn workspace @mjz-test/lerna-package-1 add @mjz-test/[email protected]

然后给每个 package 增减一个 build 的 script, 每个脚本都只要返回一个字符串即可

"scripts": {
    "test": "echo \"Error: run tests from root\" && exit 1",
    "build": "node ./lib/lerna-package-3.js"
  },

最后执行顺序构建的命令

lerna run --stream --sort build
# 以下是构建结果,可以看到顺序依次是 package-3 -> package-1 -> package-2 与依赖的顺序一致
@mjz-test/lerna-package-3: yarn run v1.15.2
@mjz-test/lerna-package-3: $ node ./lib/lerna-package-3.js #
@mjz-test/lerna-package-3: this is running in package 3
@mjz-test/lerna-package-3: Done in 0.12s.
@mjz-test/lerna-package-1: yarn run v1.15.2
@mjz-test/lerna-package-1: $ node ./lib/lerna-package-1.js
@mjz-test/lerna-package-1: this is running in package 1
@mjz-test/lerna-package-1: Done in 0.14s.
@mjz-test/lerna-package-2: yarn run v1.15.2
@mjz-test/lerna-package-2: $ node ./lib/lerna-package-2.js
@mjz-test/lerna-package-2: this is running in package 2
@mjz-test/lerna-package-2: Done in 0.14s.

4. 版本发布

1. 首先,修改当前的配置
  1. 首先更改 lerna.json 配置文件,配置要发布到那个地址
{
  "packages": [
    "packages/*"
  ],
  "version": "0.0.1",
  "npmClient": "yarn",
  "useWorkspaces": true,
  "command": {
    "publish": {
      "ignoreChanges": ["*.md"],
      "registry": "https://registry.npmjs.org/"
    }
  }
}
  1. 如果我们使用了类似 @mjz-test/lerna-package-3 这种 scope package 还需要手动给, package 所在目录的 package.json 设置如下信息,(原本的 lerna create [packageName ] --access=public 并没有效果,所以要手动设置一次)
    "access": "public"
  }
  1. 如果全局的 registry 与我们要发布的 registry 不是一个,那么我们还需要给每一个 package 设置 registry:
    "access": "public"
    "registry": "https://registry.npmjs.org/" 
  }

关于 registry 如果我们项目内设置了 .npmrc 则会优先使用.npmrc 内的 registry 作为默认创建,如果没有则会使用全局的 npm config get registry 作为默认,如果这两个都不是我们想要的发布地址,那就只能手动设置了

2. 执行 lerna publish
git commit -am "second commit"
lerna publish

结果报错, 原因是我们用了 scope package 与 普通 package 不同的是,scope package 需要到 npm 官网创建 scope 命名空间,(实际就是进入npm orgization 来创建一个组织,例如:我们设计的包名为 @mjz-test/lerna-package-3 , 那创建的这个组织就叫做 mjz-test), 创建好后再次执行,就不会出现下面的报错了

lerna WARN ENOLICENSE See https://choosealicense.com for additional guidance.
lerna http fetch PUT 404 https://registry.npmjs.org/@mjz-test%2flerna-package-3 282ms
lerna ERR! E404 Scope not found
3. 假设 更改 | 上一次 publish | 更改 lerna-package-2 后再 publish
  1. 如果当前最大版本是修订版本(0.0.x)那么即使只有一个 package 改动也会导致所有的 packages 都升一级版本即
  1. 如果当前最大版本是小版本或者大版本,那么只有改动的这个 package 会升级
  1. 另外,不需要担心被内部引用 package 的版本如何处理,实际上 lerna publish 会在执行时会将依赖更新过的 package 的 包中的依赖版本号更新
4. 集中版本号与独立版本号

集中版本号(fixed)是默认的模式,其表现是 所有 package 的版本变更都是依据当前 lerna.json 中的 version 来改动的,即:依照上边的例子,第三次在对 lerna-package-1 进行改动,publish 后 lerna-package-1 的版本号会直接从 v0.1.0 升到 v0.1.2 ,这种版本号升级方式有点不利于语义化

独立版本号(independent)可以解决语义化的问题,即每次升级都会逐个询问需要升级的版本号,其基准为自身的 package.json, 配置独立版本号只需更改 lerna.json 的 version: independent 即可

Lerna 项目搭建后的日常使用

0. 安装lerna 到项目内

为了项目可以多人协同,lerna 安在全局会比较麻烦,所以首先会将 lerna 安装到项目根目录,并将常用的工作流命令存在 scripts 中

# 根目录下安装 lerna
yarn add -D -W lerna
# /package.json 中增加如下 脚本
"scripts": {
    "lerna:create": "lerna create",
    "lerna:build": "lerna run --stream --sort build",
    "lerna:publish": "lerna publish",
    "lerna": "lerna",
    "clean": "lerna clean && rm -rf ./node_modules"
  },

1. 日常使用命令

# clone 下代码后执行安装依赖
yarn install --use-workspaces

# 可能会创建新的 package
yarn lerna:create @mjz-test/lerna-package-4

# 创建后的 package 需要设置一下 package.json 的 publicConfig
# publishConfig": {
#   "access": "public"
#   "registry": "https://registry.npmjs.org/"
# }

# 可能会需要给某个 package 安装依赖
yarn workspace @mjz-test/lerna-package-1 add lodash
yarn workspace @mjz-test/lerna-package-1 remove lodash

# 可能需要给所有 package 安装依赖
yarn workspaces add lodash
yarn workspaces remove lodash

# 可能需要将内部的package 作为依赖安装(注意要加版本号)
yarn workspace @mjz-test/lerna-package-1 add @mjz-test/[email protected]
yarn workspace @mjz-test/lerna-package-1 remove @mjz-test/[email protected]

# 可能需要将依赖安装到根目录
yarn add -D -W lodash

# 可能会需要单独执行某个 package 中的 script 命令
yarn lerna run dev --scope @mjz-test/lerna-package-1

# 可能需要对 node_modules 做清理
yarn run clean

# 开发完成后执行编译
yarn lerna:build

# commit 更改
git commit -am "xxx commit"

# 提交到代码库与 npm
yarn lerna:publish

# 关于提交到版本库的版本,我们假设当前版本为 0.3.3
yarn lerna:publish major # 0.3.3 => 1.0.0
yarn lerna:publish minor # 0.3.3 => 0.4.0
yarn lerna:publish patch # 0.3.3 => 0.3.4
yarn lerna:publish premajor # 0.3.3 => 1.0.0-alpha.0
yarn lerna:publish preminor # 0.3.3 => 0.4.0-alpha.0
yarn lerna:publish prepatch # 0.3.3 => 0.3.4-alpha.0
yarn lerna:publish 1.2.3 # 0.3.3 => 1.2.3 # 也可以是具体的版本号
yarn lerna:publish # 如果什么都不传,则在没有开启 conventionalCommits 的时候弹出命令行提示选择要升级的版本,如果开启了 conventionalCommits 则会根据 commit 信息的 type 做简单的判断输出一个合适的版本

你可能感兴趣的:(前端构建工具,lerna,yarn,workspace,源代码管理,单仓库)