經過這個系列教程一步一步學習如何使用更小更快的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進行建議或者修改。