一种构建 JavaScript 库的方法

最近手头有个项目需要构建 JavaScript 库,核心需求是库既能用于传统的多页面,也能用于 Angular 的单页面。目前网络上暂时好像没有这类详细教程,反而是一些理论阐述的文章较多,经过一番研究后,我得到了一个适合自己的方法。

由于我希望库既能用于多页面,也能用于 Angular 单页面,所以需要支持 UMD 和 ES Harmony。由于 Angular 是使用 TypeScript 开发,最好还能提供用于 TypeScript 的声明文件。最原始的想法自然是手动按要求提供各种文件,但这样工作量比较大,也不容易扩展。那么还有什么容易的办法吗?有的,最核心的想法就是库的源码只写一份,然后用工具生成各种模块系统需要文件。具体的做法可能有差异,但理念是一样的。

从我的需求出发,我最终选择用 TypeScript 来写库的源码,基于脱敏的考虑,这里选择 TypeScript 文档中的示例代码来演示。

首先我们建立好库的源码目录结构并配置好源码管理:

1
2
3
4
5
6
simple-module-example
├── src
│   ├── LettersOnlyValidator.ts
│   ├── Validation.ts
│   ├── ZipCodeValidator.ts
│   └── index.ts

然后使用 npm init 生成 package.json 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
  "name": "simple-module-example",
  "version": "1.0.0",
  "description": "A simple module example.",
  "main": "index.js",
  "directories": {
    "test": "test"
  },
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [
    "module",
    "example"
  ],
  "author": "Meiliang Dong",
  "license": "MIT"
}

其次是按需求编辑好 package.json 文件。这是关键步骤,package.json 的 main 字段通常用于指向 UMD 版本的库;module 字段则用于指向 ES 版本的库。我们还需要配置构建脚本生成对应版本的库,最终的 package.json 文件内容如下:

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
{
    "name": "simple-module-example",
    "version": "1.0.0",
    "description": "A simple module example.",
    "main": "dist/umd/simple-module-example.js",
    "module": "dist/esm/index.js",
    "browser": "dist/umd/simple-module-example.js",
    "types": "dist/types/index.d.ts",
    "files": [
        "dist"
    ],
    "directories": {
        "test": "test"
    },
    "scripts": {
        "clean": "rm -rf ./dist",
        "build": "npm run clean && npm run build:es2015 && npm run build:esm && npm run build:cjs && npm run build:umd",
        "build:es2015": "tsc --module es2015 --target es2015 --outDir dist/es2015",
        "build:esm": "tsc --module es2015 --outDir dist/esm",
        "build:umd": "rollup dist/esm/index.js --format umd --name SimpleModuleExample --sourcemap --file dist/umd/simple-module-example.js",
        "build:umd:min": "cd dist/umd && uglifyjs --compress --mangle --source-map --screw-ie8 --comments --o simple-module-example.min.js -- simple-module-example.js && gzip simple-module-example.min.js -c > simple-module-example.min.js.gz"
    },
    "keywords": [
        "module",
        "example"
    ],
    "author": "Meiliang Dong",
    "license": "MIT"
}

然后是编写 TypeScript 的配置文件 tsconfig.json, 先使用命令 npm install @tsconfig/recommended --save-dev 安装推荐的配置,之后根据需求定制,最终的内容如下:

1
2
3
4
5
6
7
8
9
10
11
{
    "extends": "@tsconfig/recommended/tsconfig.json",
    "compilerOptions": {
      "outDir": "./dist",
      "target": "es5",
      "sourceMap": true,
      "declaration": true,
      "declarationDir": "./dist/types"
    },
    "include": ["./src/**/*"]
  }

其次是测试,测试是库开发的重要环节,它能帮我们验证库是否正常工作,后续迭代重构也要依赖它。通常的做法是使用测试框架,像 Angular 是 Karma test runner 搭配Jasmine test framework , 我们可以参考选择。

我这里还玩了一下用 rollup 打包, 然后在浏览器里运行测试用例。首先在库工程目录外重新创建一个测试工程,然后使用 npm link 命令来安装我们的开发库,具体目录结构和文件内容如下:

1
2
3
4
5
6
7
8
9
10
$mkdir test-simple-module-example
├── Test.ts
├── dist
│   ├── esm
│   └── test-simple-module-example.js
├── index.html
├── package-lock.json
├── package.json
├── rollup.config.js
└── tsconfig.json

Test.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { StringValidator, ZipCodeValidator, LettersOnlyValidator } from "simple-module-example";

// Some samples to try
let strings = ["Hello", "98052", "101"];

// Validators to use
let validators: { [s: string]: StringValidator } = {};
validators["ZIP code"] = new ZipCodeValidator();
validators["Letters only"] = new LettersOnlyValidator();

// Show whether each string passed each validator
strings.forEach((s) => {
  for (let name in validators) {
    console.log(
      `"${s}" - ${
        validators[name].isAcceptable(s) ? "matches" : "does not match"
      } ${name}`
    );
  }
});

package.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
    "name": "test-simple-module-example",
    "version": "1.0.0",
    "description": "A test application for simple module example.",
    "main": ".dist/Test.js",
    "scripts": {
        "clean": "rm -rf ./dist",
        "build": "npm run clean && npm run build:esm && npm run build:bundle",
        "build:esm": "tsc --module es2015 --outDir dist/esm",
        "build:bundle": "rollup -c",
        "test": "echo \"Error: no test specified\" && exit 1"
    },
    "author": "Meiliang Dong",
    "license": "MIT",
    "dependencies": {
        "@tsconfig/recommended": "^1.0.1"
    },
    "devDependencies": {
        "@rollup/plugin-node-resolve": "^9.0.0"
    }
}

rollup.config.js

1
2
3
4
5
6
7
8
9
10
import resolve from '@rollup/plugin-node-resolve';

export default {
    input: 'dist/esm/Test.js',
    output: {
        file: 'dist/test-simple-module-example.js',
        format: 'umd'
    },
    plugins: [resolve()]
};

tsconfig.json

1
2
3
4
5
6
7
8
9
10
{
  "extends": "@tsconfig/recommended/tsconfig.json",
  "compilerOptions": {
    "outDir": "./dist",
    "target": "es5",
    "sourceMap": true,
    "moduleResolution": "Node"
  },
  "include": ["./*.ts"]
}

最后是根据需要选择发布方式,例如发布到 npm 公有仓库,具体做法参考官方文档就好了。

Reference