js模块打包利器rollup的配置与使用

1,707 阅读13分钟

Rollup 是一个 JavaScript 模块打包器,可以将小块代码编译成大块复杂的代码。使用ES6模块,会静态分析代码,排除未实际使用的代码(Tree-shaking)。

安装

npm install --global rollup

简单使用

先创建一个简单的项目

mkdir -p rollup-demo/src
cd rollup-demo

先在src文件夹下创建common.js导出文件

// src/common.js
export const name = 'rollup'
export const version = '2.7'

接着src文件夹下创建入口文件 mian.js

// src/main.js
import { name } from './common.js' // 引用了上面的common.js模块
export default function () {
  console.log(name)
}

接着在命令行中使用rollup

rollup src/main.js -f cjs -o bundle.js

上面命令行中的参数代表的意思分别为

  • -f : --output.format 的缩写,指定打包文件的类型,这里的值是cjs,代表 CommonJS
  • -o :--output.file 的缩写,表示要写入的文件

执行完命令后,打包出来的 bundle.js 代码如下

'use strict';
const name = 'rollup';
function main () {
  console.log(name);
}
module.exports = main;

可以发现 common.js 文件中的 version 变量并没有被打包进来

配置文件

可以发现上面通过命令行的方式打包文件比较麻烦,要写很多配置参数

可以把这些配置参数写在配置文件中

在项目的根目录创建 rollup.config.js 的文件

// rollup.config.js
export default {
  input: 'src/main.js',
  output: {
    file: 'bundle.js',
    format: 'cjs'
  }
};

接着在命令行执行以下命令即可

rollup -c

-c 是 --config 的缩写

如果命令行的配置选项和配置文件中的选项重复,同样的命令行选项将会覆盖配置文件中的选项

配置选项

input ( -i / --input )

指定包的入口文件

output

file ( -o / --output.file )

生成包写入的文件

name ( -n / --name )

output.format 设置为 iife 或者 umd 等,该值可以作为打包出来的函数的变量名,以便同一页面上的其他脚本可以访问使用

结合下面的 format 选项一起看看具体使用

format ( -f / --output.format )

生成包的格式,其值可以是

  • amd : 异步模块定义, 适用于类似RequireJS的模块加载器

    以上面 "简单使用" 打包的代码为例,在设置 format : "amd" 的情况下打包出来的代码如下

    define((function () { 'use strict';
      const name = 'rollup';
      function main () {
        console.log(name);
      }
      return main;
    }));
    
  • cjs : CommonJS, 适用于 Node 和 Browserify/Webpack

    如上面 "简单使用"中打包出来的代码

  • esm : ES模块, 在现代浏览器中可以通过 <script type=module> 标签引入

    在设置 format : "amd" 的情况下打包出来的代码如下

    const name = 'rollup';
    function main () {
      console.log(name);
    }
    export { main as default };
    
  • iife : 打包成一个自执行函数, 适用于 <script>标签

    (function () {
      'use strict';
      const name = 'rollup';
      function main () {
        console.log(name);
      }
      return main;
    })();
    

    如果设置了output的name属性,比如

    export default {
      input: 'src/main.js',
      output: {
        file: 'bundle.js',
        format: 'iife',
        name: 'MyBundle',
      },
    }
    

    则打包出来如下

    var MyBundle = (function () {
      'use strict';
      const name = 'rollup';
      function main () {
        console.log(name);
      }
      return main;
    })();
    
  • umd : 通用模块定义,以amdcjsiife 为一体

    注意,在指定 output.format 为 umd 时,必须设置 output.name, 否则会出现如下的报错

    ”[!] Error: You must supply "output.name" for UMD bundles that have exports so that the exports are accessible in environments without a module loader.“
    

    同样以上面的例子,当我们设置 output.format 为 umd,output.name为 ”MyBundle“时,打包出来代码如下

    (function (global, factory) {
      typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
      typeof define === 'function' && define.amd ? define(factory) :
      (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.MyBundle = factory());
    })(this, (function () { 'use strict';
      const name = 'rollup';
      function main () {
        console.log(name);
      }
      return main;
    }));
    
  • system : SystemJS 加载器格式

    打包代码如下

    System.register([], (function (exports) {
      'use strict';
      return {
        execute: (function () {
          exports('default', main);
          const name = 'rollup';
          function main() {
            console.log(name);
          }
        })
      };
    }));
    

globals ( -g / --globals )

该选项的值是一个对象

globals 主要用于 umd / iife 包。用于告诉 rollup 指定的变量代表的模块

例如 src/main.js 的代码如下使用了 jquery

// src/main.js
import $ from "jquery";
export default function () {
  console.log($("div"));
}

用下面的配置文件打包成 iife

export default {
  input: "src/main.js",
  output: {
    file: "bundle.js",
    format: "iife",
    name: "MyBundle",
  },
};

这时候可以发现命令行出现了如下提示

image-20220430142309138.png

大概意思是说让我们使用 output.globals 指定与外部模块对应的浏览器全局变量名,并帮我们把 jquery 猜测为"$"

生成的 bundle.js 的代码如下

var MyBundle = (function ($) {
  'use strict';
  function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
  var $__default = /*#__PURE__*/_interopDefaultLegacy($);
  function main () {
    console.log($__default["default"]("div"));
  }
  return main;
})($);

可以发现 bundle.js 的代码中默认全局有个 变量等同于函数中的变量等同于函数中的 变量,虽然在 jquery 的项目中确实有全局的 变量,但是这里只是以jquery举个例子,我们可以通过设置output.globals来为我们的src/main.js的代码中的变量,但是这里只是以 jquery 举个例子,我们可以通过设置 output.globals 来为我们的 src/main.js 的代码中的 变量指定对应的全局变量名,例如

export default {
  input: "src/main.js",
  output: {
    file: "bundle.js",
    format: "iife",
    name: "MyBundle",
    globals: {
      jquery: "window.jQuery",
    },
  },
};

则生成的 bundle.js 的代码为

var MyBundle = (function ($) {
  "use strict";
  function _interopDefaultLegacy(e) {
    return e && typeof e === "object" && "default" in e ? e : { default: e };
  }
  var $__default = /*#__PURE__*/ _interopDefaultLegacy($);
  function main() {
    console.log($__default["default"]("div"));
  }
  return main;
})(window.jQuery);

banner/footer

  • banner : 添加字符串到生成包的前面
  • footer : 添加字符串到生成包的后面

以上面 “简单使用” 的代码为例,以下面的配置文件生成包

export default {
  input: 'src/main.js',
  output: {
    file: 'bundle.js',
    format: 'cjs',
    banner: '/* 前面的注释 */',
    footer: '/* 后面的注释 */',  
  },
};

生成的 bundle.js 的代码如下

/* 前面的注释 */
"use strict";
const name = "rollup";
function main() {
  console.log(name);
}
module.exports = main;
/* 后面的注释 */

intro/outro

和上面的 banner/footer 类似,但是 banner/footer 是在最前面和最后面,而 intro/outro 是在format 对应的包装代码中,什么意思呢,举个例子

配置文件为

export default {
  input: "src/main.js",
  output: {
    file: "bundle.js",
    format: "cjs",
    banner: "/* 前面的注释 */",
    footer: "/* 后面的注释 */",
    intro: "var version = 2.7",
    outro: "/* outro 注释 */",
  },
};

生成的 bundle.js 为

/* 前面的注释 */
'use strict';
var version = 2.7
const name = "rollup";
function main () {
  console.log(name);
}
module.exports = main;
/* outro 注释 */
/* 后面的注释 */

配置文件为

export default {
  input: "src/main.js",
  output: {
    file: "bundle.js",
    format: "iife",
    banner: "/* 前面的注释 */",
    footer: "/* 后面的注释 */",
    intro: "var version = 2.7",
    outro: "/* outro 注释 */",
  },
};

生成的 bundle.js 为

/* 前面的注释 */
(function () {
  "use strict";
  var version = 2.7;
  const name = "rollup";
  function main() {
    console.log(name);
  }
  return main;
  /* outro 注释 */
})();
/* 后面的注释 */

sourcemap ( -m / --sourcemap )

其值可以是 true 或 inline

  • true : 将创建一个单独的sourcemap文件。
  • inline : sourcemap将作为数据URI附加到生成的output文件中。

以 “简单使用” 的代码为例,以下方的配置文件打包

export default {
  input: "src/main.js",
  output: {
    file: "bundle.js",
    format: "cjs",
    sourcemap: true,
  },
};

生成的 bundle.js 的代码如下

'use strict';
const name = "rollup";
function main () {
  console.log(name);
}
module.exports = main;
//# sourceMappingURL=bundle.js.map

同时会生成 bundle.js.map 文件,内容如下

{"version":3,"file":"bundle.js","sources":["src/common.js","src/main.js"],"sourcesContent":["export const name = \"rollup\";\r\nexport const version = \"2.7\";\r\n","import { name } from \"./common.js\"; // 引用了上面的common.js模块\r\nexport default function () {\r\n  console.log(name);\r\n}\r\n"],"names":[],"mappings":";;AAAO,MAAM,IAAI,GAAG,QAAQ;;ACCb,aAAQ,IAAI;AAC3B,EAAE,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;AACpB;;;;"}

如果是设置为 format 为 inline

export default {
  input: "src/main.js",
  output: {
    file: "bundle.js",
    format: "cjs",
    sourcemap: "inline",
  },
};

生成的 bundle.js 如下

'use strict';
const name = "rollup";
function main () {
  console.log(name);
}
module.exports = main;
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYnVuZGxlLmpzIiwic291cmNlcyI6WyJzcmMvY29tbW9uLmpzIiwic3JjL21haW4uanMiXSwic291cmNlc0NvbnRlbnQiOlsiZXhwb3J0IGNvbnN0IG5hbWUgPSBcInJvbGx1cFwiO1xyXG5leHBvcnQgY29uc3QgdmVyc2lvbiA9IFwiMi43XCI7XHJcbiIsImltcG9ydCB7IG5hbWUgfSBmcm9tIFwiLi9jb21tb24uanNcIjsgLy8g5byV55So5LqG5LiK6Z2i55qEY29tbW9uLmpz5qih5Z2XXHJcbmV4cG9ydCBkZWZhdWx0IGZ1bmN0aW9uICgpIHtcclxuICBjb25zb2xlLmxvZyhuYW1lKTtcclxufVxyXG4iXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7QUFBTyxNQUFNLElBQUksR0FBRyxRQUFROztBQ0NiLGFBQVEsSUFBSTtBQUMzQixFQUFFLE9BQU8sQ0FBQyxHQUFHLENBQUMsSUFBSSxDQUFDLENBQUM7QUFDcEI7Ozs7In0=

没有对应的 bundle.js.map 文件生成

strict

布尔值,表示是否开启严格模式(即是否在生成的代码前面加上 'use strict';),默认是 true,一般不会修改其为false。

exports

表示指定导出的模式,可配置一下四种类型

  • auto : 默认值,rollup会根据模块的导出内容猜测你想要的导出模式
  • default : 如果你的包仅仅需要导出一个内容,则可以配置这个
  • named : 使用于导出多个内容
  • none : 不需要导出,比如是在构建程序,而不是库

plugins

随着构建的代码的复杂性加大,这时候就需要更大的灵活性,这时候我们可以使用插件在打包的过程中更改 rollup 的行为,使rollup功能更强大。

例如当我们需要在代码中加入一些第三方库的时候

我们修改一下 src/main.js 的代码,使用第三方库 lodash-es

// src/main.js
import { cloneDeep } from 'lodash-es'
export default function () {
	let data = { num: 1 }
	cloneDeep(data)
}

以下面的配置文件打包

export default {
  input: 'src/main.js',
  output: {
    file: 'bundle.js',
    format: 'cjs'
  }
};

执行打包命令后会发现命令行出现了如下提示

(!) Unresolved dependencies

意思是出现了解析不了的依赖项

然后发现打包出来的 bundle.js 文件如下

'use strict';
var lodashEs = require('lodash-es');
function main () {
  let data = { num: 1 };
  lodashEs.cloneDeep(data);
}
module.exports = main;

可以发现 ”lodash-es“ 中 cloneDeep 功能的相关代码没有被打包进 bundle.js 而是保留了原先的依赖关系,这时因为默认情况下,Rollup 只会解析相对模块,这时候我们就可以使用 @rollup/plugin-node-resolve 插件来帮助rollup查找外部模块。

首先安装 @rollup/plugin-node-resolve

npm install --save-dev @rollup/plugin-node-resolve 

修改配置文件

import resolve from '@rollup/plugin-node-resolve';
export default {
  input: 'src/main.js',
  output: {
    file: 'bundle.js',
    format: 'cjs'
  },
  plugins: [
    resolve() // 记得需要调用导入的插件函数(即 commonjs(), 而不是 commonjs)
  ]
};

重新执行打包命令后可以发现,”lodash-es“中 cloneDeep 的代码被打包进了 bundle.js,现在打包出来的代码有两千多行

image-20220429170905295.png

可能你会觉得不是有 Tree-shaking 吗,一个 cloneDeep 怎么会有这么多行代码,因为像 javascript 这样的动态语言中的静态分析是很困难的,rollup 会对删除的代码保持保守,以此保证最终结果能够正确运行。所以在不可避免的情况下,如果导入的模块会有”副作用“,那么rollup也会安全的处理并包含这些”副作用“。

常用的插件

  • @rollup/plugin-commonjs : 将 CommonJS 模块转换为 ES6
  • @rollup/plugin-babel : 使用babel转译 ES6/7 代码
  • rollup-plugin-typescript2 :使用typescript

一般可以在 插件列表 中找到你需要的插件

external ( -e / --external )

外链,其值有两种形式

  • 一个函数,该函数以模块ID为参数,返回对应的 ture 或 false
    • true 代表是外部引用
    • false 代表不是外部引用
  • 数组,由应该保留在bundle的外部引用的模块ID组成的数组

上面说到的模块ID可以是外部依赖的名称,或者是一个已被找到路径的ID(比如文件的绝对路径)

还是以上面 ”简单使用“ 的代码为例,修改一下 rollup.config.js 如下

import path from 'path';
export default {
  input: 'src/main.js',
  output: {
    file: 'bundle.js',
    format: 'cjs',
  },
  external: [
    path.resolve('./src/common.js')
  ]
}

上面代码中的 external 也可以改写为以下代码,不过一般都用数组的形式比较多

external: (e) => {
    return path.resolve('./src/common.js') === e
}

生成包的文件代码如下

'use strict';
var common_js = require('./common.js');
function main () {
  console.log(common_js.name);
}
module.exports = main;

可以发现 common.js 中的代码没有被打包进 bundle.js 中,而是把它作为外链保持了原先的引用。

再看一个第三方库的例子,以上面介绍 plugins 选项时用的 lodash-es 为例

// src/main.js
import { cloneDeep } from 'lodash-es'
export default function () {
	let data = { num: 1 }
	cloneDeep(data)
}

如果我们不想把 lodash-es 的相关代码打包进 bundle.js ,而是保持原先的依赖,可以将 ”lodash-es“ 加入 external 中,如下

import resolve from '@rollup/plugin-node-resolve';
export default {
  input: 'src/main.js',
  output: {
    file: 'bundle.js',
    format: 'cjs'
  },
  external: [
    "lodash-es"
  ],
  plugins: [
    resolve()
  ]
};

执行命令后,bundle.js 的代码如下

'use strict';
var lodashEs = require('lodash-es');
function main () {
  let data = { num: 1 };
  lodashEs.cloneDeep(data);
}
module.exports = main;

onwarn

其值是一个 function ,可以拦截警告信息。

例如我们修改一下 src/main.js 的文件内容

import _ from "lodash";
export function main(data) {
  return console(data);
}

配置文件为

import resolve from "@rollup/plugin-node-resolve";
export default {
  input: "src/main.js",
  output: {
    file: "bundle.js",
    format: "cjs",
  },
  plugins: [resolve()],
  external: ["lodash"],
  onwarn(warning) {
    console.log(warning);
  },
};

执行生成命令后,页面不会发出警告,warning 打印信息如下

{
  code: 'UNUSED_EXTERNAL_IMPORT',
  message: '"default" is imported from external module "lodash" but never used in "src/main.js".',
  names: [ 'default' ],
  source: 'lodash',
  sources: [ 'D:\\project\\personal\\rollup_demo\\src\\main.js' ],
  toString: [Function (anonymous)]
}

通过修改 onwarn 函数的代码可以控制将指定的警告设置为忽略或者抛出异常取消生成,或者保留正常的警告

onwarn (warning) {
  // 跳过某些警告
  if (warning.code === 'UNUSED_EXTERNAL_IMPORT') return;
  // 抛出异常
  if (warning.code === 'NON_EXISTENT_EXPORT') throw new Error(warning.message);
  // 控制台打印一切警告
  console.warn(warning.message);
}

许多警告也有一个loc属性和一个frame,你可以定位到警告的来源:

onwarn ({ loc, frame, message }) {
  // 打印位置(如果适用)
  if (loc) {
    console.warn(`${loc.file} (${loc.line}:${loc.column}) ${message}`);
    if (frame) console.warn(frame);
  } else {
    console.warn(message);
  }
}

treeshake

其值为 布尔 值,代表是否开启treeshake去除无用代码,默认是true,一般极少需要去修改其为 false。

acorn

rollup内部是使用 acorn 来解析 js ,通过在配置文件设置 acorn 我们可以自定义 acorn 的配置选项,具体有哪些选项可以参考 acorn 库

context

默认情况下,模块的上下文 - 即顶级的this的值为undefined。在极少数情况下,可能需要改成类似 'window'等其他值。

rollup watch

上面的例子我们都是通过在命令行执行 rollup -c 命令来生成包,我们还可以在后面增加 “--watch” 来订阅文件的变化,当检测到磁盘上模块改变时,会重新构筑文件。

例如下面的代码

// src/common.js
export const name = 'rollup'
export const version = '2.7'

// src/main.js
import { name } from './common.js' // 引用了上面的common.js模块
export default function () {
  console.log(name)
}

配置文件为

export default {
  input: "src/main.js",
  output: {
    file: "bundle.js",
    format: "cjs",
  },
};

在命令行执行命令 rollup -c --watch

这时候会生成打包后的 bundle.js 文件,在修改相关的文件(src/main.js 或者 src/common.js)后,bundle.js 会自动重新打包。

Watch options

wacth的相关配置选项主要有以下三个

watch.chokidar

其值可以是布尔值

rollup 内置的监听文件的变化是通过 fs.watch 实现的,watch.chokidar 配置为 true 时,改为使用 chokidar 而不是 fs.watch

chokidar 是一个包,主要可以解决 “fs.watch” 一些问题,以及扩展它的功能,如果想改成使用 chokidar ,需要额外安装它。

也可以 将 watch.chokidar 的值配置为 chokidar 的配置选项对象,视为使用 chokidar。

watch.include

限制文件监控至某些文件

例如将上面的配置文件改为下面

export default {
  ...
  watch: {
     include: "src/**",
  }  
};

则只有改变src下的文件时才会重新打包

include 的值的类型是 string | RegExp | (string | RegExp)[]

watch.exclude

指定文件不被监控

export default {
  ...
  watch: {
     exclude: "node_modules/**",
  }  
};

exclude 和 include 值的类型一样,都是 string | RegExp | (string | RegExp)[]

rollup API

除了上面的通过命令行 rollup -c 的方式打包外,我们也可以通过使用 rollup 提供的 js API,不过一般很少使用,除非是为了扩展 Rollup 或者一些比较特殊的任务。

一般常用的 api 有 rollup.rolluprollup.watch

rollup.rollup

rollup.rollup 函数返回一个 Promise,它解析了一个 bundle 对象

例如我们可以在项目目录下插件一个 rollup.build.js (名字随便取的)

const rollup = require('rollup');
const resolve = require("@rollup/plugin-node-resolve");
const inputOptions = {
    input: 'src/main.js',
    plugins: [resolve()],
};  // 这里和上面的配置文件去掉了 output 选项后一样
const outputOptions = {
  file: "bundle.js",
  format: "esm",
}; // 这里和上面的配置文件的 output 选项配置一样

async function build() {
  const bundle = await rollup.rollup(inputOptions);

  await bundle.write(outputOptions);
}
build();

用 node 执行上面的文件后便可生成 bundle.js 文件。

rollup.watch

rollup 也提供了 API 版本的监听函数

例如我们可以在项目目录下插件一个 rollup.watch.js (名字随便取的)

const rollup = require("rollup");
const resolve = require("@rollup/plugin-node-resolve");
const watchOptions = {
  input: "src/main.js",
  output: {
    file: "bundle.js",
    format: "esm",
  },
  plugins: [resolve()],
  watch: {
    include: "src/common.js",
  },
}; // watchOptions 和配置文件一样,一样的选项

const watcher = rollup.watch(watchOptions);

watcher.on("event", (event) => {
  console.log(event);
  // event.code 会是下面其中一个:
  //   START        — 监听器正在启动(重启)
  //   BUNDLE_START — 构建单个文件束
  //   BUNDLE_END   — 完成文件束构建
  //   END          — 完成所有文件束构建
  //   ERROR        — 构建时遇到错误
  //   FATAL        — 遇到无可修复的错误
});
watcher.close(); // 停止监听

rollup和webpack

rollup比起webpack,更加的轻量,api也相对比较简单,打包出来的包体积更小。rollup是一个 JavaScript 模块打包器,其构建结果是一般是纯 js 代码,所以rollup更适合用在类似组件/工具库这种的构建。

而webpack是万物皆是模块的理念,其支持一些更高级的特点功能,可以处理各种类型的资源,所以webpack相对更大更复杂,适合用于打包应用程序。