使用Rollup打包TypeScript

背景

提到前端打包工具, 最熟悉和常用的就是webpack了, 但是对于开发纯js库来说, 其实有更好的打包工具可以使用: rollup

webpack的区别

简单对比一下两个打包工具

webpack

webpack的核心概念是一切皆模块, 然后基于模块可以实现代码缓存, 代码分割, 按需加载等特性

但这样在开发简单的js库时候, 打包出来的未压缩包里反而会出现__webpack_require__这样的代码, 压缩过后的min.js代码中也有一些遗留的模块加载的代码, 导致体积会稍大

rollup

rollup是一款ES Modules打包工具, 我们经常使用的vuereact都是使用它打包的. 对比起 webpack, rollup的核心特点是: 代码更简洁, 不会存在模块引入的问题. 但不足的是如果需要引入其他资源类型的文件需要其他插件, 而这些插件质量是没有webpack质量高的. 所以作为纯js库开发来说, rollup更轻量快速, 但整体web-app开发还是需要选择大而全的webpack或者其他如viteparcel等打包工具

开发环境搭建

rollup.config

jsrollup开发环境比较简单, 直接按照官方文档搭建都可以. 但是我们需要使用ts, 所以有些配置需要定制

同时可以看到, 我们这里输出了两种文件: umd类型和es类型, 其中umd类型是用于浏览器的, es类型是用于webpack等打包工具的, webpack会优先引入package.json里的module文件, 实现Tree-shaking

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
// rollup.config.ts
import resolve from 'rollup-plugin-node-resolve';
import commonjs from 'rollup-plugin-commonjs';
import sourceMaps from 'rollup-plugin-sourcemaps';
import camelCase from 'lodash.camelcase';
import typescript from 'rollup-plugin-typescript2';
import json from 'rollup-plugin-json';

import pkg from './package.json';

const libraryName = 'my-library';

export default {
input: `src/${libraryName}.ts`,
output: [
{ file: pkg.main, name: camelCase(libraryName), format: 'umd', sourcemap: true },
{ file: pkg.module, format: 'es', sourcemap: true },
],
// Indicate here external modules you don't wanna include in your bundle (i.e.: 'lodash')
external: [],
watch: {
include: 'src/**',
},
plugins: [
// Allow json resolution
json(),
// Compile TypeScript files
typescript({ useTsconfigDeclarationDir: true }),
// Allow bundling cjs modules (unlike webpack, rollup doesn't understand cjs)
commonjs(),
// Allow node_modules resolution, so you can use 'external' to control
// which external modules to include in the bundle
// https://github.com/rollup/rollup-plugin-node-resolve#usage
resolve({ browser: true, preferBuiltins: true, mainFields: ['browser'] }),

// Resolve source maps to the original source
sourceMaps(),
],
};

package.json

script里加上buildstart两个命令, 这里参考了其他库的用法, 在build的时候会直接使用typedoc生成一份docs文档

1
2
3
4
5
6
{
"scripts": {
"build": "tsc --module commonjs && rollup -c rollup.config.ts && typedoc --out docs --target es6 --theme minimal --mode file src",
"start": "rollup -c rollup.config.ts -w"
}
}

使用

在配置完成后, 使用npm run build就能打出生成环境包, 使用npm run start就能启动rollup监听自动编译, 性能上比webpack启动和打包会快很多, 而且打包代码的可读性也比webpack更强, 没有冗余部分, 可以更好的调试代码

优化压缩

打包之后, 发现umd格式的代码没有压缩. es模块代码不压缩比较正常, 因为如果是由webpack引入的, 往往会再次压缩打包, 不压缩更易于调试和修改. 但umd版本是可以提供浏览器直接使用的, 如果不压缩会造成体积过大, 所以需要修改rollup配置来实现不同的压缩策略, 而查了一遍文档后, 发现rollup的插件只能基于整体来配置, 而不能基于不同的output文件配置, 所以需要稍微修改一下配置结构

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
73
74
// rollup.config.ts
import resolve from 'rollup-plugin-node-resolve';
import commonjs from 'rollup-plugin-commonjs';
import sourceMaps from 'rollup-plugin-sourcemaps';
import camelCase from 'lodash.camelcase';
import typescript from 'rollup-plugin-typescript2';
import json from 'rollup-plugin-json';

import pkg from './package.json';

const libraryName = 'my-library';

const config = [
{
input: `src/index.ts`,
output: {
file: pkg.main,
name: camelCase(libraryName),
format: 'umd',
sourcemap: true,
},
// Indicate here external modules you don't wanna include in your bundle (i.e.: 'lodash')
external: [],
watch: {
include: ['src/**'],
},
plugins: [
// Allow json resolution
json(),
// Compile TypeScript files
typescript({ useTsconfigDeclarationDir: true }),
// Allow bundling cjs modules (unlike webpack, rollup doesn't understand cjs)
commonjs(),
// Allow node_modules resolution, so you can use 'external' to control
// which external modules to include in the bundle
// https://github.com/rollup/rollup-plugin-node-resolve#usage
resolve({ browser: true, preferBuiltins: true, mainFields: ['browser'] }),

// Resolve source maps to the original source
sourceMaps(),
uglify(),
],
},
{
input: `src/index.ts`,
output: {
file: pkg.module,
format: 'es',
sourcemap: true,
},
// Indicate here external modules you don't wanna include in your bundle (i.e.: 'lodash')
external: [],
watch: {
include: ['src/**'],
},
plugins: [
// Allow json resolution
json(),
// Compile TypeScript files
typescript({ useTsconfigDeclarationDir: true }),
// Allow bundling cjs modules (unlike webpack, rollup doesn't understand cjs)
commonjs(),
// Allow node_modules resolution, so you can use 'external' to control
// which external modules to include in the bundle
// https://github.com/rollup/rollup-plugin-node-resolve#usage
resolve({ browser: true, preferBuiltins: true, mainFields: ['browser'] }),

// Resolve source maps to the original source
sourceMaps(),
],
},
];

export default config;

单元测试

开发一个库自然需要单元测试, 使用最常见jest就可以了, 对ts的支持程度也非常好, 不需要编译和打包就能直接测试, 也能生成覆盖率报告

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
"scripts": {
"test": "jest --coverage",
"test:watch": "jest --coverage --watch"
},
"jest": {
"transform": {
".(ts|tsx)": "ts-jest"
},
"testEnvironment": "node",
"testRegex": "(/__tests__/.*|\\.(test|spec))\\.(ts|tsx|js)$",
"moduleFileExtensions": ["ts", "tsx", "js"],
"coveragePathIgnorePatterns": ["/node_modules/", "/test/"],
"coverageThreshold": {
"global": {
"branches": 90,
"functions": 95,
"lines": 95,
"statements": 95
}
},
"collectCoverageFrom": ["src/*.{js,ts}"]
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// test/lib.test.ts
import Lib from '../src/index';

/**
* Lib test
*/
describe('Lib test', () => {
it('works if true is truthy', () => {
expect(true).toBeTruthy();
});

it('Lib is instantiable', () => {
expect(new Lib()).toBeInstanceOf(Lib);
});

it('Lib test is 1', () => {
expect(new Lib().test()).toEqual(1);
});
});

Demo

单元测试跑完没有问题之后, 有些库的效果还需要在页面实际使用中才能看到效果, 我们也可以基于rollup打包一个简单的react-demo应用, 实现在页面上实时调试, 同时也加上自动更新和调试服务器等配置

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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
// rollup.config.ts
const isNpmStart = process.env.npm_lifecycle_event === 'start';
const isDev = isNpmStart;

if (isDev) {
fs.rmdirSync(path.resolve('./node_modules/.cache'), { recursive: true });
}

const createDemoConfig = () => {
const getIPAddress = () => {
const interfaces = os.networkInterfaces();
let result = '0.0.0.0';
for (const devName in interfaces) {
const alias = _.find(interfaces[devName], (alias) => alias.family === 'IPv4' && alias.address !== '127.0.0.1' && !alias.internal);
if (alias) {
result = alias.address;
break;
}
}
return result;
};
return {
input: `demo/index.tsx`,
output: {
file: `dist/lib-demo.js`,
name: camelCase('lib-demo'),
format: 'iife',
sourcemap: true,
sourcemapExcludeSources: false,
exports: 'named',
globals: {
react: 'React',
'react-dom': 'ReactDOM',
antd: 'antd',
store: 'store',
'@ant-design/icons': 'icons',
},
},
external: ['react', 'react-dom', 'antd', '@ant-design/icons', 'store'],
watch: {
include: ['demo/**'],
},
plugins: [
postcss({
extensions: ['.sass', '.css'],
}),
replace({
'process.env.NODE_ENV': JSON.stringify('development'),
'process.env.BASE_PATH': JSON.stringify(basePath),
}),
// Allow json resolution
json(),
// Compile TypeScript files
typescript({
include: ['demo/**/*.ts', 'demo/**/*.tsx'],
rollupCommonJSResolveHack: true,
useTsconfigDeclarationDir: true,
tsconfigOverride: { include: ['src', 'demo'] },
// check: false,
}),
babel({
babelrc: false,
extensions: ['.js', '.jsx', '.ts', '.tsx'],

presets: ['@babel/preset-env'],
// presets: ['@babel/preset-env', '@babel/preset-react'],
exclude: ['node_modules/**'],
}),
// Allow node_modules resolution, so you can use 'external' to control
// which external modules to include in the bundle
// https://github.com/rollup/rollup-plugin-node-resolve#usage
resolve({ browser: true, preferBuiltins: true, mainFields: ['browser'] }),
// Allow bundling cjs modules (unlike webpack, rollup doesn't understand cjs)
commonjs({
// namedExports: {
// react: Object.keys(react),
// },
// include: ['node_modules/**'],
}),

// Resolve source maps to the original source
sourceMaps(),

...(isNpmStart
? [
livereload({ watch: ['dist/'] }),
serve({
contentBase: '../',
host: getIPAddress(),
port: 10001,
open: true,
openPage: '/lib/demo/index.html',
}),
]
: []),
],
};
};

const config = libConfig.concat(isDev ? [createDemoConfig()] : []);

Demohtml配置, 为了减少打包速度和打包问题, 其他依赖库直接使用script引入页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1" />
<script src="../node_modules/react/umd/react.development.js"></script>
<script src="../node_modules/react-dom/umd/react-dom.development.js"></script>
<script src="../node_modules/moment/min/moment.min.js"></script>
<script src="../node_modules/antd/dist/antd.min.js"></script>
<script src="../node_modules/@ant-design/icons/dist/index.umd.min.js"></script>
<script src="../node_modules/store/dist/store.legacy.min.js"></script>
<link type="text/css" rel="stylesheet" href="../node_modules/antd/dist/antd.css" />
<title>demo</title>
</head>
<body>
<div id="root"></div>
<!-- This is the bundle generated by rollup.js -->
<script src="../dist/lib.umd.js"></script>
<!-- This is the bundle generated by rollup.js -->
<script src="../dist/lib-demo.js"></script>
</body>
</html>

Demoindex.tsx

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
import _ from 'lodash';
import React, { useReducer, useEffect } from 'react';
import ReactDOM from 'react-dom';
import Root from './containers/root';
import { ErrorBoundary, LoadingProgress } from './components';
import { defaultState, reducer, Context } from './context-store/userList';
import Lib from '../src/index';

import './App.scss';
import './main.scss';

declare global {
const lib: typeof Lib;
}

const App: React.FC = () => {
const [store, dispatch] = useReducer(reducer, defaultState);
useEffect(() => {
const sdk = Lib.getInstance();
}, []);
return (
<ErrorBoundary>
<LoadingProgress />
<Context.Provider value={{ state: store, dispatch }}>
<Root />
</Context.Provider>
</ErrorBoundary>
);
};

ReactDOM.render(<App />, document.getElementById('root'));

小结

整套配置完成后, 就基本实现了一个简单的rollup+typescriptjs库开发环境, 后续如果有其他需求继续在此基础上迭代即可, 也考虑使用最常用的配置开发一个create-rollup-ts-lib这样的工具, 减少重复工作量和踩坑的过程

作者

Mosby

发布于

2020-05-20

许可协议

评论