系列

因为内容太多,文章分开写的,目录如下:

说明

lerna 是一个用来管理和发布基于 git & npm 的多包项目工具,基于 lerna 能够非常方便的管理多包项目,利于协作与本地开发,及 npm 发布等

从包 link 模式上来说, lerna 的部分能力可以简单理解为 npm link,因为通过 lerna 可以因为某个 npm package 的本地代码。

从项目场景来说,lerna 非常适合多包依赖(并且包之间也存在互相引用)的场景,基于 lerna 能够统一发布项目中的npm 包,而不需要手动发布和修改版本号的。

高级使用中,可以基于 git 提交自动发包或者其他方面。

lerna 需要全局安装,并提供了强大的 cli 命令:

npm i -g lerna

基于 lerna 初始化项目: lerna init

$ lerna init
lerna notice cli v3.22.1
lerna info Creating package.json
lerna info Creating lerna.json
lerna info Creating packages directory
lerna success Initialized Lerna files

通过 init 初始化的 lerna 项目,默认会初始化三个文件:

  • packages:包目录
  • package.json:项目整体的 package
  • lerna.json:lerna 配置

66948-93dxcjn1cks.png

lerna create <pkg-scope>/<pkg-name>:创建一个包

$ lerna create @plui/builder-runtime
info cli using local version of lerna
lerna notice cli v3.22.1
package name: (@plui/builder-runtime)
version: (0.0.1)
description: plui, builder runtime
keywords: plui
homepage:
license: (ISC)
entry point: (lib/builder-runtime.js)
git repository: (https://github.com/xxxxxxxxxxx.git)

上面的命令 lerna create @plui/builder-runtime 是使用 lerna 在 packages 中创建了一个本地的包,这个包将来可能发布到 npm ,并配其他包引用。

可以发现填写的信息和 npm init 差不多,而 git repo 则是根据当前的 git 自动生成的。

06850-wz7xo90xtzd.png

@lerna/create 命令属性

可以关注下 --es-module ,直接安装一个初始化转义的 ES Module,导入导出都是 ES Module 风格,不过要编译这个 ES Module 需要编译支持。

26784-1xrhiksyekn.png

创建一个 ES6 模块

一般可以借助 babel 编译一个简单的 ES6 模块,这个过程用到了 lerna run 命令和 @babel/cli

比如上面截图创建的这个包 package.json 核心内容如下:

其中 scripts 是我后面添加的,主要看下 main,他的入口是 dist,而不是 src,因此如果没有对这个模块进行编译,则无法找到模块。

{
    "name": "@plui/component-input",
    "main": "dist/component-input.js",
    "module": "dist/component-input.module.js",
    "directories": {
        "lib": "dist",
        "test": "__tests__"
    },
    "scripts": {
        "test": "eco \"Error: run tests from root\" && exit 1",
        "build": "babel src -d dist",
        "prepublish": "yarn build"
    }
}

基于 babel 对模块构建后,会在 dist/index.js 构建出 ES5 代码:

"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports["default"] = componentInput;

function componentInput() {// TODO
}

然后就可以直接引用,同时,这个命令在发布的时候也会将 npm 包进行编译。(这块在后面详细讲)

关于包的创建

一般来说,项目中很少直接使用 lerna create 创建,因为依赖项包括一些其他的模块目录配置比较复杂。

基本都会使用模板引擎(yeoman)或者是自己手写 script 接收入参去做一些事情。

lerna ls or lerna list

这个命令用于查看 packages 下面有哪些包,示例:

$ lerna ls
info cli using local version of lerna
lerna notice cli v3.22.1
@plui/builder-core
@plui/builder-runtime
@plui/builder-utils
@plui/component-button
@plui/component-input
lerna success found 5 packages

lerna ls 的会对包列表进行合法检查,如果 package 存在问题会进行提示:

  • package.json 本身不存在或者是 name 不存在,会自动忽略这个包
  • 如果存在两个 name 相同的 package,会直接报错,比如下面的报错:
lerna ERR! ENAME Package name "@plui/component-input" used in multiple packages:

为什么 lerna ls 看到 package 但是无法引入使用?

第一次使用 lerna 的时候可能会有一个疑问,通过 lerna ls 命令能够查看到某个 package,但是在代码使用的时候,却提示错误,无法使用。

packages/* 创建了一个包,只是声明,但在使用的时候,需要显示的挂载到目录中。

其实和 npm link 比较像,类似一种软链实现

lerna bootstrap :挂载 package

上面提到,在 packages/* 下面创建的包,无法直接引入,构建工具会报错,需要对 package 进行挂载。

这个命令的功能类似于 link,它可以将 packages/* 下的包映射到全局的 node_modules 中,能够在 packages/* 之外,或者是之内的任意地方通过 node_modules/的包名引入本地工程的包,而不需要通过相对路径。

基于 lerna bootstrap 挂载后

挂在完之后,在 node_modules 中即可看到我们的包,从而能够使用这个包

94817-jji13p3wkl.png

--hoist: 收敛包依赖管理

通常一个包会有多个依赖,多个包会有相同依赖,如果在每个 package 里面安装,一方面会增加多个 node_modules,安装好几次同一个依赖,也不方便统一管理依赖版本号。

通过 --hoist 能够很好的管理不同的包的全局依赖,如果某些包需要安装依赖,则通过 lerna bootstrap --hoist 去安装即可。

--hoist 还有一个很棒的能力,是将所装依赖的 sh 命令工具安装到各自的 node_modules/.bin 中,这样每个包都可以非常方便的执行 sh 命令或者脚本。

关于 hoist:

https://github.com/lerna/lerna/blob/main/doc/hoist.md

实际项目中,我很少使用这个属性,因为它和 yarn workspace 是冲突的,如果你的 npmClient=yarn ,则无法使用 hoist

他们的作用差不多,yarn 通过 workspace 也能非常友好的管理不同 package 的依赖,并且都将依赖安装在全局根目录的 node_modules 中。

使用 yarn workspace 而不是 --hoist

关于 yarn workspaces

https://classic.yarnpkg.com/en/docs/cli/workspace

lerna.json 中可以配置:

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

声明 npmClient 使用 yarn,并且 workspace 是通过 packages/ 目录下各个 package 名称作为每一个 workspace。

查看项目中所有 workspaces

$ yarn workspaces info
yarn workspaces v1.22.10
{
  "@plui/builder-core": {
    "location": "packages/builder-core",
    "workspaceDependencies": [],
    "mismatchedWorkspaceDependencies": []
  }
}

在某个 workspace 安装依赖

比如下面安装三个 babel 依赖,每个包用来进行构建的时候会使用到

yarn workspace @plui/component-button add -D @babel/cli @babel/core @babel/preset-env

安装完之后,会自动执行 yarn build 如果存在,完成可用产物构建,比如 ES6 ==> ES5

06100-1vfhi5a3esx.png

在某个 workspace 执行 npm script

命令格式:yarn workspace <workspace_name> run <scripts>

$ yarn workspace @plui/component-button run build
yarn workspace v1.22.10
yarn run v1.22.10
warning package.json: No license field
$ babel src -d dist
Successfully compiled 1 file with Babel (790ms).
Done in 1.23s.
Done in 1.64s.

yarn workspaces 与 lerna --hoist 对比

从整体使用体验上来说,workspaces 要比 --hoist 使用顺畅和方便很多,毕竟有 yarn 在背书,整体实现及流畅度体验都比较友好。

lerna run <xxx>:每个 package 运行 scripts 命令

关于 lerna run 命令的各种命令行属性如下:

通过 lerna run xx 可以按照顺序依次执行各个 package 的 scripts 命令,一般在统一构建或者是运行测试脚本的时候非常有用。

示例:下面代码本质上执行的是 babel src --out-dir dist,这个命令是在 @plui/builder-core 定义的简单使用 babel 编译 ES6 到 ES5 的代码

$ lerna run build
info cli using local version of lerna
lerna notice cli v3.22.1
lerna info Executing command in 1 package: "yarn run build"
lerna info run Ran npm script 'build' in '@plui/builder-core' in 1.4s:
yarn run v1.22.10
$ babel src --out-dir dist
Successfully compiled 1 file with Babel (761ms).
Done in 1.18s.
lerna success run Ran npm script 'build' in 1 package in 1.4s:
lerna success - @plui/builder-core

运行完脚本之后:

08921-fr6bk4k2wc.png

lerna exec -- <command>:每个 package 运行 shell 命令

lerna run 不同的是,lerna run 入参是一个定义在每个 package 的 package.jsonscripts 命令

lerna exec 则是在每一个 package 的目录下执行一条 shell 命令,比如:

lerna exec -- echo "x" > readme.md 是给每个 package 创建一个 readme 文件

lerna add <package>[@version] [--dev] [--peer]:为每个 package 安装依赖

相关入参:https://github.com/lerna/lerna/tree/main/commands/add#readme

上面介绍了通过 lerna execyarn workspace 对不同的 package 可以进行不同的动作或者统一的动作。

lerna add 则专注于给每个 package 安装统一的依赖,可以理解为 lerna addlerna exec 的上层封装。

这里主要说下两个入参:

  • --dev
  • --peer

需要注意的是,通过 lerna add 一次只能安装一个包

--dev :devDenpendencies

安装依赖的时候如果是安装到开发依赖,则可以通过 --dev 控制,比如要给每一个 package 安装 babel 相关的依赖。

执行:lerna add @babel/core --dev

$ lerna add @babel/core --dev
info cli using local version of lerna
lerna notice cli v3.22.1
lerna info Adding @babel/core in 2 packages
lerna info bootstrap root only

lerna clearn:清理所有 package 的 node_modules

通过 lerna clearn 可以清理每个 package 下面的 node_modules 目录

lerna import :基于 git commit 引入包到当前项目仓库

https://github.com/lerna/lerna/tree/main/commands/import#readme

# Getting started with Lerna
$ git init lerna-repo && cd lerna-repo
$ npx lerna init
$ npm install

# Adding a commit
$ git add .
$ git commit -m "Initial lerna commit" # Without a commit, import command would fail

# Importing other repository
$ npx lerna import <path-to-external-repository>

使用场景:

有些场景中,大家一开始可能不在一个项目仓库开发,但是后面需要移动到统一的项目仓库(lerna管理的项目目录).

在原来项目模块开发完成后,可以基于 git commit 将 package 引入到当前 lerna 下面的 packages ,成为当前项目目录的一个包,然后统一管理。

示例:

在 test/ 目录下开发一个模块:

01962-e3ds536sqe.png

在 lerna 项目中引入这个模块:

$ lerna import ../test/
info cli using local version of lerna
lerna notice cli v3.22.1
lerna info About to import 1 commits from ../test/ into packages\test
? Are you sure you want to import these commits onto the current branch? Yes
lerna info f057af0
lerna success import finished

lerna link:所有互相引用的 packages 建立软链

和 npn link 差不多,实际项目中我使用的其实比较少

文章已经结束啦