创建 npm 工具库

打包工具的选择

如果我们需要构建一个简单的小型应用并让它快速运行起来,可以使用 Parcel;如果需要构建一个类库只需要导入很少第三方库,可以使用 Rollup;如果需要构建一个复杂的应用,需要集成很多第三方库,并且需要代码分拆、HMR等功能,推荐使用 Webpack [3]

所以,在开发工具库时,我们选择 rollup 作为打包工具。

初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
mkdir wow-tool
cd wow-tool
npm init -y

# 添加src目录
mkdir src

# 添加 index.js 入口文件
New-Item src/index.js

# 修改入口文件位置
# package.json 中 exports 修改为如下值,使其支持浏览器和 nodejs
{
"exports":{
"require": "./dist/yourPackageName.cjs.js",
"import": "./dist/yourPackageName.esm.js"
},
}

安装 rollup

将 rollup 安装为本地开发依赖

1
2
3
4
5
# npm
npm install rollup --save-dev

# yarn
yarn add rollup --dev

安装 rollup 插件

先安装如下rollup插件:

插件包名 作用
@rollup/plugin-node-resolve 使用 nodejs的解析算法,使得可以在 node_modules 中使用第三方包
@rollup/plugin-commonjs 将 CommonJS 模块转成 ES 模块
@rollup/plugin-alias 在打包的时候创建别名
@rollup/plugin-replace 在打包时替换目标字符串
@rollup/plugin-eslint 代码规范化
@rollup/plugin-babel 与 babel 无缝集成,将 ES6 代码转换成 ES5
rollup-plugin-terser 通过使用 terser 引擎,缩减打包后的大小
rollup-plugin-clear 在编译前清空输出目录
@rollup/plugin-json 将 json 文件转成 ES 模块
rollup-plugin-serve 创建开发服务
rollup-plugin-livereload 实时重载代码修改
rollup-plugin-filesize 在终端中显示包文件的大小

安装命令如下:

1
2
3
4
5
6
# npm
npm install @rollup/plugin-node-resolve @rollup/plugin-commonjs @rollup/plugin-alias @rollup/plugin-replace @rollup/plugin-eslint @rollup/plugin-babel rollup-plugin-terser rollup-plugin-clear @rollup/plugin-json rollup-plugin-serve rollup-plugin-livereload rollup-plugin-filesize --save-dev

# yarn
# yarn 安装开发依赖的命令如下
yarn add <package-name> --dev

插件的具体作用参考:rollup官方插件库

配置 rollup

  1. 根据开发环境区分不同的配置
  2. 设置对应的 npm script
  3. 输出不同规范的产物:umd、umd.min、cjs、esm
  4. 兼容 jest 不支持 es module的问题
1
2
3
4
5
mkdir config
cd config
New-Item rollup.config.base.js
New-Item rollup.config.dev.js
New-Item rollup.config.prod.js

rollup.config.base.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
import { nodeResolve } from '@rollup/plugin-node-resolve' // 解析 node_modules 中的模块
import commonjs from '@rollup/plugin-commonjs' // cjs => esm
import alias from '@rollup/plugin-alias' // alias 和 reslove 功能
import replace from '@rollup/plugin-replace'
import eslint from '@rollup/plugin-eslint'
import { babel } from '@rollup/plugin-babel'
import { terser } from 'rollup-plugin-terser'
import clear from 'rollup-plugin-clear'
import json from '@rollup/plugin-json' // 支持在源码中直接引入json文件,不影响下面的
import { name, version, author } from '../package.json'

// 此处 pkgName 要修改成自己的包名
const pkgName = 'vtools'
const banner =
'/*!\n' +
` * ${name} v${version}\n` +
` * (c) 2014-${new Date().getFullYear()} ${author}\n` +
' * Released under the MIT License.\n' +
' */'

export default {
input: 'src/index.js',
// 同时打包多种规范的产物
output: [
{
file: `dist/${pkgName}.umd.js`,
format: 'umd',
name: pkgName,
banner
},
{
file: `dist/${pkgName}.umd.min.js`,
format: 'umd',
name: pkgName,
banner,
plugins: [terser()]
},
{
file: `dist/${pkgName}.cjs.js`,
format: 'cjs',
name: pkgName,
banner
},
{
file: `dist/${pkgName}.esm.js`,
format: 'es',
banner
}
],
// 注意 plugin 的使用顺序
plugins: [
json(),
clear({
targets: ['dist']
}),
alias(),
replace({
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
preventAssignment: true
}),
nodeResolve(),
commonjs({
include: 'node_modules/**'
}),
eslint({
throwOnError: true, // 抛出异常并阻止打包
include: ['src/**'],
exclude: ['node_modules/**']
}),
babel({ babelHelpers: 'bundled' })
]
}

修改包名

1
2
// 此处 pkgName 要修改成自己的包名
const pkgName = 'vtools'

rollup.config.dev.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import baseConfig from './rollup.config.base'
import serve from 'rollup-plugin-serve'
import livereload from 'rollup-plugin-livereload'

export default {
...baseConfig,
plugins: [
...baseConfig.plugins,
serve({
port: 8080,
contentBase: ['dist', 'examples/brower'],
openPage: 'index.html',
}),
livereload({
watch: 'examples/brower',
})
]
}

rollup.config.prod.js

1
2
3
4
5
6
7
8
9
10
import baseConfig from './rollup.config.base'
import filesize from 'rollup-plugin-filesize'

export default {
...baseConfig,
plugins: [
...baseConfig.plugins,
filesize()
]
}

配置 prettier

prettier 主要用于代码格式校验和修正。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# --save-exact 添加准确的版本号,例如:"webpack": "^5.1.3",添加--save-exact后将没有 ^ 号
# npm
npm install --save-dev --save-exact prettier
# yarn
yarn add --dev --exact prettier

# 添加配置文件
New-Item .prettierrc.js
# 添加下列内容
module.exports = {
printWidth: 80,
tabWidth: 2,
useTabs: false,
singleQuote: true,
proseWrap: 'preserve',
arrowParens: 'avoid',
bracketSpacing: true,
disableLanguages: ['vue'],
endOfLine: 'auto',
eslintIntegration: false,
htmlWhitespaceSensitivity: 'ignore',
ignorePath: '.prettierignore',
jsxBracketSameLine: false,
jsxSingleQuote: true,
parser: 'babel',
requireConfig: false,
stylelintIntegration: false,
trailingComma: 'none',
tslintIntegration: true,
'workbench.iconTheme': 'vscode-icons',
'editor.minimap.enabled': false,
'editor.renderWhitespace': 'none',
'editor.renderControlCharacters': false,
semi: false
}

# 添加忽略文件
New-Item .prettierignore.js -value build`ncoverage

配置 eslint

eslint 主要用于代码质量的校验。

安装

1
2
3
4
5
6
7
8
9
// 在开发环境中安装 esLint
npm i eslint -D

// 生成配置文件
npx eslint --init

// 开发中使用 eslit 检查语法,用 prettier 检查格式
// 1.选择 To check syntax and find problems
// 2.选择用 javaScript modules 开发

安装插件并配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 使用 standard 规范
npm install --save-dev eslint-config-standard eslint-config-prettier eslint-plugin-promise eslint-plugin-import eslint-plugin-node

// .eslintrc.js 配置
module.exports = {
root: true,
env: {
browser: true,
es2021: true,
jest: true // 支持jest
},
extends: ['standard', 'prettier'],
parserOptions: {
ecmaVersion: 12,
sourceType: 'module'
}
}

// .eslintignore 配置忽略 dist, 防止校验打包的产物
New-Item .eslintignore.js -value dist

如果有更复杂的需求,可以安装 eslint-config-prettier 来禁用 eslint 与 prettier 之间冲突的配置。

配置 babel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
npm i -D @babel/core @babel/preset-env babel-plugin-transform-async-to-promises

New-Item .babelrc.js

// .babelrc.js
module.exports = {
presets: [
['@babel/preset-env', {
// rollupjs 会处理模块,所以设置成 false
modules: false
}]
],
plugins: [
// 避免 babel 将 async/await 转成 Generator
// 这样兼容性更好
'transform-async-to-promises'
]
}

单元测试

test 目录下创建 xxx.test.js(xxx 和 源码中的文件名保持一致)

  • 选用 jest 做单元测试
  • 配置 eslintjest 环境
  • 解决 jest 不支持 es module 的问题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
npm i -D jest
# 支持 `es module`
npm i -D rollup-jest

# 添加测试目录
mkdir test

# package.json 中设置
"jest": {
"preset": "rollup-jest"
}

# 执行测试
npx jest

# 测试覆盖率
npx jest --coverage

更新忽略文件

.gitignore

1
2
3
# `n 代表换行
# 如果是用 git 仓库管理来作为包服务的话,dist 不能忽略
New-Item .gitignore -value node_modules`ndist`ncoverage`n

.npmignore

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 新建文件
New-Item .npmignore

# 添加内容
node_modules
test
src
.babelrc.js
.eslintrc.js
scripts
coverage
docs
.czrc
.eslintignore
.huskyrc
.commitlint.config.js
.commitlint.config

README.md

添加徽标

1
2
3
4
5
https://img.shields.io/badge/{徽标标题}-{徽标内容}-{徽标颜色}.svg

// eg
![build](https://img.shields.io/badge/build-passing-success.svg)
复制代码
  • 动态徽标
1
2
3
4
https://img.shields.io/github/issues/{github用户名}/{仓库名}.svg
https://img.shields.io/github/forks/{github用户名}/{仓库名}.svg
https://img.shields.io/github/stars/{github用户名}/{仓库名}.svg
https://img.shields.io/github/license/{github用户名}/{仓库名}.svg

git 提交校验

安装下列包:

包名 作用
husky 关联git的hook与项目,可以实现在提交时校验提交信息的规范性
@commitlint/config-conventional 代码提交 message 规范校验格式库
@commitlint/cli 代码提交 message 规范校验
commitizen 代码交互提交
cz-conventional

完整安装及配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
npm install --save-dev husky @commitlint/config-conventional @commitlint/cli commitizen cz-conventional-changelog

# commitlint.config
echo "module.exports = {extends: ['@commitlint/config-conventional']}" > commitlint.config.js

# 初始化 git
git init

# commitlint 使用 husky 的消息钩子
npx husky install
# 注意,-- commitlint 之间有一个空格
npx husky add .husky/commit-msg 'npx --no -- commitlint --edit "$1"'

# .czrc
New-Item .czrc

# 内容
{ "path": "cz-conventional-changelog" }

# package.json
# commitizen 快捷命令
{
"scripts": {
"commit": "cz"
}
}

常用的commitlint type类别:

  • build:发布
  • chore:构建过程或辅助工具的变动
  • ci:合并其它贡献者的代码变化(continuous integration)
  • docs:文档(documentation)
  • feat:新功能(feature)
  • fix:修补bug
  • perf:
  • refactor:重构(即不是新增功能,也不是修改bug的代码变动)
  • style: 格式(不影响代码运行的变动)
  • test:增加测试

例:

git commit -m 'feat: 增加 xxx 功能' git commit -m 'bug: 修复 xxx 功能'

配置package.json

经过上述配置,我们还需要配置一些 npm 脚本来运行,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
"scripts": {
"dev": "npx rollup -w --environment NODE_ENV:development -c config/rollup.config.dev.js",
"build": "npx rollup --environment NODE_ENV:production -c config/rollup.config.prod.js",
"x": "npm --no-git-tag-version version major",
"y": "npm --no-git-tag-version version minor",
"z": "npm --no-git-tag-version version patch",
"lint": "eslint src",
"fix": "npm run lint --fix",
"commit": "git-cz",
"test": "jest",
"test:c": "jest --coverage",
"prepublish": "npm run build",
"pub": "npm publish --access=public",
"pub:x": "npm run x && npm publish --access=public",
"pub:y": "npm run y && npm publish --access=public",
"pub:z": "npm run z && npm publish --access=public"
}

完整版:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
{
"name": "wow-tool",
"version": "0.0.1",
"author": "",
"description": "provide some usefull uities for javascript",
"main": "src/index.js",
// main,module,broser 分别代表不同环境下的入口文件
"main": "dst/sw-utils.cjs.js",
"module": "dst/sw-utils.esm.js",
"scripts": {
"dev": "npx rollup -w --environment NODE_ENV:development -c config/rollup.config.dev.js",
"build": "npx rollup --environment NODE_ENV:production -c config/rollup.config.prod.js",
"x": "npm --no-git-tag-version version major",
"y": "npm --no-git-tag-version version minor",
"z": "npm --no-git-tag-version version patch",
"lint": "eslint src",
"fix": "npm run lint --fix",
"commit": "git-cz",
"test": "jest",
"test:c": "jest --coverage",
"prepublish": "npm run build",
"pub": "npm publish --access=public",
"pub:x": "npm run x && npm publish --access=public",
"pub:y": "npm run y && npm publish --access=public",
"pub:z": "npm run z && npm publish --access=public"
},
"jest": {
"preset": "rollup-jest"
},
"repository": {
"type": "git",
"url": "https://gitee.com/noctiflorous/wow-tool.git"
},
"keywords": [],
"license": "ISC",
"bugs": {
"url": "https://gitee.com/noctiflorous/wow-tool.git/issues"
},
"homepage": "https://gitee.com/noctiflorous/wow-tool.git#readme",
// 开发依赖(作为npm包被install时,开发依赖不会被下载进node_modules)
"devDependencies": {
"@babel/core": "^7.16.7",
"@babel/preset-env": "^7.16.7",
"@commitlint/cli": "^16.0.2",
"@commitlint/config-conventional": "^16.0.0",
"@rollup/plugin-alias": "^3.1.9",
"@rollup/plugin-babel": "^5.3.0",
"@rollup/plugin-commonjs": "^21.0.1",
"@rollup/plugin-eslint": "^8.0.1",
"@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-node-resolve": "^13.1.3",
"@rollup/plugin-replace": "^3.0.1",
"commitizen": "^4.2.4",
"cz-conventional-changelog": "^3.3.0",
"eslint": "^7.32.0",
"eslint-config-standard": "^16.0.3",
"eslint-plugin-import": "^2.25.4",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^5.2.0",
"husky": "^7.0.4",
"jest": "^27.4.7",
"rollup": "^2.63.0",
"rollup-jest": "^1.1.3",
"rollup-plugin-clear": "^2.0.7",
"rollup-plugin-filesize": "^9.1.1",
"rollup-plugin-livereload": "^2.0.5",
"rollup-plugin-serve": "^1.1.0",
"rollup-plugin-terser": "^7.0.2"
},
// 依赖(作为npm包被install时,依赖会被下载进node_modules)
"dependencies": {},
}

编写功能

上述配置完成后,就可以开始编写功能了。

入口函数为 src/index.js ,该入口在 rollup.config.base.js 中定义的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// src/index.js

const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']

/**
* 字节转存储大小
* @param {*} bytes
* @returns
*/
export function humanStorageSize(bytes) {
let u = 0

while (parseInt(bytes, 10) >= 1024 && u < units.length - 1) {
bytes /= 1024
++u
}

return `${bytes.toFixed(1)}${units[u]}`
}

编写测试

针对上述的 humanStorageSize 方法编写一个单元测试,单元编写参考:rollup-jestjest

使用示例:

1
2
3
4
5
import path from 'path'

test('parses extname', () => {
expect(path.extname('foo.md')).toBe('.md')
})

humanStorageSize 测试示例:

1
2
3
4
5
6
7
8
9
// 新建文件
New-Item test/humanStorageSize.test.js

// 添加如下内容
import { humanStorageSize } from '../src/index'

test('test humanStorageSize', () => {
expect(humanStorageSize(1024)).toBe('1.0KB')
})

执行测试

1
2
// 运行测试
npx jest test/humanStorageSize

测试结果

image-20220407181351999

通过 Git 管理包

如果公司没有私有包服务器,同时也不想将包发到 npm 上,可以发布到私有 Git 仓库里,然后直接从 Git 安装包。>> 用 git 管理私有包

发布到 npm

发布到 verdaccio

参考

本文主要参考以下文章,在此致以诚挚谢意!

  1. 详解从零创建自己的NPM包
  2. 开发一个规范的 npm 包
  3. 深入对比Webpack、Parcel、Rollup打包工具
  4. Node.js 如何处理 ES6 模块 - 阮一峰的网络日志 (ruanyifeng.com)
  5. package.json 中 你还不清楚的 browser,module,main 字段优先级
  6. Package exports | webpack
  7. ES6和commonJs模块化规范的混用