跳到主要內容
黯羽輕揚每天積累一點點

從 webpack 到 rollup

免費2017-09-30#Tool#webpack缺陷#rollup入门指南#slow __webpack_require__#rollup tutorial#babel guide

為什麼要丟棄 webpack,改用 rollup?

一。放棄 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 ScriptEvaluate Script 上,其餘部分可以忽略不計,如下圖:

[caption id="attachment_1531" align="alignnone" width="844"]rollup-performance 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

umdcjs 有優勢,看起來很奇怪,但實際結果確實是這樣。看 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-rollupexternal-helpers 的作用後面介紹

如果想保留 ES6 風格,需要這樣的 babel 配置:

{
  "presets": [
    ["stage-0"]
  ],
  "plugins": [
    "external-helpers"
  ]
}

轉換後得到的是把項目各模塊文件拼在一起的ES6 模塊,代碼里的 classconstlet 都會保留,因為 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 是唯一的選擇

參考資料

評論

暫無評論,快來發表你的看法吧

提交評論