Home > Article > Web Front-end > How to package JavaScript using Rollup
Tutorial: How to use Rollup to package JavaScript
Learn step by step through this series of tutorials how to use the smaller and faster Rollup to replace webpack and Browserify to package JavaScript files.
This week, we’re building our first project using Rollup, a build tool that packages JavaScript (and styles, but we won’t do that until next week).
Through this tutorial, our Rollup will be able to:
merge scripts code,
delete redundant code,
compile into code that is friendly to old browsers,
support the use of Node modules in browsers,
can Use environment variables to
compress as much as possible to reduce file size.
What is Rollup?
In their own words:
Rollup is the next generation JavaScript module packaging tool. Developers can use ES2015 modules in your application or library, and then efficiently package them into a single file for use in the browser and Node.js.
Very similar to Browserify and webpack.
You can also call Rollup a build tool that can be configured and used together with things like Grunt and Gulp. However, one thing to note is that when you use Grunt and Gulp to handle tasks like bundling JavaScript, these tools still use things like Rollup, Browserify, or webpack under the hood.
Why should you pay attention to Rollup?
The most exciting thing about Rollup is that it can make the package file size very small. This is hard to understand, but a more detailed explanation: Compared with other JavaScript packaging tools, Rollup can always produce smaller and faster packages.
Because Rollup is based on ES2015 modules, it is more efficient than the CommonJS module mechanism used by webpack and Browserify. This also makes it easier for Rollup to remove useless code from modules, i.e. tree-shaking.
Tree-shaking will become important when we introduce third-party tools or frameworks with a large number of functions and methods. Think about lodash or jQuery, if we only use one or two methods, we will incur a lot of useless overhead by loading the rest of the content.
Browserify and webpack will contain a lot of useless code. But Rollup won't - it will only include things we actually use.
Update (2016-08-22): To clarify, Rollup can only perform tree-shaking on ES modules. CommonJS modules - modules written like lodash and jQuery cannot do tree-shaking. However, tree-shaking is not Rollup’s only speed/performance advantage. You can read Rich Harris's explanation and Nolan Lawson's addition to learn more.
There is another big news.
Note: Since Rollup is very efficient, webpack 2 will also support tree-shaking.
Part I: How to use Rollup to process and package JavaScript files
To show how Rollup is used, let us go through the process of using Rollup to package JavaScript by building a simple project.
STEP 0: Create a project that contains JavaScript and CSS that will be compiled.
To get started, we need some code to work with. For this tutorial, we will use a small application, available on GitHub.
The directory structure is as follows:
learn-rollup/ ├── src/ │ ├── scripts/ │ │ ├── modules/ │ │ │ ├── mod1.js │ │ │ └── mod2.js │ │ └── main.js │ └── styles/ │ └── main.css └── package.json
You can execute the following command in the terminal to download this application, which we will use in this tutorial.
# Move to the folder where you keep your dev projects. cd /path/to/your/projects # Clone the starter branch of the app from GitHub. git clone -b step-0 --single-branch https://github.com/jlengstorf/learn-rollup.git # The files are downloaded to /path/to/your/projects/learn-rollup/
STEP 1: Install Rollup and create configuration file.
The first step is to install Rollup by executing the following command:
npm install --save-dev rollup
Then, create a new rollup.config.js in the learn-rollup folder. Add the following content to the file.
export default { entry: 'src/scripts/main.js', dest: 'build/js/main.min.js', format: 'iife', sourceMap: 'inline', };
Let’s talk about what each configuration item actually does:
entry — 希望Rollup处理的文件路径。大多数应用中,它将是入口文件,初始化所有东西并启动应用。
dest — 编译完的文件需要被存放的路径。
format — Rollup支持多种输出格式。因为我们是要在浏览器中使用,需要使用立即执行函数表达式(IIFE)[注1]
sourceMap — 调试时sourcemap是非常有用的。这个配置项会在生成文件中添加一个sourcemap,让开发更方便。
NOTE: 对于其他的format选项以及你为什么需要他们,看Rollup’s wiki。
测试Rollup配置
当创建好配置文件后,在终端执行下面的命令测试每项配置是否工作:
./node_modules/.bin/rollup -c
在你的项目下会出现一个build目录,包含js子目录,子目录中包含生成的main.min.js文件。
在浏览器中打开build/index.html可以看到打包文件正确生成了。
完成第一步后我们的示例项目的状态。
注意:现在,只有现代浏览器下不会报错。为了能够在不支持ES2015/ES6的老浏览器中运行,我们需要添加一些插件。
看看打包出来的文件
事实上Rollup强大是因为它使用了“tree-shaking”,可以在你引入的模块中删除没有用的代码。举个例子,在src/scripts/modules/mod1.js中的sayGoodbyeTo()函数在我们的应用中并没有使用 - 而且因为它从不会被使用,Rollup不会将它打包到bundle中:
(function () { 'use strict'; /** * Says hello. * @param {String} name a name * @return {String} a greeting for `name` */ function sayHelloTo( name ) { const toSay = `Hello, ${name}!`; return toSay; } /** * Adds all the values in an array. * @param {Array} arr an array of numbers * @return {Number} the sum of all the array values */ const addArray = arr => { const result = arr.reduce((a, b) => a + b, 0); return result; }; // Import a couple modules for testing. // Run some functions from our imported modules. const result1 = sayHelloTo('Jason'); const result2 = addArray([1, 2, 3, 4]); // Print the results on the page. const printTarget = document.getElementsByClassName('debug__output')[0]; printTarget.innerText = `sayHelloTo('Jason') => ${result1}\n\n` printTarget.innerText += `addArray([1, 2, 3, 4]) => ${result2}`; }()); //# sourceMappingURL=data:application/json;charset=utf-8;base64,...
其他的构建工具则不是这样的,所以如果我们引入了一个像lodash这样一个很大的库而只是使用其中一两个函数时,我们的包文件会变得非常大。
比如使用webpack的话,sayGoodbyeTo()也会打包进去,产生的打包文件比Rollup生成的大了两倍多。
STEP 2: 配置babel支持JavaScript新特性。
现在我们已经得到能在现代浏览器中运行的包文件了,但是在一些旧版本浏览器中就会崩溃 - 这并不理想。
幸运的是,Babel已发布了。这个项目编译JavaScript新特性(ES6/ES2015等等)到ES5, 差不多在今天的任何浏览器上都能运行。
如果你还没用过Babel,那么你的开发生涯要永远地改变了。使用JavaScript的新方法让语言更简单,更简洁而且整体上更友好。
那么让我们为Rollup加上这个过程,就不用担心上面的问题了。
INSTALL THE NECESSARY MODULES.
安装必要模块
首先,我们需要安装Babel Rollup plugin和适当的Babel preset。
# Install Rollup’s Babel plugin. npm install --save-dev rollup-plugin-babel # Install the Babel preset for transpiling ES2015 using Rollup. npm install --save-dev babel-preset-es2015-rollup
提示: Babel preset是告诉Babel我们实际需要哪些babel插件的集合。
创建.babelrc
然后,在项目根目录(learn-rollup/)下创建一个.babelrc文件。在文件中添加下面的JSON:
{ "presets": ["es2015-rollup"], }
它会告诉Babel在转换时哪些preset将会用到。
更新rollup.config.js
为了让它能真正工作,我们需要更新rollup.config.js。
在文件中,importBabel插件,将它添加到新配置属性plugins中,这个属性接收一个插件组成的数组。
// Rollup plugins import babel from 'rollup-plugin-babel'; export default { entry: 'src/scripts/main.js', dest: 'build/js/main.min.js', format: 'iife', sourceMap: 'inline', plugins: [ babel({ exclude: 'node_modules/**', }), ], };
为避免编译三方脚本,通过设置exclude属性忽略node_modules目录。
检查输出文件
全部都安装并配置好后,重新打包一下:
./node_modules/.bin/rollup -c
再看一下输出结果,大部分是一样的。但是有一些地方不一样:比如,addArray()这个函数:
var addArray = function addArray(arr) { var result = arr.reduce(function (a, b) { return a + b; }, 0); return result; };
Babel是如何将箭头函数(arr.reduce((a, b) => a + b, 0))转换成一个普通函数的呢?
这就是编译的意义:结果是相同的,但是现在的代码可以向后支持到IE9.
注意: Babel也提供了babel-polyfill,使得像Array.prototype.reduce()这些方法在IE8甚至更早的浏览器也能使用。
STEP 3: 添加ESLint检查常规JavaScript错误
在你的项目中使用linter是个好主意,因为它强制统一了代码风格并且能帮你发现很难找到的bug,比如花括号或者圆括号。
在这个项目中,我们将使用ESLint。
安装模块
为使用ESLint,我们需要安装ESLint Rollup plugin:
npm install --save-dev rollup-plugin-eslint
生成一个.eslintrc.json
为确保我们只得到我们想检测的错误,首先要配置ESLint。很幸运,我们可以通过执行下面的命令自动生成大多数配置:
$ ./node_modules/.bin/eslint --init ? How would you like to configure ESLint? Answer questions about your style ? Are you using ECMAScript 6 features? Yes ? Are you using ES6 modules? Yes ? Where will your code run? Browser ? Do you use CommonJS? No ? Do you use JSX? No ? What style of indentation do you use? Spaces ? What quotes do you use for strings? Single ? What line endings do you use? Unix ? Do you require semicolons? Yes ? What format do you want your config file to be in? JSON Successfully created .eslintrc.json file in /Users/jlengstorf/dev/code.lengstorf.com/projects/learn-rollup
如果你按上面展示的那样回答问题,你将在生成的.eslintrc.json中得到下面的内容:
{ "env": { "browser": true, "es6": true }, "extends": "eslint:recommended", "parserOptions": { "sourceType": "module" }, "rules": { "indent": [ "error", 4 ], "linebreak-style": [ "error", "unix" ], "quotes": [ "error", "single" ], "semi": [ "error", "always" ] } }
修改.eslintrc.json
然而我们需要改动两个地方来避免项目报错。
使用2空格代替4空格。
后面会使用到ENV这个全局变量,因此要把它加入白名单中。
在.eslintrc.json进行如下修改 — 添加globals属性并修改indent属性:
{ "env": { "browser": true, "es6": true }, "globals": { "ENV": true }, "extends": "eslint:recommended", "parserOptions": { "sourceType": "module" }, "rules": { "indent": [ "error", 2 ], "linebreak-style": [ "error", "unix" ], "quotes": [ "error", "single" ], "semi": [ "error", "always" ] } }
更新rollup.config.js
然后,引入ESLint插件并添加到Rollup配置中:
// Rollup plugins import babel from 'rollup-plugin-babel'; import eslint from 'rollup-plugin-eslint'; export default { entry: 'src/scripts/main.js', dest: 'build/js/main.min.js', format: 'iife', sourceMap: 'inline', plugins: [ babel({ exclude: 'node_modules/**', }), eslint({ exclude: [ 'src/styles/**', ] }), ], };
检查控制台输出
第一次,当执行./node_modules/.bin/rollup -c时,似乎什么都没发生。因为这表示应用的代码通过了linter,没有问题。
但是如果我们制造一个错误 - 比如删除一个分号 - 我们会看到ESLint是如何提示的:
$ ./node_modules/.bin/rollup -c /Users/jlengstorf/dev/code.lengstorf.com/projects/learn-rollup/src/scripts/main.js 12:64 error Missing semicolon semi ✖ 1 problem (1 error, 0 warnings)
一些包含潜在风险和解释神秘bug的东西立刻出现了,包括出现问题的文件,行和列。
但是它不能排除我们调试时的所有问题,很多由于拼写错误和疏漏产生的bug还是要自己花时间去解决。
STEP 4: 添加插件处理非ES模块
如果你的依赖中有任何使用Node风格的模块这个插件就很重要。如果没有它,你会得到关于require的错误。
添加一个Node模块作为依赖
在这个小项目中不引用三方模块很正常,但实际项目中不会如此。所以为了让我们的Rollup配置变得真正可用,需要保证在我们的代码中也能引用是三方模块。
举个简单的例子,我们将使用debug包添加一个简单的日志打印器到项目中。先安装它:
npm install --save debug
注意:因为它是会在主程序中引用的,应该使用--save参数,可以避免在生产环境下出现错误,因为devDependencies在生产环境下不会被安装。
然后在src/scripts/main.js中添加一个简单的日志:
// Import a couple modules for testing. import { sayHelloTo } from './modules/mod1'; import addArray from './modules/mod2'; // Import a logger for easier debugging. import debug from 'debug'; const log = debug('app:log'); // Enable the logger. debug.enable('*'); log('Logging is enabled!'); // Run some functions from our imported modules. const result1 = sayHelloTo('Jason'); const result2 = addArray([1, 2, 3, 4]); // Print the results on the page. const printTarget = document.getElementsByClassName('debug__output')[0]; printTarget.innerText = `sayHelloTo('Jason') => ${result1}\n\n`; printTarget.innerText += `addArray([1, 2, 3, 4]) => ${result2}`;
到此一切都很好,但是当运行rollup时会得到一个警告:
$ ./node_modules/.bin/rollup -c Treating 'debug' as external dependency No name was provided for external module 'debug' in options.globals – guessing 'debug'
而且如果在查看index.html,会发现一个ReferenceError抛出了:
默认情况下,三方的Node模块无法在Rollup中正确加载。
哦,真糟糕。完全无法运行。
因为Node模块使用CommonJS,无法与Rollup直接兼容。为解决这个问题,需要添加一组处理Node模块和CommonJS模块的插件。
安装模块
围绕这个问题,我们将在Rollup中新增两个插件:
rollup-plugin-node-resolve,运行加载node_modules中的三方模块。
rollup-plugin-commonjs,将CommonJS模块转换成ES6,防止他们在Rollup中失效。
通过下面的命令安装两个插件:
npm install --save-dev rollup-plugin-node-resolve rollup-plugin-commonjs
更新rollup.config.js.
然后,引入插件并添加进Rollup配置:
// Rollup plugins import babel from 'rollup-plugin-babel'; import eslint from 'rollup-plugin-eslint'; import resolve from 'rollup-plugin-node-resolve'; import commonjs from 'rollup-plugin-commonjs'; export default { entry: 'src/scripts/main.js', dest: 'build/js/main.min.js', format: 'iife', sourceMap: 'inline', plugins: [ resolve({ jsnext: true, main: true, browser: true, }), commonjs(), eslint({ exclude: [ 'src/styles/**', ] }), babel({ exclude: 'node_modules/**', }), ], };
注意: jsnext属性是为了帮助Node模块迁移到ES2015的一部分。main和browser 属性帮助插件决定哪个文件应该被bundle文件使用。
检查控制台输出
执行./node_modules/.bin/rollup -c重新打包,然后再检查浏览器输出:
成功了!日志现在打印出来了。
STEP 5: 添加插件替换环境变量
环境变量使开发流程更强大,让我们有能力做一些事情,比如打开或关闭日志,注入仅在开发环境使用的脚本等等。
那么让Rollup支持这些功能吧。
在main.js中添加ENV变量
让我们通过一个环境变量控制日志脚本,让日志脚本只能在非生产环境下使用。在src/scripts/main.js中修改log()的初始化方式。
// Import a logger for easier debugging. import debug from 'debug'; const log = debug('app:log'); // The logger should only be disabled if we’re not in production. if (ENV !== 'production') { // Enable the logger. debug.enable('*'); log('Logging is enabled!'); } else { debug.disable(); }
然而,重新打包(./node_modules/.bin/rollup -c)后检查浏览器,会看到一个ENV的ReferenceError。
不必惊讶,因为我们没有在任何地方定义它。如果我们尝试ENV=production ./node_modules/.bin/rollup -c,还是不会成功。因为那样设置的环境变量只是在Rollup中可用,不是在Rollup打包的bundle中可用。
我们需要使用一个插件将环境变量传入bundle。
安装模块
安装rollup-plugin-replace插件,它本质上只是做了查找-替换的工作。它能做很多事情,但现在我们只需要让它简单地找到出现的环境变量并将其替换成实际的值。(比如,所有在bundle出现的ENV变量都会被替换成"production" )。
npm install --save-dev rollup-plugin-replace
更新rollup.config.js
在rollup.config.js中引入插件并且添加到插件列表中。
配置非常简单:只需添加一个键值对的列表,key是将被替换的字符串,value是应该被替换成的值。
// Rollup plugins import babel from 'rollup-plugin-babel'; import eslint from 'rollup-plugin-eslint'; import resolve from 'rollup-plugin-node-resolve'; import commonjs from 'rollup-plugin-commonjs'; import replace from 'rollup-plugin-replace'; export default { entry: 'src/scripts/main.js', dest: 'build/js/main.min.js', format: 'iife', sourceMap: 'inline', plugins: [ resolve({ jsnext: true, main: true, browser: true, }), commonjs(), eslint({ exclude: [ 'src/styles/**', ] }), babel({ exclude: 'node_modules/**', }), replace({ exclude: 'node_modules/**', ENV: JSON.stringify(process.env.NODE_ENV || 'development'), }), ], };
在我们的配置中,将找打所有出现的ENV并且替换成process.env.NODE_ENV - 在Node应用中最普遍的设置环境变量的方法 - 或者 "development"中的一个。使用JSON.stringify()确保值被双引号包裹,如果ENV没有的话。
为了确保不会和三方代码造成问题,同样设置exclude属性来忽略node_modules目录和其中的全部包。
检查结果
首先,重新打包然后在浏览器中检查。控制台日志会显示,就像之前一样。很棒 - 这意味着我们的默认值生效了。
为了展示新引入的能力,我们在production模式下运行命令:
NODE_ENV=production ./node_modules/.bin/rollup -c
注意: 在Windows上,使用SET NODE_ENV=production ./node_modules/.bin/rollup -c防止在设置环境变量时报错。
当刷新浏览器后,控制台没有任何日志打出了:
不改变任何代码的情况下,使用一个环境变量禁用了日志插件。
STEP 6: 添加UglifyJS压缩减小生成代码体积
这个教程中最后一步是添加UglifyJS来减小和压缩bundle文件。可以通过移除注释,缩短变量名和其他压缩换行等方式大幅度减少bundle的大小 - 会让文件的可读性变差,但提高了网络间传输的效率。
安装插件
我们将使用UglifyJS压缩bundle,通过rollup-plugin-uglify插件。
通过下面命令安装:
npm install --save-dev rollup-plugin-uglify
更新rollup.config.js
然后添加Uglify到Rollup配置。为了开发环境下可读性更好,设置代码丑化仅在生产环境下使用:
// Rollup plugins import babel from 'rollup-plugin-babel'; import eslint from 'rollup-plugin-eslint'; import resolve from 'rollup-plugin-node-resolve'; import commonjs from 'rollup-plugin-commonjs'; import replace from 'rollup-plugin-replace'; import uglify from 'rollup-plugin-uglify'; export default { entry: 'src/scripts/main.js', dest: 'build/js/main.min.js', format: 'iife', sourceMap: 'inline', plugins: [ resolve({ jsnext: true, main: true, browser: true, }), commonjs(), eslint({ exclude: [ 'src/styles/**', ] }), babel({ exclude: 'node_modules/**', }), replace({ ENV: JSON.stringify(process.env.NODE_ENV || 'development'), }), (process.env.NODE_ENV === 'production' && uglify()), ], };
我们使用了短路运算,很常用(虽然也有争议)的条件性设置值的方法。[注4]
在我们的例子中,只有在NODE_ENV是"production"时才会加载uglify()。
检查压缩过的bundle
保存配置文件,让我们在生成环境下运行Rollup:
NODE_ENV=production ./node_modules/.bin/rollup -c
注意: 在Windows上,使用SET NODE_ENV=production ./node_modules/.bin/rollup -c防止在设置环境变量时报错。
输出内容并不美观,但是更小了。这有build/js/main.min.js的截屏,看起来像这样:
丑化过的代码确实能更高效地传输。
之前,我们的bundle大约42KB。使用UglifyJS后,减少到大约29KB - 在没做其他优化的情况下节省了超过30%文件大小。