一。放棄 webpack 的原因
1.webpack 模塊可讀性太低
// 引用模块
var _myModule1 = __webpack_require__(0);
var _myModule2 = __webpack_require__(10);
var _myModule3 = __webpack_require__(24);
// 模块定义
/* 10 */
/***/function (module, exports, __webpack_require__) {...}
// 源码
_myModule2.default.xxx()
這種代碼讀起來相當費勁,先找到 _myModule2 對應的 __webpack_require__ id,再找對應的模塊定義,最後看該模塊 exports 身上掛了什麼東西。模塊定義這個部分很討厭,延長了閱讀引用鏈
當然,一般不需要讀 bundle,這一點並不致命
2.文件很大
如上面提到的,這些額外的 bundle 代碼(子模塊定義、子模塊引用等等)導致文件體積膨脹,因為:
-
源碼每個獨立文件外面都包了一層模塊定義
-
模塊內對其它模塊的引用都插了一條
__webpack_require__聲明 -
__webpack_require__工具函數自身的體積
文件體積不但會帶來傳輸負擔,還會影響 Compile 時間,打包方案的 bundle size 是一項重要指標
3.執行很慢
子模塊定義和運行時依賴處理(__webpack_require__),不僅導致文件體積增大,還會大幅拉低性能,如下圖:

(圖片來自 webpack_require is too slow)
打包方案對性能產生大幅影響,這是一點最為致命,無法忍受
二.rollup 的優勢
1.文件很小
幾乎沒什麼多餘代碼,除了必要的 cjs, umd 頭外,bundle 代碼基本和源碼沒什麼差異,沒有奇怪的 __webpack_require__, Object.defineProperty
bundle 大小對比如下:
webpack 132KB
rollup 82KB
2.執行很快
因為沒什麼多餘代碼,如上文提到的,webpack bundle 不僅體積大,非業務代碼(__webpack_require__, Object.defineProperty)執行耗時也不容小視
rollup 沒有生成這些額外的東西,執行耗時主要在於 Compile Script 和 Evaluate Script 上,其餘部分可以忽略不計,如下圖:
[caption id="attachment_1531" align="alignnone" width="844"]
rollup-performance[/caption]
3.es 模塊及 iife 格式支持
// rollup
amd – Asynchronous Module Definition, used with module loaders like RequireJS
cjs – CommonJS, suitable for Node and Browserify/Webpack
es – Keep the bundle as an ES module file
iife – A self-executing function, suitable for inclusion as a <script> tag. (If you want to create a bundle for your application, you probably want to use this, because it leads to smaller file sizes.)
umd – Universal Module Definition, works as amd, cjs and iife all in one
// webpack
"var" - Export by setting a variable: var Library = xxx (default)
"this" - Export by setting a property of this: this["Library"] = xxx
"commonjs" - Export by setting a property of exports: exports["Library"] = xxx
"commonjs2" - Export by setting module.exports: module.exports = xxx
"amd" - Export to AMD (optionally named - set the name via the library option)
"umd" - Export to AMD, CommonJS2 or as property in root
支持打包 es6 模塊,對於基礎庫之類的東西很合適,因為 es6 項目一般會用 babel 轉一遍,這樣保證一次統一的 babel 翻譯
支持打包成 iife,非常小。另外,單從最終 bundle 大小來看:
default uglify
cjs 81KB 34K
amd 81KB 30KB
iife 81KB 30KB
umd 82KB 30KB
umd 比 cjs 有優勢,看起來很奇怪,但實際結果確實是這樣。看 bundle 差異主要在於函數名簡化,cjs bundle 中很多長函數名保留下來了,沒有被混淆掉
三.rollup 的缺陷
目前最新版本(0.50.0)仍然處於 0.x 的不穩定狀態,版本相關的問題比較多(甚至某些問題還需要通過版本降級來解決)
- 插件生態 相對較弱,一些常見需求無法滿足
比如打包多個依賴庫,把公共依賴項提出來(webpack 的 CommonsChunkPlugin)
-
早些版本(0.43)循環依賴處理得不好,會出現打包/執行出錯
-
文檔相對較少,遇到問題無法快速解決
比如常見錯誤 'foo' is not exported by bar.js (imported by baz.js),Troubleshooting 算是 FAQ,但沒有提供詳細可靠的解決方案(即照做了也不一定能解決)
四.babel 配置
babel 翻譯一般是必不可少的,作為 rollup/webpack 打包過程的一個中間處理環節,都提供了相應的包裝插件,可以把 babel 配置嵌進來,實際需要掌握的是 babel 配置
babel preset
In Babel, a preset is a set of plugins used to support particular language features.
常見的有:
-
es2015:僅支持 ES6 特性,如果 preset 里含有該項,會把 ES6 語法轉換為 ES5
-
stage-0:還支持最新的 es7 甚至 es8 特性,實際上是指 ES Stage 0 Proposals,如果 preset 里含有該項,會把 ESn 轉換為 ES6
-
react:支持 React JSX
stage-0 是最激進的做法,表示想要用babel 能轉的所有 JS 新特性,無論是否穩定。es2015 最保守,規範已經發布了,沒有特性不穩定的風險。像 stage-0 一樣能打的還有 4 個(TC39 規範制定流程):
- stage-0 - Strawman: just an idea, possible Babel plugin.
- stage-1 - Proposal: this is worth working on.
- stage-2 - Draft: initial spec.
- stage-3 - Candidate: complete spec and initial browser implementations.
- stage-4 - Finished: will be added to the next yearly release.
P.S.最近 babel 提供了 babel-preset-env,根據目標平台環境來自動添加 preset,就不需要裝一堆 esxxx 了,但只提供 ES 支持,react 和 polyfill 並不會內置,也不應該內置。關於 env 的更多信息,請查看 babel-preset-env: a preset that configures Babel for you
注意,各 preset 僅負責一步轉換,比如 stage-0 能把 ESn 轉 ES6,而不是 ES5,也就是說,對於一個語法很激進的項目,想要轉換成 ES5 的話,需要這樣的 babel 配置:
{
"presets": [
["stage-0"],
["es2015", {"modules": false}]
],
"plugins": [
"external-helpers"
]
}
P.S.其中,{"modules": false} 是 rollup 需要,用來代替 babel-preset-es2015-rollup,external-helpers 的作用後面介紹
如果想保留 ES6 風格,需要這樣的 babel 配置:
{
"presets": [
["stage-0"]
],
"plugins": [
"external-helpers"
]
}
轉換後得到的是把項目各模塊文件拼在一起的ES6 模塊,代碼里的 class、const、let 都會保留,因為 ES6 支持這些特性,但 async&await 之類的更高級特性會被轉換到 ES6
babel plugin
在 babel 的 3 個處理環節中:
parsing -> transforming -> generation
插件作用於第 2 個環節(transforming),即解析完源語法之後,把它轉換為等價的目標語法,在這個階段可以通過插件做進一步處理,例如簡單的:
// 把标识符成员访问转换为字面量形式,例如 a.catch -> a['catch']
es3-member-expression-literals
// 把标识符成员声明转换为字面量形式,例如{catch: xxx} -> {'catch': xxx}
es3-property-literals
還有常用的:
// 支持 class 静态属性和实例属性,例如 class A{instanceProp = 1; static staticProp = 2;}
transform-class-properties
// 把 babel 自己用的公共方法提出来,例如_createClass, _inherits 等等
external-helpers
// 常量修改检查,const 声明的常量被修改时报错
check-es2015-constants
所以 babel plugin 大致分3 類:
-
ES5/ES6 補丁,修補更低環境相關的問題(es3-xxx,es2015-xxx)
-
靜態檢查,比如 const 修改報錯提前到「編譯」階段
-
風險特性,比如 class-properties 等不適合放在 stage 里的爭議特性
補丁針對生產環境,靜態檢查是質量保證的一部分,風險特性則是更激進的一些 JS 語法
babel polyfill
babel 把 ESn 高級語法轉換到 ES5/ES3 會遇到 4 種情況:
-
簡單語法糖。無腦轉換,例如
for...of, arrow function -
複雜語法糖。需要工具函數處理,例如
createClass, inherits -
low 環境缺少的基礎特性。需要 polyfill,例如
Symbol, Promise, String.repeat -
無法被 polyfill 的特性。例如
Proxy
對於 low 環境缺少的基礎特性,babel 默認不提供 polyfill(babel 翻譯結果不含 polyfill),可以引入 babel-polyfill,或者引入想要的特殊 polyfill(更輕量小巧的,或者更可靠的重量級的)
babelHelpers
babel 有一些轉換相關的工具函數,例如:
_typeof
_instanceof
_createClass
_interopRequireDefault
_classCallCheck
_inherits
asyncGenerator
這些工具函數都屬於 babelHelpers,完整的 helpers 可以通過命令生成:
npm install babel-cli --save-dev
// type 可選 global/umd/var
./node_modules/.bin/babel-external-helpers -t umd > helpers.js
P.S.關於生成 babelHelpers 的更多信息,請查看 External helpers
默認配置下,這些工具函數會被生成多份,也就是說 bundle 中會存在多個 _createClass 聲明,是冗餘代碼。可以通過插件配置優化或去掉
默認配置,bundle 中存在多份 helper 聲明:
{
"presets": [
["es2015"]
]
}
添上 external-helpers 插件,把 helper 聲明提到 bundle 頂部,不存在多份聲明:
{
"presets": [
["es2015"]
],
"plugins": [
"external-helpers"
]
}
引用外部 babelHelpers,bundle 中不含 helper 聲明:
{
"presets": [
["es2015"]
],
"plugins": [
"external-helpers"
],
externalHelpers: true
}
一般添上 external-helpers 把 helper 提到 bundle 頂部就能滿足優化要求,所以 babel 配置都建議至少添上 external-helpers 插件去除冗餘 helper 代碼
externalHelpers: true 是針對多 bundle(multi entry)的情況,不添的話每個 bundle 頂部都有一份 helper 聲明,添上之後 bundle 都引用外部 helper,例如:
babelHelpers.createClass(xxx)
babelHelpers 在 bundle 里是未定義的,需要提前引入,比如 web 環境:
<script src="babelHelpers.js"></script>
<script src="bundle.js"></script>
五。總結
相比 webpack,rollup 擁有無可比擬的性能優勢,這是由依賴處理方式決定的,編譯時依賴處理(rollup)自然比運行時依賴處理(webpack)性能更好,但對循環依賴的處理不是百分百可靠。盡量通過內部實現(或設計)來避免,解決循環依賴的常用技巧有:
-
依賴提升,把需要相互依賴的部分提升一層
-
依賴注入,運行時從模塊外部注入依賴
-
依賴查找,運行時由模塊內部查找依賴
依賴提升針對不合理的設計,此類循環依賴是本能夠避免的,例如 A->B, B->A 可能可以通過提出 C 來轉換為 A->C, B->C
對於無法避免的循環依賴,可以通過運行時依賴注入和依賴查找來解決,例如 factory->A, A->factory,一種簡單的依賴注入方案是:
// factory.js
import A from './A';
export create() {
// 构造函数注入
return new A(create);
// 属性注入
// let a = new A();
// a._createFromFactory = create;
// return a;
}
// A.js
class A {
constructor(create) {
this._createFromFactory = create;
}
// Will be injected from factory
_createFromFactory() {
return null;
}
}
所以循環依賴是可以從設計/實現上解決的,不是大問題
就應用場景而言,rollup 最適合打包成單文件,因為目前 rollup 對 multi entry 不很友好(公共依賴項都提出來)。另外,穩定性及插件生態、文檔等還不如 webpack,但在苛求性能的場景,rollup 是唯一的選擇
暫無評論,快來發表你的看法吧