튜토리얼: Rollup을 사용하여 JavaScript를 패키징하는 방법
이 튜토리얼 시리즈를 통해 더 작고 빠른 Rollup을 사용하여 webpack을 대체하고 Browserify를 사용하여 JavaScript 파일을 패키징하는 방법을 단계별로 알아보세요.
이번 주에는 JavaScript(및 스타일)를 패키징하는 빌드 도구인 Rollup을 사용하여 첫 번째 프로젝트를 빌드할 예정입니다. 하지만 다음 주까지는 그렇게 하지 않을 것입니다.
이 튜토리얼을 통해 롤업은 다음을 수행할 수 있습니다.
스크립트 코드 병합,
중복 코드 제거,
이전 브라우저에 친숙하도록 컴파일 코드
는 브라우저에서 Node 모듈 사용을 지원하고
는 환경 변수를 사용할 수 있으며
최대한 압축하여 파일 크기를 줄입니다.
롤업이란 무엇인가요?
자체 표현:
롤업은 차세대 JavaScript 모듈 패키징 도구입니다. 개발자는 애플리케이션이나 라이브러리에서 ES2015 모듈을 사용한 다음 브라우저와 Node.js에서 사용할 수 있도록 단일 파일로 효율적으로 패키징할 수 있습니다.
Browserify 및 webpack과 매우 유사합니다.
Grunt 및 Gulp와 같이 구성하고 함께 사용할 수 있는 빌드 도구인 Rollup을 호출할 수도 있습니다. 그러나 한 가지 주목할 점은 Grunt 및 Gulp를 사용하여 JavaScript 번들링과 같은 작업을 처리할 때 이러한 도구는 여전히 내부적으로 Rollup, Browserify 또는 webpack과 같은 것을 사용한다는 것입니다.
롤업에 주목해야 하는 이유는 무엇인가요?
롤업의 가장 흥미로운 점은 패키지 파일을 매우 작게 만들 수 있다는 것입니다. 이해하기 어렵지만 더 자세히 설명하면 다른 JavaScript 패키징 도구와 비교할 때 Rollup은 항상 더 작고 빠른 패키지를 생성할 수 있습니다.
Rollup은 ES2015 모듈을 기반으로 하기 때문에 webpack 및 Browserify에서 사용하는 CommonJS 모듈 메커니즘보다 더 효율적입니다. 이는 또한 Rollup이 모듈에서 쓸모 없는 코드(예: 트리 쉐이킹)를 제거하는 것을 더 쉽게 만듭니다.
트리 쉐이킹은 많은 기능과 메서드를 갖춘 타사 도구나 프레임워크를 도입할 때 중요해집니다. lodash나 jQuery를 생각해 보세요. 한두 가지 메서드만 사용하면 나머지 콘텐츠를 로드하여 쓸데없는 오버헤드가 많이 발생하게 됩니다.
Browserify와 webpack에는 쓸모없는 코드가 많이 포함됩니다. 그러나 Rollup은 그렇지 않습니다. 실제로 사용하는 것만 포함합니다.
업데이트(2016-08-22): 명확히 하자면 Rollup은 ES 모듈에서만 트리 쉐이킹을 수행할 수 있습니다. CommonJS 모듈 - lodash 및 jQuery와 같이 작성된 모듈은 트리 쉐이킹을 수행할 수 없습니다. 그러나 트리 쉐이킹은 롤업의 유일한 속도/성능 이점이 아닙니다. 자세한 내용은 Rich Harris의 설명과 Nolan Lawson의 추가 내용을 읽어보세요.
또 하나의 빅뉴스가 있습니다.
참고: 롤업은 매우 효율적이므로 webpack 2는 트리 쉐이킹도 지원합니다.
1부: Rollup을 사용하여 JavaScript 파일을 처리하고 패키징하는 방법
Rollup이 어떻게 사용되는지 보여주기 위해 간단한 프로젝트를 빌드하여 Rollup을 사용하여 JavaScript를 패키징하는 과정을 살펴보겠습니다.
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/
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%文件大小。