经过这个系列教程一步一步学习如何使用更小更快的Rollup取代webpack和Browserify打包JavaScript文件。javascript
这周,咱们要使用Rollup构建咱们的第一个项目,Rollup是一个打包JavaScript(和样式,不过下周才会作)的构建工具。css
经过这个教程,咱们的Rollup将可以:html
合并scripts代码,java
删除多余代码,node
编译成对旧浏览器友好的代码,webpack
支持在浏览器中使用Node模块,git
能使用环境变量,es6
尽量的压缩,减小文件大小。github
至少懂一点JavaScript的话将会更好理解。web
对ES2015 modules有基本了解,不过不了解也无妨。
在你的设备上要有npm。(尚未?在这下载Node.js)
用他们本身的话说:
Rollup是下一代JavaScript模块打包工具。开发者能够在你的应用或库中使用ES2015模块,而后高效地将它们打包成一个单一文件用于浏览器和Node.js使用。
和Browserify和webpack很像。
你也能够称Rollup是一个构建工具,能够和像Grunt和Gulp等一块儿配置使用。可是,须要注意的一点是当你使用Grunt和Gulp来处理像打包JavaScript这样的任务时,这些工具的底层仍是使用了像Rollup,Browserify或webpack这些东西。
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。
为了展现Rollup如何使用,让咱们经过构建一个简单的项目来走一遍使用Rollup打包JavaScript的过程。
为了开始工做,咱们须要一些用来处理的代码。这个教程里,咱们将用一个小应用,可从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/
第一步,执行下面的命令安装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。
当建立好配置文件后,在终端执行下面的命令测试每项配置是否工做:
./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生成的大了两倍多。
如今咱们已经获得能在现代浏览器中运行的包文件了,可是在一些旧版本浏览器中就会崩溃 - 这并不理想。
幸运的是,Babel已发布了。这个项目编译JavaScript新特性(ES6/ES2015等等)到ES5, 差很少在今天的任何浏览器上都能运行。
若是你还没用过Babel,那么你的开发生涯要永远地改变了。使用JavaScript的新方法让语言更简单,更简洁并且总体上更友好。
那么让咱们为Rollup加上这个过程,就不用担忧上面的问题了。
首先,咱们须要安装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
。
在文件中,import
Babel插件,将它添加到新配置属性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甚至更早的浏览器也能使用。
在你的项目中使用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仍是要本身花时间去解决。
若是你的依赖中有任何使用Node风格的模块这个插件就很重要。若是没有它,你会获得关于require
的错误。
在这个小项目中不引用三方模块很正常,但实际项目中不会如此。因此为了让咱们的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
从新打包,而后再检查浏览器输出:
成功了!日志如今打印出来了。
环境变量使开发流程更强大,让咱们有能力作一些事情,好比打开或关闭日志,注入仅在开发环境使用的脚本等等。
那么让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
目录和其中的所有包。(幸好@wesleycoder先在这上面踩坑了。)
首先,从新打包而后在浏览器中检查。控制台日志会显示,就像以前同样。很棒 - 这意味着咱们的默认值生效了。
为了展现新引入的能力,咱们在production
模式下运行命令:
NODE_ENV=production ./node_modules/.bin/rollup -c
注意: 在Windows上,使用
SET NODE_ENV=production ./node_modules/.bin/rollup -c
防止在设置环境变量时报错。
当刷新浏览器后,控制台没有任何日志打出了:
不改变任何代码的状况下,使用一个环境变量禁用了日志插件。
这个教程中最后一步是添加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()
。
保存配置文件,让咱们在生成环境下运行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%文件大小。
在这个系列的下一节,咱们将了解经过Rollup和PostCSS处理样式,而且添加live reloading来实时看见咱们的修改。
The cost of small modules - 这篇文章让我开始对Rollup感兴趣,由于它展现了一些Rollup相比webpack和Browserify的优点。
注1: 这是一个很是难理解的概念,因此没全理解也不要有压力。简单来讲,咱们但愿咱们的代码在他们本身的做用域中,防止和其它脚本的冲突。IIFE是一个包括咱们的代码在自身做用域的一个[闭包]。
注2:It’s important to keep in mind, though, that when we’re dealing with such a small example app it doesn’t take much to double a file size. The comparison at this point is ~3KB vs. ~8KB.
注3:做为曾经花数小时找bug而后发现拼错一个变量名的人,不须要夸大使用linter带来的效率提高。
注4:举个例子,使用这种方法来赋默认值时很是常见的。(好比var foo = maybeThisExists || 'default';
)
这篇文章的代码放在GitHub上。你能够fork 这个仓库进行修改或测试,开issue或者报告bug,或者新建pull request进行建议或者修改。