Lerna多包管理的搭建指南

什么是lernaJs

lernaJs是由Babel团队出的一个多包管理工具。因为Babel包含很多子包,以前都是放在多个仓库里的,管理比较困难,特别是有调用系统内包的时候,发布比较麻烦。所以为了能更好更快的夸包管理,babel推出了lernaJs,使用了monorepo的概念,现在React,Babel,Angular,Jest都在使用这个工具来管理包。

lerna官网: lerna.js.org/
lerna仓库: github.com/lerna/lerna…

什么是monorepo?

monorepo是比较于multi-package的。multi-package就是建立多个仓库,每个包建立一个仓库。而monorepo是建立一个仓库,多个包都在这个仓库中管理,这样可以有两个好处:

  1. 各个包之间的沟通更加的方便,如果multi-package的话,系统内其中一个包修改,需要单独发版,而且引用这个包的其他包都需要发版。使用lerna的话可以自动管理这些包的发版,很方便
  2. 一些可以共用的配置,比如eslint,babel,rollup等,可以统一的管理这些开发配置

如何搭建环境

安装和初始化lerna项目

安装lernajs

npm install lerna -g

初始化lernaJs项目

lerna init

执行后,会自动生成一个由lerna管理的monorepo。 其文件结构如下:

lerna-repo/
  packages/          // 子包都放在这个目录中
  package.json
  lerna.json         // lerna js的配置文件

添加子包

环境初始好以后,首先需要添加一个子包,添加子包的命令如下:

lerna create <pkgName>

执行这个命令后,会问一些包名啊,版本等问题和执行npm init后问的差不多,填完这些问题后包就会自动创建一个子包。 子包默认的目录结构如下:

<packName>
  __tests__         // 测试文件
  lib               // 包的入口文件默认在这个目录下
  package.json
  README.md

怎么给子包添加依赖

添加依赖的命令是:

lerna add <moduleName>  // 所有子包都添加这个依赖
lerna add <moduleName> --scope = <pkgName> // 给scope后的包添加依赖
lerna add <pkgName1> --scope = <pkgName2> // 给pkgName2中添加pkgName1,包内的互相引用,会复制pkgName1到pkgName2中

所有子包更新依赖

lerna bootstrap

如何打包

因为需要打包的只是纯js文件,我这里选择了rollup,webpack的配置思路是一样的。 先介绍一下想实现的效果。

  • 每个子包的src/index.js是源代码的入口文件
  • 每个子包的lib/index.js是打包后的文件,也是这个包引用时的入口
  • 每个子包的配置是基本一样的,比如用babel转换,都会压缩代码
  • npm run build packName 打包指定的包
  • npm run dev packName 开发指定的包

先在根目录建立一个script目录,该目录下会包含一些开发和打包需要的配置文件。之后在script下建一个build.js,用来配置rollup。 在build.js中,要用到rollup的javascript api。关于rollup的配置可以看rollup的官网: www.rollupjs.org

执行以下命令安装必要的包:

npm i rollup rollup-plugin-babel rollup-plugin-commonjs rollup-plugin-terser --save-dev

build.js的主要代码如下:

const rollup = require('rollup'); // 引入rollup
const terser =require('rollup-plugin-terser').terser // 压缩代码的插件
const commonjs = require('rollup-plugin-commonjs') // rollup默认支持es6的模块系统,需要支持commonjs的话需要这个插件
const babel = require('rollup-plugin-babel') // rollup的babel 插件 
const args = process.argv[2] // 拿到 npm run build packName 中的packName

const projectPath = `./packages/${args}` // 子包所在的路劲

// 输入的文件配置
const inputOptions = {
  input: `${projectPath}/src/index.js`,
  plugins: [
    babel({ // babel文件的设置,会读取根目录的babel.config.js文件配置
      runtimeHelpers: true,
      exclude: 'node_modules/**'
    }),
    commonjs(),
    terser()
  ]
};
// 输出的配置
const outputOptions = {
  file:  `${projectPath}/lib/index.js`,
  format: 'esm',  // 引出的方式为es6的方式
  name: `${args}` // 输出可引用名为package的名字
};

async function build() {
  // create a bundle
  const bundle = await rollup.rollup(inputOptions); // inputOptions放在这里

  console.log(bundle.watchFiles); // an array of file names this bundle depends on

  await bundle.write(outputOptions); // outputOptions放在这里
}

build();

编辑完这个文件后,可在package.json的scripts下添加命令,来执行这个build文件

"scripts": {
    "build": "node script/build.js",
}

如何测试

测试选用了facebook的jest框架,对于测试有以下要求:

  • npm run test packName 运行这个子包下的所有测试
  • npm run test packName testFileName 因为每个子包包含多个测试文件,这个命令用来运行这个包下的测试文件

和上面配置rollup一样,也需要用jest的javascript api。 在script下添加test.js文件

const rawArgs = process.argv[2]      // 获取包名 
const testFile = process.argv[3]|| ''    // 获取测试文件名
const path = require('path')
let rootDir = path.resolve(__dirname, '../')


rootDir = rootDir + '\\packages\\' + rawArgs // 拼出包的路劲

const jestArgs = [
  '--runInBand',
  '--rootDir', rootDir,  // 传入包路径
  testFile?`${testFile}.spec.js`:'' // 
] // jest 的一些配置

console.log(`\n===> running: jest ${jestArgs.join(' ')}`)

require('jest').run(jestArgs) // 执行

然后在package.json加入以下script:

"scripts": {
    "test": "node script/test.js"
}

添加eslint和commitlint

添加eslint和commitlint这里比较简单, 这里提一点就是因为Monorepo的结构,所以做一次eslint和commitlint的配置就好,所有的子包都会使用这个配置。

如何发布

发布的命令是

lerna publish

在lerna.json中可以设置版本号的规则

// lerna.json

"version": "0.0.3" // 如果是数字,就是所有子包都是这个版本。设置成‘independent’,独立管理每个包的版本

lernajs 会对比包的变化,自动发布需要发布的子包。

自动添加CHANGELOG

在lerna.json中添加如下设置:

// lerna.json

"command": {
  "publish": {
    "conventionalCommits": true
  }
}

这样就能在发布时自动添加CHANGELOG了

总结

这样我们就实现了多包的monorepo的结构,可以通过命令开发,打包,测试其中的每个子包。所有子包的eslint,babel,jest的配置都是一样的。可以方便的一个命令发版,一个命令更新和添加依赖。

最终文件结构

docs
|---README.md
packages
|---packageOne
|---packageTwo
      .
      .
      .
script
|---build.js
|---test.js
.commitlintrc.js
.eslintignore
.eslintrc
.gitignore
babel.config.js
lerna.json
package.json

使用命令

关于包的命令

新建一个包:

lerna creat <packname>

发布

lerna publish

所有包更新node_modules

lerna bootstrap

A包中添加B包

lerna add pack-B --scope = pack-A

关于开发的命令

构建其中一个包:

npm run build <packname>

测试其中一个包

npm run test <packname>

开发其中一个包

npm run dev <packname>