Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
qhbin opened this issue Dec 23, 2021 · 0 comments
Open

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

qhbin opened this issue Dec 23, 2021 · 0 comments

Comments

@qhbin
Copy link

qhbin commented Dec 23, 2021

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant