教程:如何使用Rollup打包JavaScript
通过这个系列教程一步一步学习如何使用更小更快的Rollup取代webpack和Browserify打包JavaScript文件。
这周,我们要使用Rollup构建我们的第一个项目,Rollup是一个打包JavaScript(和样式,不过下周才会做)的构建工具。
通过这个教程,我们的Rollup将能够:
合并scripts代码,
删除多余代码,
编译成对旧浏览器友好的代码,
支持在浏览器中使用Node模块,
能使用环境变量,
尽可能的压缩,减少文件大小。
Rollup是什么?
用他们自己的话说:
Rollup是下一代JavaScript模块打包工具。开发者可以在你的应用或库中使用ES2015模块,然后高效地将它们打包成一个单一文件用于浏览器和Node.js使用。
和Browserify和webpack很像。
你也可以称Rollup是一个构建工具,可以和像Grunt和Gulp等一起配置使用。但是,需要注意的一点是当你使用Grunt和Gulp来处理像打包JavaScript这样的任务时,这些工具的底层还是使用了像Rollup,Browserify或webpack这些东西。
为什么应该关注Rollup?
Rollup最令人激动的地方,就是能让打包文件体积很小。这么说很难理解,更详细的解释:相比其他JavaScript打包工具,Rollup总能打出更小,更快的包。
因为Rollup基于ES2015模块,比webpack和Browserify使用的CommonJS模块机制更高效。这也让Rollup从模块中删除无用的代码,即tree-shaking变得更容易。
当我们引入拥有大量函数和方法的三方工具或者框架时tree-shaking会变得很重要。想想lodash或者jQuery,如果我们只使用一个或者两个方法,就会因为加载其余内容而产生大量无用的开销。
Browserify和webpack就会包含大量无用的代码。但是Rollup不会 - 它只会包括我们真正用到的东西。
更新 (2016-08-22): 澄清一下,Rollup只能对ES模块上进行tree-shaking。CommonJS模块 - 像lodash和jQuery那样写的模块不能进行tree-shaking。然而,tree-shaking不是Rollup在速度/性能上唯一的优势。可以看Rich Harris的解释和Nolan Lawson的补充了解更多。
还有一个大新闻。
注意: 由于Rollup很高效,webpack 2 也将支持tree-shaking。
Part I: 如何使用Rollup处理并打包JavaScript文件
为了展示Rollup如何使用,让我们通过构建一个简单的项目来走一遍使用Rollup打包JavaScript的过程。
STEP 0: 创建一个包含将被编译的JavaScript和CSS的项目.
为了开始工作,我们需要一些用来处理的代码。这个教程里,我们将用一个小应用,可从GitHub获取。
目录结构如下:
learn-rollup/ ├── src/ │ ├── scripts/ │ │ ├── modules/ │ │ │ ├── mod1.js │ │ │ └── mod2.js │ │ └── main.js │ └── styles/ │ └── main.css └── package.json
你可以在终端执行下面的命令下载这个应用,我们将在这个教程中使用它。
# 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: 安装Rollup并且创建配置文件。
第一步,执行下面的命令安装Rollup:
npm install --save-dev rollup
然后,在learn-rollup文件夹下新建rollup.config.js。在文件中添加如下内容。
export default { entry: 'src/scripts/main.js', dest: 'build/js/main.min.js', format: 'iife', sourceMap: 'inline', };
说说每个配置项实际上做了什么:
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%文件大小。