之前翻譯過一篇文章,介紹了通過 ES2015 的解構賦值語法引入模組,可以讓打包工具(browserify)最終編譯出來的程式碼量最小化。
殊不知在 webpack 1.X 版本是無法利用該特性來避免引入冗餘模組程式碼的,導致打出來的 bundle 檔案大小難免略有臃腫。
今天則向大家介紹一個當紅炸子雞——Rollup.js,通過它可以讓你的 bundle 最小化,有效減少檔案請求大小——以至於連 vue 都迅速地轉投它來打包模組。
Tree-shaking
在 Rollup 編譯模組的過程中,通過 Tree-shacking 的方式來剔除各模組中最終未被引用到的方法,通過僅保留被呼叫到的程式碼塊來縮減 bundle 的大小。
我們來看下官網的例子。
頁面入口檔案 main.js:
1 2 |
import { cube } from './maths.js'; console.log( cube( 5 ) ); // 125,即5的立方值 |
被引如的 math.js 模組如下:
1 2 3 4 5 6 7 8 9 10 |
// 注意這個方法在入口檔案裡沒有被呼叫過 //最終會被 Rollup 剔除 export function square ( x ) { return x * x; } //入口檔案需要呼叫到的求立方值的方法 export function cube ( x ) { return x * x * x; } |
通過 Rollup 打包之後如下:
1 2 3 4 5 6 7 8 9 |
'use strict'; function cube ( x ) { // rewrite this as `square( x ) * x` // and see what happens! return x * x * x; } console.log( cube( 5 ) ); // 125 |
可以很明顯地體會到 Tree-shaking 的作用 —— Math 模組裡有個從未用到的 square 方法,我們們在 bundle 檔案裡把它消滅掉了。
另外 TS 會抽取引用到的模組內容,將它們置於同一個作用域下,進而直接用變數名就可以訪問各個模組的介面;而不像 webpack 這樣每個模組外還要包一層函式定義,再通過合併進去的 define/require 相互呼叫。
當然這種方法需要 ES2015 的解構賦值語法來配合,多虧了它,Rollup 才能有效地對模組內容進行可靠的靜態分析。
使用方式
安裝自然不用說,走 npm 的老套路:
1 |
npm i rollup |
執行打包的方式也是簡單到爆:
1 |
rollup src/main.js -o rel/bundle.js |
這意味著將入口檔案 src/main.js 打包為 rel/bundle.js 檔案。
很多時候我們開發走的 ES2015 模組語法,但最終編譯出來的模組希望它能走 commonjs 語法,只需要加上 -f cjs 執行時引數(f for format)即可:
1 |
rollup src/main.js -o rel/bundle.js -f cjs |
當然,如果你想編譯為其它格式,可以把 cjs 更換為:
1 |
amd / es6 / iife / umd |
我們分別來個參考~ 假設入口檔案 src/main.js 如下:
1 2 3 4 5 6 7 |
var name = 'VaJoy'; function main () { console.log(name); } export default main; |
編譯為各種模式後的bundle:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
//////////////////////////////commonjs(-f cjs) 'use strict'; var name = 'VaJoy'; function main () { console.log(name); } module.exports = main; //////////////////////////////AMD(-f amd) define(function () { 'use strict'; var name = 'VaJoy'; function main () { console.log(name); } return main; }); //ES2015/ES6(-f es6) var name = 'VaJoy'; function main () { console.log(name); } export default main; //////////////////////////////Global(-f iife) //注意該方法需要通過配置檔案形式來執行(見下一節) var main = (function () { 'use strict'; var name = 'VaJoy'; function main () { console.log(name); } return main; }()); //////////////////////////////UMD(-f umd) //注意該方法需要通過配置檔案形式來執行(見下一節) (function (global, factory) { typeof exports === 'object' & typeof module !== 'undefined' ? module.exports = factory() : typeof define === 'function' && define.amd ? define(factory) : (global.main = factory()); }(this, function () { 'use strict'; var name = 'VaJoy'; function main () { console.log(name); } return main; })); |
配置檔案
和 webpack 一樣,rollup 也支援通過配置檔案來實現更靈活的功能。
我們在專案根目錄新建一個 rollup.config.js :
1 2 3 4 5 |
export default { entry: 'src/main.js', format: 'cjs', dest: 'rel/bundle.js' // 輸出檔案 }; |
然後執行
1 |
rollup -c |
即可通過預設配置檔案(rollup.config.js)所設定的資訊來進行打包。
如果你的配置檔案另有其名(例如“rollup.config.dev.js”),在後面加上配置檔名即可:
1 |
rollup -c rollup.config.dev.js |
Rollup 也支援使用外掛,寫到配置物件的 plugin 裡即可,這裡我們以 rollup-plugin-babel 為例:
1 2 3 4 5 6 7 8 |
import babel from 'rollup-plugin-babel'; export default { entry: 'src/main.js', format: 'cjs', plugins: [ babel() ], dest: 'rel/bundle.js' }; |
比較不爽的是,babel 的預設不像 webpack 可以直接寫在配置檔案裡,而還是得獨立寫個“src/.babelrc”(注意我們可以寫在 src 下,而不是非得放在專案根目錄下):
1 2 3 |
{ "presets": ["es2015-rollup"] } |
注意我們得確保安裝了 rollup-plugin-babel 和 babel 預設 babel-preset-es2015-rollup:
1 |
npm i rollup-plugin-babel babel-preset-es2015-rollup |
這時候就能配合 babel 一起把 ES6 的模組編譯為 ES5 的 bundle 了。
更多有趣的外掛可以在 rollup 專案組織裡找,貌似沒有 webpack 那樣專門有個外掛列表頁彙總,這點找起來不太方便。
Rollup 也支援直接在模組中來被呼叫執行,這樣很方便跟 grunt/gulp 等工具進行協作。
如我們修改 rollup.config.dev.js 內容為:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
var rollup = require( 'rollup' ); var babel = require('rollup-plugin-babel'); rollup.rollup({ entry: 'src/main.js', plugins: [ babel() ] }).then( function ( bundle ) { bundle.write({ format: 'umd', moduleName: 'main', //umd或iife模式下,若入口檔案含 export,必須加上該屬性 dest: 'rel/bundle.js' }); }); |
然後用 node 直接執行
1 |
node rollup.config.dev.js |
可以得到一樣的執行結果。
注意 “rollup.rollup()”返回一個帶著 bundle 作為 resolve 回撥引數的 Promise 物件,我們常規直接使用語法糖 bundle.write 來打包輸出檔案:
1 2 3 4 5 |
bundle.write({ format: 'umd', moduleName: 'main', dest: 'rel/bundle.js' }); |
其等價於
1 2 3 4 5 6 7 |
var result = bundle.generate({ //生成一個 bundle + sourcemap format: 'umd', moduleName: 'main', dest: 'rel/bundle.js', }); fs.writeFileSync( 'rel/bundle.js', result.code ); |
SourceMap
為了方便除錯編譯後的檔案,rollup 肯定不會忘記新增 source map 功能,而且其配置也非常簡單:
1 2 3 4 5 6 |
{ format: 'umd', moduleName: 'main', dest: 'rel/bundle.js', sourceMap: true //加上這裡即可 } |
這樣編譯後,rollup 會自動生成一個 rel/bundle.js.map 關聯到 rel/bundle.js 中。
也可以將其直接內聯在 bundle 裡而不是獨立生成一個 map 檔案:
1 2 3 4 5 6 |
{ format: 'umd', moduleName: 'main', dest: 'rel/bundle.js', sourceMap: 'inline' } |
若希望 map 檔案可以自定義位置和名稱,就得使用上面稍微提到的 bundle.generate 方法了:
1 2 3 4 5 6 |
var result = bundle.generate({ //生成一個 bundle + sourcemap //... }); fs.writeFileSync( 'rel/bundle.js', result.code ); fs.writeFileSync( 'map/bundle.js.map', result.map.toString() ); |
issue
Rollup 雖然利用 ES6 的特性幫我們節省了不少檔案大小,但它並沒有類似 webpack 的 -p 引數幫你壓縮混淆檔案。
因此即使是官方文件也推薦配合使用 UglifyJS 來進一步縮小 bundle 體積。
另外 webpack2 已經出來好幾款 beta 版本了,同樣也加上了對 Tree-shaking 的支援,相信 webpack2 出來後,Rollup 的熱度會大大消減。
共勉~