blog
blog copied to clipboard
【bigo】Rollup极速教程,手把手带你打包一个npm包
Rollup基础
什么是Rollup
Rollup 是一个 JavaScript 模块打包器,可以将小块代码编译成大块复杂的代码,例如 library 或应用程序。
Rollup简单使用
安装Rollup
npm install --global rollup
命令行模式
我们执行help命令查看下rollup的常见命令行参数
rollup -help
-i, --input <filename> 要打包的文件(必须)
-o, --file <output> 输出的文件 (如果没有这个参数,则直接输出到控制台)
-f, --format <format> 输出的文件类型 (amd, cjs, esm, iife, umd)
-e, --external <ids> 将模块ID的逗号分隔列表排除
-g, --globals <pairs> 以`module ID:Global` 键值对的形式,用逗号分隔开
任何定义在这里模块ID定义添加到外部依赖
-n, --name <name> 生成UMD模块的名字
-h, --help 输出 help 信息
-m, --sourcemap 生成 sourcemap (`-m inline` for inline map)
--amd.id AMD模块的ID,默认是个匿名函数
--amd.define 使用Function来代替`define`
--no-strict 在生成的包中省略`"use strict";`
--no-conflict 对于UMD模块来说,给全局变量生成一个无冲突的方法
--intro 在打包好的文件的块的内部(wrapper内部)的最顶部插入一段内容
--outro 在打包好的文件的块的内部(wrapper内部)的最底部插入一段内容
--banner 在打包好的文件的块的外部(wrapper外部)的最顶部插入一段内容
--footer 在打包好的文件的块的外部(wrapper外部)的最底部插入一段内容
--interop 包含公共的模块(这个选项是默认添加的)
初始化一个rollup-demo的项目
rollup-demo
├── main.js
└── package.json
编写main.js文件
// main.js
export function sayHello() {
console.log('hello rollup');
}
根据help命令提示,我们构建一个umd规范且命名空间为hello的文件,执行rollup命令
rollup --input main.js --name hello --file bundle.js --format umd
我们发现多了一个叫bundle.js的文件
rollup-demo
├── bundle.js
├── main.js
└── package.json
查看bundle.js
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
typeof define === 'function' && define.amd ? define(['exports'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.hello = {}));
})(this, (function (exports) { 'use strict';
// main.js
function sayHello() {
console.log('hello rollup');
}
exports.sayHello = sayHello;
Object.defineProperty(exports, '__esModule', { value: true });
}));
在浏览器执行下刚刚打包的文件,验证是否符合预期
到此我们已经简单了解rollup命令打包的使用。
但是一般情况下,为了最大发挥rollup的打包功能,我们一般都不会使用命令行模式进行打包,而是通过 Node.js来运行rollup提供的JavaScript接口来进行打包构建。接下来我们将继续通过本实例了解rollup的使用。
配置方式使用
在当前项目下安装rollup,并创建一个build.js的文件,
# 安装rollup
npm i rollup -D
# 当前文件目录
rollup-demo
├── build.js
├── main.js
├── node_modules
│ └── rollup
├── package-lock.json
└── package.json
// build.js
const rollup = require('rollup');
const inputOptions = {
input: 'main.js' // 要打包文件的入口文件
}
const outputOptions = {
file: 'bundle.js', // 输出的文件
format: 'umd', // 输出的文件类型 (amd, cjs, esm, iife, umd)
name: 'hello' // 生成UMD模块的名字
}
async function build() {
const bundle = await rollup.rollup(inputOptions);
// 创建 code and a sourcemap
const { code, map } = await bundle.generate(outputOptions);
// 生成文件
await bundle.write(outputOptions);
console.log('build success!');
}
build();
用node运行跑build.js进行构建,我们发现打包出了跟上述命令行模式相同的bundle.js文件。
# 执行构建
node build.js
# 当前文件目录
rollup-demo
├── build.js
├── bundle.js
├── main.js
├── node_modules
│ └── rollup
├── package-lock.json
└── package.json
到此我们已经大致了解了如何在node下使用rollup进行构建。
注意在上述构建代码编写中,我们发现inputOptions与outputOptions的参数跟上述命令行参数名是一致的!下面是inputOptions与outputOptions参数的参数分类(可以结合上面命令行参数进行一一对应)。
const inputOptions = {
// 核心参数
input, // 对应命令行参数中的--input, 唯一必填参数
external,
plugins,
// 高级参数
onwarn,
cache,
// 危险参数
acorn,
context,
moduleContext,
legacy
};
const outputOptions = {
// 核心参数
file, // 对应命令行参数中的--file,若有bundle.write,必填
format, // 对应命令行参数中的--format
name, // 对应命令行参数中的--name
globals,
// 高级参数
paths,
banner, // 对应命令行参数中的--banner
footer,
intro,
outro,
sourcemap,
sourcemapFile,
interop,
// 危险区域
exports,
amd,
indent
strict
};
rollup的插件
我们尝试新增一个utils.js文件,并在main.js中引入。
// utils.js
module.exports = {
add: function(a, b) {
return a + b
}
}
// main.js
import utils from './utils';
export function add(a,b) {
const result = utils.add(a, b);
console.log('add result is:', result);
return result;
}
我们执行构建,发现报错了,因为utils.js是commonjs规范的代码,rollup无法识别。
我们尝试引入rollup-plugin-commonjs插件
// build.js
const rollup = require('rollup');
const commonjs = require('rollup-plugin-commonjs');
const inputOptions = {
input: 'main.js', // 要打包文件的入口文件
plugins: [commonjs()],
external: []
}
const outputOptions = {
file: 'bundle.js', // 输出的文件
format: 'umd', // 输出的文件类型 (amd, cjs, esm, iife, umd)
name: 'hello' // 生成UMD模块的名字
}
async function build() {
const bundle = await rollup.rollup(inputOptions);
// 创建 code and a sourcemap
const { code, map } = await bundle.generate(outputOptions);
// 生成文件
await bundle.write(outputOptions);
console.log('build success!');
}
build();
再次执行构建,发现已成功构建。
在项目中,我们一般不会重复造轮子,往往需要引入第三方库,为此,我们也引入lodash来实现一个求和sum函数。
// main.js
import utils from './utils';
import lodash from 'lodash';
export function add(a,b) {
const result = utils.add(a, b);
console.log('add result is:', result);
return result;
}
// lodash求和
export function sum(array) {
const result = lodash.sum(array);
console.log('lodash sum result is:', result);
return result;
}
执行构建命令,控制台输出如下提示,虽然构建成功,但是把lodash作为外部引入
我们查看下打包出来的代码,发现lodash确实只作为一个外部引入,并未打包到生成的代码中。
上面打包出来的umd包是无法单独执行的,除非我们在项目中已有外部引入的lodash。如果我们也想把lodash也一同打包到项目中,使得我们的umd包可以单独执行呢?
那么我们就需要用到另一个插件了,rollup-plugin-node-resolve
// build.js
const rollup = require('rollup');
const commonjs = require('rollup-plugin-commonjs');
const node = require('rollup-plugin-node-resolve');
const inputOptions = {
input: 'main.js', // 要打包文件的入口文件
plugins: [node(), commonjs()],
external: []
}
const outputOptions = {
file: 'bundle.js', // 输出的文件
format: 'umd', // 输出的文件类型 (amd, cjs, esm, iife, umd)
name: 'hello' // 生成UMD模块的名字
}
async function build() {
const bundle = await rollup.rollup(inputOptions);
// 创建 code and a sourcemap
const { code, map } = await bundle.generate(outputOptions);
// 生成文件
await bundle.write(outputOptions);
console.log('build success!')
}
build();
再次执行构建,我们发现构建成功,且将lodash也打包到bundle.js中了。
到浏览器控制台执行下打包出来的代码,代码成功运行。
以下是常用的rollup插件
- rollup-plugin-alias: 提供modules名称的 alias 和reslove 功能
- rollup-plugin-babel: 提供babel能力
- rollup-plugin-eslint: 提供eslint能力
- rollup-plugin-node-resolve: 解析 node_modules 中的模块
- rollup-plugin-commonjs: 转换 CJS -> ESM, 通常配合上面一个插件使用
- rollup-plugin-serve: 类比 webpack-dev-server, 提供静态服务器能力
- rollup-plugin-filesize: 显示 bundle 文件大小
- rollup-plugin-uglify: 压缩 bundle 文件
- rollup-plugin-replace: 类比 Webpack 的 DefinePlugin , 可在源码中通过 process.env.NODE_ENV 用于构建区分 Development 与 Production 环境.
用Rollup构建一个简单的库
通过上述实践,我们已经掌握了rollup的基本使用,接下来,我们将进一步完善我们的项目,使得我们可以打包出一个符合生产需求的npm包。
多环境适配
一般情况下,一个npm包应该包含esm/cjs/umd三种规范,供我们在前端项目/后端项目以及浏览器外链引入。
我们调整下项目结构
rollup-demo
├── package-lock.json
├── package.json
├── scripts
│ └── build.js
└── src
├── main.js
└── utils.js
由于调整了路径,我们需要调整下build.js,将打包生成的文件放在dist目录下。
// build.js
const rollup = require('rollup');
const commonjs = require('rollup-plugin-commonjs');
const node = require('rollup-plugin-node-resolve');
const path = require('path');
// 按照项目根目录解析文件路径
const resolve = p => path.resolve(__dirname, '../', p);
const inputOptions = {
input: resolve('src/main.js'), // 要打包文件的入口文件
plugins: [node(), commonjs()],
external: []
}
const outputOptions = {
file: resolve('dist/bundle.js'), // 输出的文件到dist/bundle.js
format: 'umd', // 输出的文件类型 (amd, cjs, esm, iife, umd)
name: 'hello' // 生成UMD模块的名字
}
async function build() {
const bundle = await rollup.rollup(inputOptions);
// 创建 code and a sourcemap
const { code, map } = await bundle.generate(outputOptions);
// 生成文件
await bundle.write(outputOptions);
console.log('build success!')
}
build();
上述仅打包了一个umd规范的文件,并不满足我们的需求,对项目进一步改造。
// build.js
const rollup = require('rollup');
const commonjs = require('rollup-plugin-commonjs');
const node = require('rollup-plugin-node-resolve');
const babel = require('rollup-plugin-babel');
const { terser } = require('rollup-plugin-terser');
const path = require('path');
const packageJson = require('../package.json');
const { version, author } = packageJson;
// 按照项目根目录解析文件路径
const resolve = p => path.resolve(__dirname, '../', p);
const entry = resolve('src/main.js');
const pkgName = packageJson.name.includes('/') ? packageJson.name.split('/')[1] : packageJson.name;
// 转换命名为驼峰命名
function transformCamel(str) {
const re = /-(\w)/g;
return str.replace(re, function ($0, $1) {
return $1.toUpperCase();
});
}
// umd的全局变量名
const moduleName = transformCamel(pkgName);
// 编译后,未压缩版文件添加的前缀
const banner =
'/*!\n' +
` * ${pkgName} v${version}\n` +
` * (c) 2020-${new Date().getFullYear()} ${author}\n` +
' * Released under the MIT License.\n' +
' */';
const builds = {
'esm': {
input: entry,
plugins: [node(), commonjs()],
output: {
file: resolve(`dist/${pkgName}.esm.js`),
format: 'esm',
banner
}
},
'cjs': {
input: entry,
plugins: [node(), commonjs()],
output: {
file: resolve(`dist/${pkgName}.cjs.js`),
format: 'cjs',
banner
}
},
'umd': {
input: entry,
plugins: [node(), commonjs()],
terser: true,
output: {
file: resolve(`dist/${pkgName}.umd.js`),
format: 'umd',
name: moduleName,
banner
}
}
}
async function build() {
for (let config of Object.values(builds)) {
const inputOptions = config;
// 如果不需要babel
if (config.transpile !== false) {
config.plugins.push(babel({
exclude: ['node_modules/**'],
}));
}
if(config.terser) {
config.plugins.push(terser());
}
const outputOptions = config.output;
// 打包生成文件
const bundle = await rollup.rollup(inputOptions);
await bundle.write(outputOptions);
console.log(outputOptions.file);
}
console.log('build success!');
}
build();
修改package.json
{
"name": "rollup-demo",
"version": "1.0.0",
"description": "rollup demo",
"main": "dist/rollup-demo.cjs.js",
"module": "dist/rollup-demo.esm.js",
"browser": "dist/rollup-demo.umd.js",
"scripts": {
"build": "node scripts/build.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/preset-env": "^7.16.5",
"rollup": "^2.61.1",
"rollup-plugin-babel": "^4.4.0",
"rollup-plugin-commonjs": "^10.1.0",
"rollup-plugin-node-resolve": "^5.2.0"
},
"dependencies": {
"lodash": "^4.17.21"
}
}
由于我们引入了babel,需要新增.babelrc.js文件对babel进行配置
//.babelrc.js
module.exports = {
presets: [
require('@babel/preset-env'),
],
ignore: [
'dist/*.js'
]
};
加入eslint
一个良好的编码规范能使得我们的项目具有更好的可维护性,引入ESLint能帮助我们更好的在规范种编码。以下我们将引入阿里egg的eslint规范对我们的编码进行约束。
安装eslint与eslint-config-egg插件
npm i eslint eslint-config-egg -D
编写eslint配置文件.eslintrc
// .eslintrc
{
"extends": "eslint-config-egg",
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true,
"experimentalObjectRestSpread": true,
"modules": true
}
},
"env": {
"amd": true,
"es6": true,
"browser": true,
"node": false
},
"rules": {
"linebreak-style": 0,
"no-trailing-spaces": 0
},
"globals": {
"module": true
}
}
新增.eslintignore,忽略无需校验的文件
#.eslintignore
node_modules/
dist/
scripts/
加入单元测试
作为一个完善的类库项目,单元测试是必不可少的。我们将引入mocha与chai的组合构建我们的单元测试。
mocha是一个javascript的测试框架,chai是一个断言库,两者搭配使用更佳,所以合称“抹茶”(其实mocha是咖啡)。“抹茶”特点是: 简单,node和浏览器都可运行。
安装mocha与chai
npm install -D mocha chai
新增测试文件test/index.test.js,并编写用例。
rollup-demo
├── dist
│ ├── rollup-demo.cjs.js
│ ├── rollup-demo.esm.js
│ └── rollup-demo.umd.js
├── package-lock.json
├── package.json
├── scripts
│ └── build.js
├── src
│ ├── main.js
│ └── utils.js
└── test
└── index.test.js
// index.test.js
const chai = require('chai');
const name = require('../package.json').name;
const pkgName = name.split('/').pop();
const myPackage = require(`../dist/${pkgName}.cjs`);
const expect = chai.expect;
const { add, sum } = myPackage;
describe('测试包方法', function() {
it('should add === 3', function() {
const result = add(1, 2);
expect(result).to.be.equal(3);
});
it('should sum === 6', function() {
const result = sum([ 1, 2, 3 ]);
expect(result).to.be.equal(6);
});
});
修改package.json,加入测试命令
{
"name": "rollup-demo",
"version": "1.0.0",
"description": "rollup demo",
"main": "dist/rollup-demo.cjs.js",
"module": "dist/rollup-demo.esm.js",
"browser": "dist/rollup-demo.umd.js",
"scripts": {
"build": "npm run eslint && node scripts/build.js",
"eslint": "eslint .",
"test": "mocha -t 10000 -s 2000 test/*.test.js"
},
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/preset-env": "^7.16.5",
"chai": "^4.3.4",
"eslint": "^8.4.1",
"eslint-config-egg": "^10.0.0",
"mocha": "^9.1.3",
"rollup": "^2.61.1",
"rollup-plugin-babel": "^4.4.0",
"rollup-plugin-commonjs": "^10.1.0",
"rollup-plugin-node-resolve": "^5.2.0",
"rollup-plugin-terser": "^7.0.2"
},
"dependencies": {
"lodash": "^4.17.21"
}
}
我们跑下测试脚本,已经成功
使用TypeScript
采用 TypeScript,可以避免 JavaScript 动态性所带来的一些无法预料的错误信息。经过上述流程已整理出一个堪用的lib脚手架了,但是为了打造一个更完善的脚手架,我们将引入TypeScript进行升级。
安装typescript
npm i -D typescript
新增tsconfig.json配置文件
// tsconfig.json
{
"compilerOptions": {
/* 基础配置 */
"target": "esnext",
"module": "esnext",
"lib": [
"dom",
"esnext"
],
"removeComments": false,
"declaration": true,
"sourceMap": true,
/* 强类型检查配置 */
"strict": true,
"noImplicitAny": false,
/* 模块分析配置 */
"baseUrl": ".",
"outDir": "./dist",
"esModuleInterop": true,
"moduleResolution": "node",
"resolveJsonModule": true
},
"include": [
"src/*.ts",
]
}
新增rollup的typescript插件
npm i -D rollup-plugin-typescript2
修改build.js,加入typescript插件
// build.js
const rollup = require('rollup');
const commonjs = require('rollup-plugin-commonjs');
const node = require('rollup-plugin-node-resolve');
const babel = require('rollup-plugin-babel');
const { DEFAULT_EXTENSIONS } = require('@babel/core');
const { terser } = require('rollup-plugin-terser');
const typescript = require('rollup-plugin-typescript2');
const path = require('path');
const packageJson = require('../package.json');
const { version, author } = packageJson;
// 按照项目根目录解析文件路径
const resolve = p => path.resolve(__dirname, '../', p);
const entry = resolve('src/main.ts');
const pkgName = packageJson.name.includes('/') ? packageJson.name.split('/')[1] : packageJson.name;
// 转换命名为驼峰命名
function transformCamel(str) {
const re = /-(\w)/g;
return str.replace(re, function($0, $1) {
return $1.toUpperCase();
});
}
// umd的全局变量名
const moduleName = transformCamel(pkgName);
// 编译后,未压缩版文件添加的前缀
const banner =
'/*!\n' +
` * ${pkgName} v${version}\n` +
` * (c) 2020-${new Date().getFullYear()} ${author}\n` +
' * Released under the MIT License.\n' +
' */';
const basePlugins = [typescript(), node(), commonjs()]
const builds = {
esm: {
input: entry,
plugins: [ ...basePlugins ],
external: [ 'lodash' ],
output: {
file: resolve(`dist/${pkgName}.esm.js`),
format: 'esm',
banner,
},
},
cjs: {
input: entry,
plugins: [ ...basePlugins ],
output: {
file: resolve(`dist/${pkgName}.cjs.js`),
format: 'cjs',
banner,
},
},
umd: {
input: entry,
plugins: [ ...basePlugins, terser() ],
output: {
file: resolve(`dist/${pkgName}.umd.js`),
format: 'umd',
name: moduleName,
banner,
},
},
};
async function build() {
for (const config of Object.values(builds)) {
const inputOptions = config;
// 如果不需要babel
if (config.transpile !== false) {
config.plugins.push(babel({
exclude: [ 'node_modules/**' ],
// babel 默认不支持 ts 需要手动添加
extensions: [
...DEFAULT_EXTENSIONS,
'.ts',
],
}));
}
const outputOptions = config.output;
// 打包生成文件
const bundle = await rollup.rollup(inputOptions);
await bundle.write(outputOptions);
console.log(outputOptions.file);
}
console.log('build success!');
}
build();
修改.eslintrc
{
"extends": "eslint-config-egg/typescript",
"parserOptions": {
"project": "./tsconfig.json"
},
"env": {
"amd": true,
"es6": true,
"browser": true,
"node": false
},
"rules": {
"linebreak-style": 0,
"no-trailing-spaces": 0,
"@typescript-eslint/ban-ts-ignore": 0
},
"globals": {
"module": true,
"__dirname": true
}
}
结语
本文通过一个简单到示例介绍了如何使用rollup进行npm包的构建,大家可以在此基础上扩展,制订一套完善的npm包脚手架,落地到自己团队中。