【第三期】使用lerna管理常用工具库

5,433 阅读6分钟

在工作中我们有时会写一些常用的库,比如包含数据类型判断、cookie存储模块的工具库等,但可能在某些业务场景中,并不需要用到所有的模块。

我们通常会将这个库拆分成多个,分别创建git仓库,分别打包上传到npm,这样做看起来并没有什么问题。

但当多个库之间产生依赖的时候,问题就就会显露出来;你需要打包发布修改后的库,还需要修改所有依赖库的版本号,重新发包。

可想而知,当库多起来后,这个过程将会变得多么繁琐。

那么有什么好的方式来解决呢?Lerna正适合这样的应用场景。

lerna是什么

Lerna是一个用于管理具有多个包的JavaScript项目的工具,它采用monorepo(单代码仓库)的管理方式。

将所有相关module都放到一个repo里,每个module独立发布,(例如BabelReactjest等),issue和PR都集中到该repo中。

你不需要手动去维护每个包的依赖关系,当发布时,会自动更新相关包的版本号,并自动发布。

Lerna项目文件结构:

├── lerna.json
├── package.json
└── packages
    ├── package-a
    │   ├── index.js
    │   └── package.json
    └── package-b
        ├── index.js
        └── package.json

lerna主要做了什么

  • 通过lerna bootstrap 命令安装依赖并将代码库进行npm link
  • 通过lerna publish发布最新改动的库

如何使用

安装

npm install --global lerna
#or
yarn global add lerna

初始化一个项目

mkdir demo 
cd demo
lerna init  

执行后将生成以下目录:

├── lerna.json # lerna配置文件
├── package.json
└── packages # 包存放文件夹

Lerna有两种管理项目的模式:固定模式或独立模式

固定模式

固定模式是默认的模式,版本号使用lerna.json文件中的version属性。执行lerna publish时,如果代码有更新,会自动更新此版本号的值。

独立模式

独立模式,允许维护人员独立的增加修改每个包的版本,每次发布,所有更改的包都会提示输入指定版本号。

使用方式:

lerna init --independent

修改lerna.json中的version值为independent,可将固定模式改为独立模式运行。

lerna配置解析

{
  "npmClient": "yarn", // 执行命令所用的客户端,默认为npm
  "command": { // 命令相关配置
    "publish": { // 发布时配置
      "ignoreChanges": ["ignored-file", "*.md"], // 发布时忽略的文件
      "message": "chore(release): publish" // 发布时的自定义提示消息
    },
    "bootstrap": { // 安装依赖配置
      "ignore": "component-*", // 忽略项
      "npmClientArgs": ["--no-package-lock"] // 执行 lerna bootstrap命令时传的参数
    }
  },
  "packages": [ // 指定存放包的位置
    "packages/*"
  ],
  "version": "0.0.0" // 当前版本号
}

共用devDependencies

开发过程中,很多模块都会依赖babeleslint等模块,这些大多都是可以共用的,

我们可以通过lerna link convert命令,将它们自动放到根目录的package.json文件中去。

这样做即可以保证每个依赖的版本统一,也可以减少存储空间,减少依赖安装的速度。

注意: 一些npm可执行的包,仍然需要安装到使用模块的包中,才能正常执行,例如jest

使用yarn Workspaces

工作区是设置软件包体系结构的一种新方式,只需要运行一次 yarn install 便可将指定工作区中所有依赖包全部安装。

优势

  • 依赖包可以链接在一起,这意味着你的工作区可以相互依赖,同时始终使用最新的可用代码。 这也是一个比 yarn link 更好的机制,因为它只影响你工作区的依赖树,而不会影响整个系统。
  • 所有的项目依赖将被安装在一起,这样可以让 Yarn 来更好地优化它们。
  • Yarn 将使用一个单一的 lock 文件,而不是每个包都有一个,这意味着拥有更少的冲突和更容易的进行代码检查。

如何使用

package.json 文件中添加以下内容:

package.json

{
  "private": true,
  "workspaces": ["packages/*"]
}

注意:private: true 是必需的!工作区本身不应当被发布出去,所以我们添加了这个安全措施以确保它不会被意外暴露。

lerna中使用

需要在lerna.json文件中增加以下配置来启用yarn workspaces:

{
  "useWorkspaces": true
}

创建模块

lerna create package-a

执行上面的命令,会在package文件夹下创建模块,并根据交互提示生成对应的package.json

生成目录结构如下:

├── lerna.json
├── package.json
└── packages
    └── package-a
    		├── __tests__
    		│    └── name.test.js
    		├── lib
    		│    └── name.js
        ├── package.json
        └── README.md

添加依赖

将模块package-a添加到package-b模块依赖中

larna add package-a --scope=package-b

添加完成后会在package-bpackage.json中增加以下依赖项

{
  "dependencies": {
    "package-a": "file:../package-a"
  }
}

包依赖使用file:来指定本地路径文件

发布

发布时,需要先提交commit代码,然后执行lerna publish命令,提示选择版本号:

这里选择Patch,然后会提示,哪些包会升级到1.0.1

接着根据提示选择确认即可发布成功。

也可以使用lerna publish -y默认选项全部选择Yes,并根据commit信息自动升级版本号。

Lerna Changelog

lerna自带生成Changelog的功能,只需要通过简单的配置就可以生成CHANGELOG.md文件。

配置如下:

{
  "command": {
    "publish": {
      "allowBranch": "master", // 只在master分支执行publish
      "conventionalCommits": true, // 生成changelog文件
      "exact": true // 准确的依赖项
    }
  }
}

配置后,当我们执行lerna publish后会在项目根目录以及每个packages包下,生成CHANGELOG.md

注意: 只有符合约定commit提交才能正确生成CHANGELOG.md文件。

如果提交的commitfix会自动升级版本的修订号;

如果为feat则自动更新次版本号;

如果有破坏性的更改,则会修改主版本号

Lerna与Jest集成

在包发布之前,为了保证代码的质量,都需要来编写单元测试,为了提高效率并方便测试运行,我们想要做到以下功能:

  • 所有包只维护一份公共的jest配置文件
  • 可以整体运行所有单元测试
  • 可以只对某个包执行单元测试

jest配置

在项目根目录配置jest.config.js文件如下:

const path = require('path')
module.exports = {
  collectCoverage: true, // 收集测试时的覆盖率信息
  coverageDirectory: path.resolve(__dirname, './coverage'), // 指定输出覆盖信息文件的目录
  collectCoverageFrom: [ // 指定收集覆盖率的目录文件,只收集每个包的lib目录,不收集打包后的dist目录
    '**/lib/**',
    '!**/dist/**'
  ],
  testURL: 'https://www.shuidichou.com/jd', // 设置jsdom环境的URL
  testMatch: [ // 测试文件匹配规则
    '**/__tests__/**/*.test.js'
  ],
  testPathIgnorePatterns: [ // 忽略测试路径
    '/node_modules/'
  ],
  coverageThreshold: { // 配置测试最低阈值
    global: {
      branches: 100,
      functions: 100,
      lines: 100,
      statements: 100
    }
  }
}

编写测试脚本

新增scripts文件夹,添加test.js文件:

const minimist = require('minimist')
const rawArgs = process.argv.slice(2)
const args = minimist(rawArgs)
const path = require('path')
let rootDir = path.resolve(__dirname, '../')
// 指定包测试
if (args.p) {
  rootDir = rootDir + '/packages/' + args.p
}
const jestArgs = [
  '--runInBand',
  '--rootDir', rootDir
]

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

require('jest').run(jestArgs)

该脚本通过解析命令行参数-p来决定执行指定包的测试用例,如果没有指定-p参数,则执行全部测试用例。

修改根目录下package.jsonscript增加如下命令:

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

运行测试脚本:

# 执行全部测试
yarn ut

# 执行某个包测试
yarn ut -p package-a

Lerna与webpack集成

发包时,会需要使用webpack进行es6转码或压缩打包,如果每个都维护一份配置文件,就会很繁琐;我们有与jest相同的需求:

  • 只用一份webpack配置文件
  • 可以一次性将所有的模块分别打包
  • 也可以单独对指定模块打包

webpack配置

在根目录创建webpack.config.js文件,如下:

var path = require('path')
const CleanWebpackPlugin = require('clean-webpack-plugin')

module.exports = (opt) => {
  return {
    mode: 'production',
    entry: path.resolve(opt.path, './lib/index.js'),
    output: {
      path: path.resolve(opt.path, './dist'),
      filename: `${opt.name}.min.js`,
      library: opt.name,
      libraryTarget: 'umd',
      umdNamedDefine: true
    },
    externals: opt.externals,
    plugins: [
      new CleanWebpackPlugin()
    ],
    module: {
      rules: [
        {
          test: /\.js$/,
          loader: 'babel-loader',
          include: [path.resolve(opt.path, './lib')],
          options: {
            // 指定babel配置文件
            configFile: path.resolve(__dirname, '.babelrc')
          }
        }
      ]
    },
    optimization: {
      minimize: true
    }
  }
}

这个配置文件是一个函数,通过接受一个参数对象,来返回最终的配置内容,

编写build脚本

思路:

  1. 读取packages目录下的所有模块,获取模块的路径
  2. 读取模块下的package.json,获取name及依赖项
  3. 通过模块路径、包名和package.json中的dependencies参数来获取webpack配置
  4. 通过webpackNode API执行配置编译打包
  5. 根据命令行参数,判断执行需要打包的配置文件(单独打包)

具体实现如下:

/scripts/build.js

const minimist = require('minimist')
const rawArgs = process.argv.slice(2)
const args = minimist(rawArgs)
const webpack = require('webpack')
const webpackConfig = require('../webpack.config')
const fs = require('fs')
const path = require('path')
const packages = fs.readdirSync(path.resolve(__dirname, '../packages/'))

// 获取外部依赖配置
function getExternals (dependencies) {
  let externals = {}
  if (dependencies) {
    Object.keys(dependencies).forEach(p => {
      externals[p] = `commonjs ${p}`
    })
    return externals
  }
}
const packageWebpackConfig = {}

// 遍历所有的包生成配置参数
packages.forEach(item => {
  let packagePath = path.resolve(__dirname, '../packages/', item)
  const { name, dependencies } = require(path.resolve(packagePath, 'package.json'))
  packageWebpackConfig[item] = {
    path: packagePath,
    name,
    externals: getExternals(dependencies)
  }
})

function build (configs) {
  // 遍历执行配置项
  configs.forEach(config => {
    webpack(webpackConfig(config), (err, stats) => {
      if (err) {
        console.error(err)
        return
      }

      console.log(stats.toString({
        chunks: false, // 使构建过程更静默无输出
        colors: true // 在控制台展示颜色
      }))
      if (stats.hasErrors()) {
        return
      }
      console.log(`${config.name} build successed!`)
    })
  })
}

console.log('\n===> running build')

// 根据 -p 参数获取执行对应的webpack配置项
if (args.p) {
  if (packageWebpackConfig[args.p]) {
    build([packageWebpackConfig[args.p]])
  } else {
    console.error(`${args.p} package is not find!`)
  }
} else {
  // 执行所有配置
  build(Object.values(packageWebpackConfig))
}

然后在根目录下package.jsonscript增加如下命令:

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

运行构建脚本:

# 全部打包
yarn build

# 指定打包
yarn build -p package-a

至此,Lerna的使用方法就介绍完成了。


水滴前端团队招募伙伴,欢迎投递简历到邮箱:fed@shuidihuzhu.com