blog icon indicating copy to clipboard operation
blog copied to clipboard

【bigo】Rollup极速教程,手把手带你打包一个npm包

Open qhbin opened this issue 2 years ago • 0 comments

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 });

}));

在浏览器执行下刚刚打包的文件,验证是否符合预期

image 到此我们已经简单了解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无法识别。 image

我们尝试引入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();

再次执行构建,发现已成功构建。 image

在项目中,我们一般不会重复造轮子,往往需要引入第三方库,为此,我们也引入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作为外部引入 image

我们查看下打包出来的代码,发现lodash确实只作为一个外部引入,并未打包到生成的代码中。 image

上面打包出来的umd包是无法单独执行的,除非我们在项目中已有外部引入的lodash。如果我们也想把lodash也一同打包到项目中,使得我们的umd包可以单独执行呢? image

那么我们就需要用到另一个插件了,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中了。 image

到浏览器控制台执行下打包出来的代码,代码成功运行。 image

以下是常用的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"
  }
}

我们跑下测试脚本,已经成功 image

使用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包脚手架,落地到自己团队中。

qhbin avatar Dec 23 '21 03:12 qhbin