logo头像

-------------

JavaScript模块打包工具Rollup——完全入门指南

本文于 1712 天之前发表,文中内容可能已经过时。

版本:v0.63.5。

Rollup 是前端模块化的一个打包工具,可以将小块代码编译成大块复杂的代码,例如 library 或应用程序。简单地说,它可以从一个入口文件开始,将所有使用的模块根据命令或者根据 Rollup 配置文件打包成一个目标文件,并且 Rollup 会自动过滤掉那些没有被使用过的函数或变量,从而使代码最小化,如果想使用直接导入这一个目标文件即可,因此 Rollup 极其适合构建一个工具库。

这里提到 Rollup 的两个特别重要的特性,第一个就是它使用了 ES2015 的模板标准,这意味着你可以直接使用 import 和 export 而不需要引入 babel。另一个重要特性叫做 tree-shaking,这个特性可以帮助你将无用代码(即没有使用的代码)从最终的目标文件中过滤掉。举个简单的例子,我们在 foo.js 文件定义了 f1 和 f2 两个方法,然后在入口文件 index.js 只引入了 foo.js 文件中的 f1 方法,那么在最后打包 index.js 文件时,Rollup 就不会将 f2 方法打包到最终文件中。(这个特性是基于 ES6 模块的静态分析的,也就是说,只有 export 而没有 import 的变量是不会被打包到最终代码中的)。

入门

创建第一个 bundle

开始前,需要安装Node.js,这样才可以使用npm;还需要了解如何使用command line

使用 Rollup 最简单的方法是通过 Command Line Interface (或 CLI)。先全局安装 Rollup (之后会介绍如何在项目中进行安装,更便于打包,但现在不用担心这个问题)。在命令行中输入以下内容:

$ npm install rollup --global

现在可以运行 rollup 命令了。试试吧~

$ rollup

由于没有传递参数,所以 Rollup 打印出了使用说明。这和运行 rollup –help 或 rollup -h 的效果一样。

我们来创建一个简单的项目:

$ mkdir -p my-rollup-project/src
$ cd my-rollup-project

首先,我们需要个入口文件。将以下代码粘贴到新建的文件 src/main.js 中:

// src/main.js
import { foo1 } from './foo.js';

export default function () {
  foo1();
}

之后创建入口文件引用的 foo.js 模块:

// src/foo.js
export function foo1() {
    console.log('function foo1')
}

export function foo2() {
    console.log('function foo2')
}

现在可以创建 bundle 了:

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

-o 表示打包后输出的文件路径,在 -o 后面的 bundle.js 就是我们最终生成的打包文件了(其实这里我们省略了参数 -i,用来表示入口文件的路径, Rollup 是会把没有加参数的文件默认是入口文件);-f 选项(–output.format 的缩写)指定了所创建 bundle 的类型(默认使用 es 模块标准来对文件进行打包)——这里是 CommonJS(在 Node.js 中运行)。现在我们看一下输出文件 bundle.js:

'use strict';

function foo1() {
    console.log('function foo1');
}

// src/main.js

function main () {
  foo1();
}

module.exports = main;

恭喜,你已经用 Rollup 完成了第一个 bundle。

使用配置文件

在项目中创建一个名为 rollup.config.js 的文件,增加如下代码:

// rollup.config.js 

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

// 2. output 为数组
export default {
    input: 'src/main.js',
    output: [{
      file: 'dist/bundle.cjs.js',
      format: 'cjs'
    }, {
      file: 'dist/bundle.umd.js',
      name: 'moduleName',
      format: 'umd'
    }, {
      file: 'dist/bundle.es.js',
      format: 'es'
    }]
};

// 整个配置为数组
export default [{
  input: 'src/main.js',
  output: {
    file: 'dist/bundle.cjs1.js',
    format: 'cjs'
  }
}, {
  input: 'src/main.js',
  output: [{
    file: 'dist/bundle.cjs2.js',
    format: 'cjs'
  }, {
    file: 'dist/bundle.umd2.js',
    name: 'moduleName',
    format: 'umd'
  }, {
    file: 'dist/bundle.es2.js',
    format: 'es'
  }]
}]

input 表示打包的入口文件,output 表示打包输出文件的配置(如果要输出多个,可以是一个数组,如果是数组,Rollup 会把每一个数组元素当成一个配置输出结果,因此可以在一个配置文件内设置多种输出配置),file 表示输出文件的名称路径,format 表示要打包成的模块类型。若使用 iife 或 umd 模块类型打包,需要添加属性moduleName,用来表示模块的名称;若用 amd 模块打包,可以配置 amd 相关的参数(使用 umd 模块模式时,也会使用到 amd 相关配置参数):

amd: {
    id: 'amd-name',   // amd具名函数名称
    define: 'def'     // 用来代替define函数的函数名称
}

我们用 –config 或 -c 来使用配置文件:

$ rollup -c

同样的命令行选项将会覆盖配置文件中的选项:

$ rollup -c -o bundle-2.js

在这里我们发现配置文件使用了 ES6 语法,这是因为 Rollup 本身会处理配置文件 ,所以可以使用 export default 语法——代码不会经过 Babel 等类似工具编译,所以只能使用所用 Node.js 版本支持的 ES2015 语法。

如果愿意的话,也可以指定与默认 rollup.config.js 文件不同的配置文件:

$ rollup --config rollup.config.dev.js
$ rollup --config rollup.config.prod.js

当然,我们也可以在 package.json 文件中编写 npm scripts 命令:

"build": "rollup -c"

针对不同模板类型我们简单编写几个命令:

"build:amd": "rollup index.js -f amd -o ./dist/dist.amd.js",
"build:cjs": "rollup index.js -f cjs -o ./dist/dist.cjs.js",
"build:es": "rollup index.js -f es -o ./dist/dist.es.js",
"build:iife": "rollup index.js -f iife -n result -o ./dist/dist.iife.js",
"build:umd": "rollup index.js -f umd -n result -o ./dist/dist.umd.js",
"build": "npm run build:amd && npm run build:cjs && npm run build:es && npm run build:iife && npm run build:umd"

在这里我们发现在设置模块为 iife(立即执行函数)和 umd 时,还加上了一个参数 -n,这是为了事先设定模块的名称,才能让其他人通过这个模块名称引用。

使用ES6编写代码

许多开发人员在他们的项目中使用Babel,以便他们可以使用未被浏览器和 Node.js 支持的将来版本的 JavaScript 特性。Rollup 虽然支持了解析 import 和 export 两种语法,但是不会解析其他不被支持 JavaScript 特性,使用 Babel 和 Rollup 的最简单方法是使用rollup-plugin-babel。 安装它:

$ npm i -D babel-core rollup-plugin-babel rollup-plugin-node-resolve

编写 Rollup 配置文件 rollup.config.js:

// rollup.config.js
import json from 'rollup-plugin-json';
import resolve from 'rollup-plugin-node-resolve';
import babel from 'rollup-plugin-babel';

export default {
    input: 'src/main.js',
    output: {
      file: 'dist/bundle.js',
      format: 'cjs'
    },
    plugins: [
      json(),
      resolve(),
      babel({
        exclude: 'node_modules/**' // 只编译我们的源代码
      })
    ]
};

这里有几个特别注意的地方。首先,我们设置 “modules”: false,否则 Babel 会在 Rollup 有机会做处理之前,将我们的模块转成 CommonJS,导致 Rollup 的一些处理失败。

其次,我们使用 external-helpers 插件,它允许 Rollup 在包的顶部只引用一次 “helpers”,而不是每个使用它们的模块中都引用一遍(这是默认行为)。

第三,我们将 .babelrc 文件放在 src 中,而不是根目录下。 这允许我们对于不同的任务有不同的 .babelrc 配置,比如像测试,如果我们以后需要的话 - 通常为单独的任务单独配置会更好。

现在,在我们运行 rollup 之前,我们需要安装 latest preset 和 external-helpers 插件:

$ npm i -D babel-preset-latest babel-plugin-external-helpers

现在我们用 es6 编辑 src / main.js:

// src/main.js
import { version } from '../package.json';

export default () => {
  console.log('version:' + version);
}

运行 Rollup npm run build,检查打包后的 bundle:

'use strict';

var version = "0.0.1";

// src/main.js

var main$1 = (function () {
  console.log('version:' + version);
});

module.exports = main$1;

配置文件

配置参数

external:为rollup设置外部模块和全局变量

平时开发中,我们经常会引入一些第三方模块,但是在使用的时候,我们又不想把它们打包到一个文件里,想让它们作为单独的模块(或文件)来使用,方便浏览器进行缓存,这个时候就需要使用配置文件中的 external 属性了。

我们这边以 jquery 为例,在开始使用之前,我们先安装它:

$ npm i jquery --save-dev

编写 Rollup 配置文件 rollup.config.js,加入external配置:

// rollup.config.js
import json from 'rollup-plugin-json';
import resolve from 'rollup-plugin-node-resolve';
import babel from 'rollup-plugin-babel';
import commonjs from 'rollup-plugin-commonjs'

export default {
    input: 'src/main.js',
    output: {
      file: 'dist/bundle.js',
      format: 'cjs'
    },
    plugins: [
      json(),
      resolve(),
      commonjs(),
      babel({
        exclude: 'node_modules/**' // 只编译我们的源代码
      })
    ],
    external: ['jquery']
};

external 用来表示一个模块是否要被当成外部模块使用,属性的值可以是一个字符串数组或一个方法,当传入的是一个字符串数组时,所有数组内的模块名称都会被当成是外部模块,不会被打包到最终文件中。当传入的是一个方法时,方法有一个参数 id,表示解析的模块的名称,我们可以自定义解析方式,若是要当做外部模块不打包到最终文件中,则返回 true,若要一起打包到最终文件中,则返回 false。

globals

globals 的值是一个对象,key表示使用的模块名称(npm 模块名),value 表示在打包文件中引用的全局变量名,在这里我们就是把jquery模块的全局变量名设置为jQuery,重新打包。

编写 Rollup 配置文件 rollup.config.js,加入globals配置:

// rollup.config.js
import json from 'rollup-plugin-json';
import resolve from 'rollup-plugin-node-resolve';
import babel from 'rollup-plugin-babel';
import commonjs from 'rollup-plugin-commonjs'

export default {
    input: 'src/main.js',
    output: {
        file: 'dist/bundle.js',
        name: 'result',
        format: 'iife',
        globals: {
            jquery: 'jQuery'
        }
    },
    plugins: [
      json(),
      resolve(),
      commonjs(),
      babel({
        exclude: 'node_modules/**' // 只编译我们的源代码
      })
    ],
    external: ['jquery']
};

运行 Rollup npm run build,检查打包后的 bundle:

var result = (function (jQuery) {
  'use strict';

  jQuery = jQuery && jQuery.hasOwnProperty('default') ? jQuery['default'] : jQuery;

  var version = "0.0.1";

  // src/main.js

  var main$1 = (function () {
      console.log(jQuery);
      console.log('version:' + version);
  });

  return main$1;

}(jQuery));

在重新打包出来的文件中,我们发现最后传入的参数已经由 $ 变为了 jQuery,而且 Rollup 也没有输出提示信息。

paths

有时候我们可能会使用 CDN 上的 js 文件,但是又不想在本地安装一个相同的模块(也有可能没有对应的模块),可能在版本升级的时候会产生一些问题,这个时候我们就需要使用 Rollup 的 paths 属性了,这个属性可以帮你把依赖的文件地址注入到打包后的文件里。

编写 Rollup 配置文件 rollup.config.js,加入 paths 配置:

// rollup.config.js
import json from 'rollup-plugin-json';
import resolve from 'rollup-plugin-node-resolve';
import babel from 'rollup-plugin-babel';
import commonjs from 'rollup-plugin-commonjs'

export default {
    input: 'src/main.js',
    output: {
        file: 'dist/bundle.js',
        name: 'result',
        format: 'amd',
        globals: {
            jquery: 'jQuery'
        },
        paths: {
            jquery: 'https://cdn.bootcss.com/jquery/3.2.1/jquery.js'
        }
    },
    plugins: [
      json(),
      resolve(),
      commonjs(),
      babel({
        exclude: 'node_modules/**' // 只编译我们的源代码
      })
    ],
    external: ['jquery']
};

运行 Rollup npm run build,检查打包后的 bundle:

define(['https://cdn.bootcss.com/jquery/3.2.1/jquery.js'], function (jQuery) { 'use strict';

  jQuery = jQuery && jQuery.hasOwnProperty('default') ? jQuery['default'] : jQuery;

  var version = "0.0.1";

  // src/main.js

  var main$1 = (function () {
      console.log(jQuery);
      console.log('version:' + version);
  });

  return main$1;

});

可以看到 Rollup 已经把我们需要的 CDN 地址作为依赖加入到了打包文件中。

插件

使用插件

随着构建更复杂的 bundle,通常需要更大的灵活性——引入 npm 安装的模块、通过 Babel 编译代码、和 JSON 文件打交道等。为此,我们可以用 插件(plugins) 在打包的关键过程中更改 Rollup 的行为。the Rollup wiki维护了可用的插件列表。

我们这边将以rollup-plugin-json的使用为例,它的作用是令 Rollup 从 JSON 文件中读取数据。

将 rollup-plugin-json 安装为开发依赖:

$ npm install --save-dev rollup-plugin-json

我们用的是 –save-dev 而不是 –save,因为实际执行的代码并不依赖这个插件——只是在打包时使用。

更新 src/main.js 文件,从 package.json 而非 src/foo.js 中读取数据:

// src/main.js
import { version } from '../package.json';

export default function () {
  console.log('version:' + version);
}

编写 Rollup 配置文件 rollup.config.js,加入 JSON 插件:

// rollup.config.js
import json from 'rollup-plugin-json';

export default {
    input: 'src/main.js',
    output: {
      file: 'dist/bundle.js',
      format: 'cjs'
    },
    plugins: [ json() ]
};

npm run build 执行 Rollup。结果如下:

'use strict';

var version = "0.0.1";

// src/main.js

function main$1 () {
  console.log('version:' + version);
}

module.exports = main$1;

插件列表

rollup-plugin-commonjs

有时候我们会引入一些其他模块的文件(第三方的或是自己编写的),但是目前,npm 中的大多数包都是以 CommonJS 模块的形式出现的。在它们更改之前,我们需要将CommonJS模块转换为 ES2015 供 Rollup 解析。这个rollup-plugin-commonjs插件就是用来将 CommonJS 转换成 ES2015 模块的。请注意,rollup-plugin-commonjs 应该用在其他插件转换你的模块之前 - 这是为了防止其他插件的改变破坏 CommonJS 的检测。

在我们使用之前,需要先安装它:

$ npm i rollup-plugin-commonjs --save-dev

编写 Rollup 配置文件 rollup.config.js:

// rollup.config.js
import json from 'rollup-plugin-json';
import resolve from 'rollup-plugin-node-resolve';
import babel from 'rollup-plugin-babel';
import commonjs from 'rollup-plugin-commonjs'

export default {
    input: 'src/main.js',
    output: {
      file: 'dist/bundle.js',
      format: 'cjs'
    },
    plugins: [
      json(),
      resolve(),
      commonjs(),
      babel({
        exclude: 'node_modules/**' // 只编译我们的源代码
      })
    ]
};

编写 cjs 模块的文件:

exports.foo1 = function() {
    console.log('function foo1')
}

exports.foo2 = function() {
    console.log('function foo2')
}

npm run build 执行 Rollup。结果如下:

'use strict';

var version = "0.0.1";

// src/main.js

var main$1 = (function () {
  console.log('version:' + version);
});

module.exports = main$1;

rollup-plugin-uglify

代码发布时,我们经常会把自己的代码压缩到最小,以减少网络请求中的传输文件大小。Rollup rollup-plugin-uglify 就是来帮你压缩代码的,在使用之前,我们先安装它:

$ npm i rollup-plugin-uglify --save-dev

编写 Rollup 配置文件 rollup.config.js,加入 uglify 插件:

// rollup.config.js
import json from 'rollup-plugin-json';
import resolve from 'rollup-plugin-node-resolve';
import babel from 'rollup-plugin-babel';
import commonjs from 'rollup-plugin-commonjs';
import { uglify } from 'rollup-plugin-uglify'

export default {
    input: 'src/main.js',
    output: {
        file: 'dist/bundle.js',
        name: 'result',
        format: 'amd',
        globals: {
            jquery: 'jQuery'
        },
        paths: {
            jquery: 'https://cdn.bootcss.com/jquery/3.2.1/jquery.js'
        }
    },
    plugins: [
      json(),
      resolve(),
      commonjs(),
      babel({
        exclude: 'node_modules/**' // 只编译我们的源代码
      }),
      uglify()
    ],
    external: ['jquery']
};

运行打包命令,查看打包后的目标文件,发现代码已经被压缩了。但是,压缩过的代码在 debug 时会带来很大的不便,因此我们需要在压缩代码的同时生成一个 sourceMap 文件。幸运的是,Rollup 自己就支持 sourceMap 文件的生成,不需要我们去引入其他插件,只需要在配置文件中 output 选项加上以下代码即可:

// rollup.config.js
sourcemap: true

若是将 sourceMap 属性的值设置为 inline,则会将 sourceMap 的内容添加到打包文件的最后。

rollup-plugin-eslint

在大型工程的团队开发中,我们需要保证团队代码风格的一致性,因此需要引入 eslint,而且在打包时需要检测源文件是否符合 eslint 设置的规范,若是不符合则抛出异常并停止打包。Rollup rollup-plugin-eslint 就是用于设置代码规范,使用之前我们先安装它:

$ npm i eslint rollup-plugin-eslint --save-dev

编写 eslint 配置文件 .eslintrc:

{
    "env": {
        "browser": true,
        "commonjs": true,
        "es6": true,
        "node": true
    },
    "parserOptions": {
        "ecmaFeatures": {
            "jsx": false
        },
        "sourceType": "module"
    },
    "rules": {
        "semi": ["error","never"]
    }
}

编写 Rollup 配置文件 rollup.config.js,加入 eslint 插件:

// rollup.config.js
import json from 'rollup-plugin-json';
import resolve from 'rollup-plugin-node-resolve';
import babel from 'rollup-plugin-babel';
import commonjs from 'rollup-plugin-commonjs';
import { uglify } from 'rollup-plugin-uglify';
import { eslint } from 'rollup-plugin-eslint';

export default {
    input: 'src/main.js',
    output: {
        file: 'dist/bundle.js',
        name: 'result',
        format: 'amd',
        globals: {
            jquery: 'jQuery'
        },
        paths: {
            jquery: 'https://cdn.bootcss.com/jquery/3.2.1/jquery.js'
        },
        sourcemap: true
    },
    plugins: [
        eslint({
            throwOnError: true,
            throwOnWarning: true,
            include: ['src/**'],
            exclude: ['node_modules/**']
        }),
        json(),
        resolve(),
        commonjs(),
        babel({
            exclude: 'node_modules/**' // 只编译我们的源代码
        }),
        uglify()
    ],
    external: ['jquery']
};

这里有两个属性需要特别说明下:throwOnError 和 throwOnWarning 设置为 true 时,如果在 eslint 的检查过程中发现了 error 或 warning,就会抛出异常,阻止打包继续执行(如果设置为 false,就只会输出 eslint 检测结果,而不会停止打包)。

如果我们使用IDE或编辑器的 eslint 插件,有时候这些插件会去检查打包完的文件,导致你的提示框里一直会有 eslint 检测到错误的消息,我们现在有两种解决方案,第一种是创建一个 .eslintignore 文件,将打包文件加进去,让 eslint 忽略这个文件,还有一种就是让 Rollup 在打包文件的开始和最后自动生成注释来阻止 eslint 检测代码,使用这种方法时,需要使用 Rollup 配置文件的两个属性:banner和footer,这两个属性会在生成文件的开头和结尾插入一段你自定义的字符串。我们利用这个属性,在打包文件的开头添加/*eslint-disable */注释,让 eslint 不检测这个文件。

添加banner和footer属性

banner: '/*eslint-disable */'

如果说 banner 和 footer 是在文件开始和结尾添加字符串,那么 intro 和 outro 就是在被打包的代码开头和结尾添加字符串了,以 iife 模式来举例,如果我们配置了这四个属性,那么输出结果就会是:

// banner字符串
(function () {
'use strict';
// intro字符串

// 被打包的代码

// outro字符串
}());
// footer字符串

rollup-plugin-replace

有时候我们会把开发/生产环境的信息直接写在源文件里面,这个时候用 intro/outro 来注入代码的方式就不适合了。这个时候我们就需要使用 rollup-plugin-replace 插件来对源代码的变量值进行替换,在使用之前,我们先安装它:

$ npm i rollup-plugin-replace --save-dev

编写 Rollup 配置文件 rollup.config.js,加入 replace 插件:

// rollup.config.js
import json from 'rollup-plugin-json';
import resolve from 'rollup-plugin-node-resolve';
import babel from 'rollup-plugin-babel';
import commonjs from 'rollup-plugin-commonjs';
import { uglify } from 'rollup-plugin-uglify';
import { eslint } from 'rollup-plugin-eslint';
import { version } from '../package.json';
import replace  from 'rollup-plugin-replace';

const VERSION = process.env.VERSION || version;

const copyright = new Date().getFullYear() > 2018 ? '2018-' + new Date().getFullYear() : 2018;

const banner =
  '/*!\n' +
  ' * idebug v' + VERSION + '\n' +
  ' * (c) ' + copyright + ' Weich\n' +
  ' * Released under the MIT License.\n' +
  ' */';

// const weexFactoryPlugin = {
//     intro () {
//         return 'module.exports = function weexFactory (exports, document) {'
//     },
//     outro () {
//         return '}'
//     }
// };

export default {
    input: 'src/main.js',
    output: {
        banner: banner,
        footer: '/* my-library version ' + VERSION + ' */',
        file: 'dist/bundle.js',
        name: 'result',
        format: 'iife',
        globals: {
            jquery: 'jQuery'
        },
        paths: {
            jquery: 'https://cdn.bootcss.com/jquery/3.2.1/jquery.js'
        },
        sourcemap: true
    },
    plugins: [
        // weexFactoryPlugin,
        replace({
            __VERSION__: VERSION
        }),
        eslint({
            throwOnError: true,
            throwOnWarning: true,
            include: ['src/**'],
            exclude: ['node_modules/**']
        }),
        json(),
        resolve(),
        commonjs(),
        babel({
            exclude: 'node_modules/**' // 只编译我们的源代码
        }),
        uglify({
            output: {
              comments: function(node, comment) {
                  var text = comment.value;
                  var type = comment.type;
                  if (type == "comment2") {
                      // multiline comment
                      return /idebug|ENVIRONMENT/i.test(text);
                  }
              }
            }
          })
    ],
    external: ['jquery']
};

接下来就可以直接在源码中使用 __VERSION__ 了,编写入口文件 index.js:

// src/main.js
import jQuery from 'jquery'

export default () => {
    console.log(jQuery)
    console.log('version:__VERSION__' )
}

执行打包命令,并检查源文件里有没有被替换。

命令行

命令行的参数

-v/--version:打印已安装的Rollup版本号。

-w/--watch:我们在开发过程中,需要频繁对源文件进行修改,如果每次都自己手动输一遍打包命令,那真的是要烦死,因此,我们在 rollup 命令后面加上 -w/–watch 参数,就能让 rollup 监听文件变化,即时打包。

本文持续更新中

评论系统未开启,无法评论!