使用verdaccio和lerna和管理npm包

封装公共逻辑,我们往往会通过函数->模块文件->包等方式,本文主要研究如何编写和管理公共模块,包括配置模块入口、选择打包方式、搭建本地npm仓库、多模块项目管理等问题。

<!--more-->

在前端开发中,

  • 在单个项目中,我们可以通过函数或组件的方式封装公共逻辑
  • 在多个业务项目,且各个项目之间存在公共依赖或逻辑,常见的办法是将这些公共依赖封装为模块

本文将从npm包的编写和打包开始,研究如何正确的管理公共模块。

1. npm包入口配置

1.1. main

参考:关于package.json中main字段的指向问题

main字段指向库文件的入口,一般来说有三种选项

  • 指向src源码入口文件,这种情况下需要使用者自行对库进行打包,一般只适用于NodeJS包
  • 指向development环境下对库文件打包的开发版本文件,保留了注释和基本代码格式等信息,方便使用者进行调试,推荐使用这种方式
  • 指向production打包的生产环境,进行了注释移除和代码压缩等工作,只适用于通过script标签引入的使用者。

1.2. module

参考:package.json 中的 Module 字段是干嘛的

在早期,npm包基本是基于CommonJS规范的;在ES6中,JavaScript增加了新的模块机制,更规范,且方便使用Tree shaking优化代码。

库开发者和库使用者的模块系统和构建环境可能是不一样的,因此按照约定,一个模块根据package.json中的main字段查找CommonJS模块入口,根据module字段查找ES6 Module模块入口。

module字段最开始实际上是rollup提出来的,相比webpack而言,roullup对库开发者更友好,因此决定使用其开发公共模块。

1.3. types

参考:官方文档

对于使用TypeScript编写的模块而言,可以通过typestypings字段指定整个模块的入口文件。

1.4. 使用rollup

参考:携手Rollup与TS造轮子

下面展示一个使用rollup打包typescipt编写的模块

mkdir rollup-demo
cd rollup-demo

npm init -y
npm i rollup -D
npm i rollup-plugin-typescript2 typescript

编写配置rollup.config.jstsconfig.json

// rollup.config.js
import typescript from "rollup-plugin-typescript2"; // 支持declaration
export default {
    input: "./src/main.ts",
    plugins: [
        typescript({
            tsconfig: "./tsconfig.json",
            exclude: "node_modules/**"
        })
    ],
    output: [
        {
            format: "es",
            file: "lib/bundle.esm.js",
            sourcemap: true
        }
    ]
};
// tsconfig.json
{
    "compilerOptions": {
        "declaration": true
    }
}

然后编写模块文件

// src/util.ts
const add = (a: number, b: number): number => {
    return a + b
}
export {add}

// src/main.ts
import {add} from './util'
const test = (): string => {
    return `hello world` + add(10, 1);
};

export { test }

最后执行打包命令rollup -c,可以看见在lib目录下输出了bundle.esm.jsmain.d.tsutil.d.ts等文件,如此,便完成了一个模块的编写和打包,接下来的工作就是将其发布到包服务器上。

2. 搭建npm私有服务器

参考

跟第三方模块类似,我们可以将每个公共模块放在独立的仓库里面维护是一个比较自然的操作,然后通过package.json维护每个业务项目所依赖的公共模块。

在默认情况下,通过npm publish会将包发布到npm官方仓库上;事实上,出于某些原因我们并不能将业务公共模块直接发布到npm官方源仓库里面,可以通过搭建私有npm仓库解决这个问题。

下面介绍使用verdaccio快速搭建本地npm私有服务器。

2.1. 启动服务

# 全局安装
npm i verdaccio -g 

# 启动服务
verdaccio

# 如果希望开启守护经常,可以使用pm2 
pm2 start verdaccio

然后就可以输入http://localhost:4873访问本地的npm服务了。

如果需要在多个npm源直接切换时,建议使用nrm

nrm add local http://localhost:4873
nrm use local

默认配置文件位于~/.config/verdaccio/config.yaml

uplinks:
  npmjs:
    url: https://registry.npmjs.org/
    # url: https://registry.npm.taobao.org/ # 当找不到包时去其他镜像查找,此处可以修改为淘宝镜像 
packages:
  '@*/*':
    # scoped packages
    access: $all
    publish: $authenticated # 发布带命名空间的包时,需要先使用npm login登录当前用户名的账号后才能发布
    unpublish: $authenticated
    proxy: npmjs
  '**':
      # 常规包...

2.2. 发布包到本地服务器

首先添加一个账号

npm adduser --registry http://localhost:4873
# 按照提示依次输入username、password和email

然后新建一个测试模块

mkdir test_mod
cd test_mod

# 创建模块
npm init -y
echo "module.exports = {hello(){console.log('test hello')}}" > index.js

# 打包
npm pack 

最后发布到本地服务器

npm publish --registry http://localhost:4873

然后重新访问http://localhost:4873,就可以看见刚才发布的测试模块了。

在业务模块中安装npm i test_mod -S,然后引入模块

let test = require('test_mod')
test.hello()

大功告成,所有的公共模块都可以按照这种方式发布到私有仓库里,在生产环境的机器上通过指定registry安装私有仓库的模块。

最后,如果希望移除某个已经发布的包,可以使用

npm unpublish xxx_module_name --force

在本地开发npm模块的时候,我们可以使用npm link命令,将npm 模块链接到对应的运行项目中去,方便地对模块进行调试和测试。

首先切换到模块目录,进行全局link,实际上会

cd test_mod

npm link
# 实际输出
# ~/.nvm/versions/node/v12.14.0/lib/node_modules/test_mod -> ~/test/test_mod

然后切换到业务项目目录,从全局模块中进行link

cd test
npm link test_mod

# 实际链接
# ~/test/node_modules/test_mod -> ~/.nvm/versions/node/v12.14.0/lib/node_modules/test_mod -> ~/test/test_mod

然后就可以在不安装npm i test_mod的情况下使用test_mod模块了,通过link可以很方便的再开发阶段进行调试。

2.4. 单仓库模块的问题

在单个仓库中管理单个模块是很自然的一件事情,但是当模块逐渐增多时,就会出现下面几个问题

  • 本地开发调试时,可能需要频繁的npm link
  • 每个模块都包含一些如babelwebpack等公共的依赖,会导致同步开发环境带来的额外开销
  • 某些模块从功能上来说是有一定关联的,但是从模块上来说应该是独立的,如开发一个类React库,我们可能需要配套实现ReactRouterReactRedux等模块

因此在实际开发中,我们可能会遇到再同一个项目仓库中维护多个模块,但在使用时,我们更希望将他们各自独立发布到npm服务器上,下面介绍一种在单项目中管理多package的方法。

3. 使用lerna管理多模块项目

如果需要在单项目中管理多package,大概的设计是

  • 在项目中按照目录设计和管理模块,所有模块公用同一个git仓库和issue列表
  • 每个模块包含独立的package.jsonnode_modules
  • 每个模块包含独立的版本号和npm publish操作

按照上面设计手动创建目录和文件可能是一件比较繁琐的事情,我们可以通过Lerna来实现

3.1. 基本使用

参考

大致使用步骤如下

npm install lerna -g

mkdir lerna_test
cd lerna_test

lerna init # 会生成lerna.json配置文件
lerna create test_mod_1
# 根据提示一路回车确认创建模块即可
lerna create test_mod_2 # 创建模块2

# 初次发布时需要先提交一次git到远程仓库
lerna publish # 会发布所有的模块
# 选择版本号等,最后会发布到本地npm设置的npm仓库

此外还有很多其他的命令,具体作用可参考官方文档,常见的有

lerna add xxx --scope=xx_mod # 为xx_mod模块添加xxx依赖,不添加scope参数则为全部模块添加依赖
lerna bootstrap # leran add 指定依赖后,需要运行命令并安装到node_modules下

lerna publish --skip-git # 默认每次publish时会提交一个git记录,通过该参数可跳过git
lerna list # 查看当前目录下的包

3.2. 使用lerna管理NeZha

NeZha是之前写的一个类React库,在同一个仓库中简单实现了包括ReactReact-RouterRedux,最初的时候未采用lerna,而是将三个模块打包到一个模块中,导致引入模块时需要写很长的路径,此处决定将其使用lerna重构目录。

大致流程

  • 将项目拆分成三个包:@shymean/nezha@shymean/nax@shymean/nezha-router,使用lerna create初始化模块

  • 由于后两个包依赖基础包nezha,使用lerna add @shymean/nezha声明依赖,然后使用lerna bootstart建立本地模块的连接

  • 由于三个包均使用TypeScript开发,因此决定使用rollup打包,通过配置每个包对应的package.json

    • "main": "lib/index.js"对应commonJS规范的模块入口
  • "module": "esm/index.js"对应ES 6规范的模块入口

    • "types": "src/index.ts"对应TypeScript模块入口

最后,通过build.js脚本执行rollup打包命令,输出模块文件,最后通过lerna publish发布到本地verdaccio服务器上。

4. 小结

本文主要包含如下内容

  • npm模块入口配置字段开始,研究了如何编写规范的npm包;
  • 学习rollup的基本配置,并使用其编写库文件
  • 研究了搭建本地私有npm仓库,方便托管业务模块,
  • 研究了如何管理业务开发中的公共模块,并给出了具体的实践方案,即使用lerna管理多package的项目,并将每个模块发布到npm服务器上。

至此,对于整个npm包的编写、发布和管理有了更清晰的认识,在新的一年里,希望自己能够积极参与开源项目,为整个社区贡献一点绵薄之力。