webpack配置babel篇

  • 2020 年 11 月 12 日
  • 筆記

babel-polyfill & babel-runtime & babel-preset-env

babel-core

babel-core 的作用是把 js 程式碼分析成 ast ,方便各個插件分析語法進行相應的處理。(ast=抽象語法 # 樹)

babel-polyfill

babel-polyfill 是為了模擬一個完整的ES2015+環境,旨在用於應用程式而不是庫/工具。並且使用babel-node時,這個polyfill會自動載入。這裡要注意的是babel-polyfill是一次性引入你的項目中的,並且同項目程式碼一起編譯到生產環境。而且會污染全局變數。像Map,Array.prototype.find這些就存在於全局空間中。

babel-runtime

babel-runtime不會污染全局空間和內置對象原型。事實上babel-runtime是一個模組,你可以把它作為依賴來達成ES2015的支援。

比如環境不支援Promise,你可以在項目中加入

require(『babel-runtime/core-js/promise』)

來獲取Promise。

這樣我們就彌補了babel-polyfill的缺點,達到了按需載入的效果。但是在實際項目開發過程中,我們往往會寫很多新的es6 api,每次都要手動引入相應的包比較麻煩,維護起來也不方便,每個文件重複引入也造成程式碼的臃腫。

要解決這個問題,就要用到 babel-plugin-transform-runtime,它會分析我們的 ast 中,是否有引用 babel-rumtime 中的墊片(通過映射關係),如果有,就會在當前模組頂部插入我們需要的墊片。

接下來我們嘗試一下,先安裝babel-runtime和babel-plugin-transform-runtime

npm install --save babel-runtime
npm install --save-dev babel-plugin-transform-runtime

由於 babel-runtime只是集中了polyfill的library,對應需要的 polyfill 都是要引入項目中,並跟項目程式碼一起打包的,所以就要加入到生產環境依賴中

下面在.babelrc中加入以下配置

{
  "plugins": ["transform-runtime"]
}

執行打包命令,打包出來的bundle.js的大小為63K,比完整引入polyfill小了好多。但是事物都有兩面性,babel-runtime有個缺點,它不模擬實例方法,即內置對象原型上的方法,所以類似Array.prototype.find,你通過babel-runtime是無法使用的,這隻能通過 babel-polyfill 來轉碼,因為 babel-polyfill 是直接在原型鏈上增加方法。這就悲催了,難道還是要完整引入babel-polyfill?其實還有一個解決的辦法,就是用babel-preset-env

babel-runtime的相關配置

core-js

core-js 是用於 JavaScript 的組合式標準化庫,它包含 es5 (e.g: object.freeze), es6的 promise,symbols, collections, iterators, typed arrays, es7+提案等等的 polyfills 實現。也就是說,它幾乎包含了所有 JavaScript 最新標準的墊片。不過為什麼它不把 generator 也實現了... 😁

// 比如,只不過需要單個引用
require('core-js/array/reduce');
require('core-js/object/values');

regenerator

它是來自於 facebook 的一個庫,鏈接。主要就是實現了 generator/yeild, async/await。

所以 babel-runtime 是單純的實現了 core-js 和 regenerator 引入和導出,比如這裡是 filter 函數的定義,做了一個中轉並處理了 esModule 的兼容。

module.exports = { "default": require("core-js/library/fn/array/filter"), __esModule: true };

helpers

它把每個 helper 都單獨放到一個文件夾里。這樣,配合 transform-runtime 使用的時候,需要用 helper 轉化的時候,就從 babel-runtime 中直接引用了。

var _asyncToGenerator2 = require('babel-runtime/helpers/asyncToGenerator');

var _asyncToGenerator3 = _interopRequireDefault(_asyncToGenerator2);

文件結構:

 

文件结构

 

使用

可以單獨引入require('babel-runtime/core-js/object/values');

不過這些模組都做了 esModule 的兼容處理,也就是上面引入的模組是{ "default": require("core-js/library/fn/array/filter"), __esModule: true }這樣的,要使用還得加上 .default。所以我們期待的是,最好能有幫我們自動處理的插件,babel-plugin-transform-runtime就是用來做這個的。這個我們放到 plugin 去講。

babel-preset-env

1、babel-preset-env 能根據當前的運行環境,自動確定你需要的 plugins 和 polyfills。通過各個 es標準 feature 在不同瀏覽器以及 node 版本的支援情況,再去維護一個 feature 跟 plugins 之間的映射關係,最終確定需要的 plugins。
2、單獨使用時,只能轉化部分語法箭頭函數,let,const之類的,不能轉化內置對象,實例方法等。如果進一步需要轉換內置對象、實例方法,那就得用polyfill。

{ "presets": [ ["env", { "targets": { "chrome": 52, "browsers": ["last 2 versions", "safari 7"] }, "modules": false, "useBuiltIns": "usage", "debug": false }] ] }
 
其中的useBuiltIns就是是否開啟自動支援 polyfill,它能自動給每個文件添加其需要的poly-fill。
重要的參數 "useBuiltIns",他是控制 @babel/preset-env 使用何種方式幫我們導入 polyfill 的核心

entry

這是一種入口導入方式, 只要我們在打包配置入口 或者 文件入口寫入 import "core-js" 這樣一串程式碼, babel 就會替我們根據當前你所配置的目標瀏覽器(browserslist)來引入所需要的polyfill 。

useage

這個就比較神奇了, useBuiltIns = useage 時,會參考目標瀏覽器(browserslist) 和 程式碼中所使用到的特性來按需加入 polyfill

當然, 使用 useBuiltIns = useage, 還需要填寫另一個參數 corejs 的版本號,

core-js 支援兩個版本, 2 或 3, 很多新特性已經不會加入到 2 裡面了, 比如: flat 等等最新的方法, 2 這個版本裡面都是沒有的, 所以建議大家用3

.babelrc

 
{
  "presets": [
    ["@babel/preset-env", 
      {
        "useBuiltIns": "usage",
        "corejs": 3
      }
    ]
  ]
}
這種方式打包體積不大,但是如果我們排除node_modules/目錄,遇上沒有經過轉譯的第三方包,就檢測不到第三方包內部的 『hello『.includes(『h『)這種句法,這時候我們就會遇到bug,
但是不排除的話又耗費性能。

false

剩下最後一個 useBuiltIns = false , 那就簡單了, 這也是默認值 , 使用這個值時不引入 polyfill。

options.targets

用來配置需要支援的的環境,不僅支援瀏覽器,還支援node。

如果沒有配置targets選項,就會讀取項目中的browserslist配置項。

options.loose

默認值是false,如果preset-env中包含的plugin支援loose的設置,那麼可以通過這個欄位來做統一的設置。

options.modules

"amd" | "umd" | "systemjs" | "commonjs" | "cjs" | "auto" | false,默認值是auto

用來轉換ES6的模組語法。如果使用false,將不會對文件的模組語法進行轉化。

如果要使用webpack中的一些新特性,比如tree shaking 和 sideEffects,就需要設置為false,對ES6的模組文件不做轉化,因為這些特性只對ES6的模組有效。

options.useBuiltIns

"usage" | "entry" | false,默認值是false

這個配置項主要是用來處理@babel/polyfill。

  • 設置為false時,會把@babel/polyfill這個包引進來,忽略targets配置項和項目里的browserslist配置
  • 設置為entry時,在整個項目里,只需要引入一次@babel/polyfill,它會根據targets和browserslist,然後只載入目標環境不支援的api文件
  • 設置為usage時,babel會在目標環境的基礎上,只載入項目里用的那些不支援的api的文件,做到按需載入

其他的一些配置項可以看 官方文檔

babel-runtime和plugin-transform-runtime區別

babel-runtime這種方式會藉助 helper function 來實現特性的兼容,
並且利用 @babel/plugin-transform-runtime 插件還能以沙箱墊片的方式防止污染全局, 並抽離公共的 helper function , 以節省程式碼的冗餘

也就是說 @babel/runtime 是一個核心, 一種實現方式, 而 @babel/plugin-transform-runtime 就是一個管家, 負責更好的重複使用 @babel/runtime

@babel/plugin-transform-runtime 插件也有一個 corejs 參數需要填寫

版本2 不支援內置對象 , 但自從Babel 7.4.0 之後,擁有了 @babel/runtime-corejs3 , 我們可以放心使用 corejs: 3 對實例方法做支援

當前的 .babelrc

 
{
  "presets": [
    ["@babel/preset-env"]
  ],
  "plugins": [
    [ 
      "@babel/plugin-transform-runtime", {
        "corejs": 3
      }
    ]
  ]
}

babel-preset-env和plugin-transform-runtime區別

使用 @babel/plugin-transform-runtime 編譯後的程式碼和之前的 @babel/preset-env 編譯結果不一樣,
它使用了幫助函數, 並且賦予了別名 , 抽出為公共方法, 實現復用。 比如它用了 _Promise 代替了 new Promise , 從而避免了創建全局對象

preset-env 和 plugin-transform-runtime同時使用

useage 和 @babel/runtime 同時使用的情況下比較智慧, 並沒有引入重複的 polyfill

個人分析原因應該是: babel 的 plugin 比 prset 要先執行, 所以preset-env 得到了 @babel/runtime 使用幫助函數包裝後的程式碼,而 useage 又是檢測程式碼使用哪些新特性來判斷的, 所以它拿到手的只是一堆 幫助函數, 自然沒有效果了

.babelrc

{
  "presets": [
    ["@babel/preset-env", 
      {
        "useBuiltIns": "usage",
        "corejs": 3
      }
    ]
  ],
  "plugins": [
    [ 
      "@babel/plugin-transform-runtime", {
        "corejs": 3
      }
    ]
  ]
}

entry 和 @babel/runtime

跟 useage 的情況不一樣, entry 模式下, 在經過 @babel/runtime 處理後不但有了各種幫助函數還引入了許多polyfill, 這就會導致打包體積無情的增大

個人分析: entry 模式下遭遇到入口的 import "core-js" 及就立即替換為當前目標瀏覽器下所需的所有 polyfill, 所以也就跟 @babel/runtime 互不衝突了, 導致了重複引入程式碼的問題, 所以這兩種方式千萬不要一起使用, 二選一即可
{
  "presets": [
    ["@babel/preset-env", 
      {
        "useBuiltIns": "entry"
      }
    ]
  ],
  "plugins": [
    [ 
      "@babel/plugin-transform-runtime", {
        "corejs": 3
      }
    ]
  ]
}

@babel/plugin-transform-runtime默認情況下安裝@babel/runtime這個庫,即corejs為false時使用;
當corejs設置為2時,需要安裝使用@babel/runtime-corejs2。
runtime和runtime-corejs2這兩個庫唯一的區別是,corejs2這個庫增加了對core-js這個庫的依賴,
而core-js是用來對ES6各個語法polyfill的庫,所以在corejs為false的情況下,只能做語法的轉換,並不能polyfill任何api。

總結

  1. @babel/preset-env 擁有根據 useBuiltIns 參數的多種polyfill實現,優點是覆蓋面比較全(entry), 缺點是會污染全局, 推薦在業務項目中使用

    • entry 的覆蓋面積全, 但是打包體積自然就大,
    • useage 可以按需引入 polyfill, 打包體積就小, 但如果打包忽略node_modules 時如果第三方包未轉譯則會出現兼容問題
  2. @babel/runtime 在 babel 7.4 之後大放異彩, 利用 corejs 3 也實現了各種內置對象的支援, 並且依靠 @babel/plugin-transform-runtime 的能力,沙箱墊片和程式碼復用, 避免幫助函數重複 inject 過多的問題, 該方式的優點是不會污染全局, 適合在類庫開發中使用

    上面 1, 2 兩種方式取其一即可, 同時使用沒有意義, 還可能造成重複的 polyfill 文件

對比以上三種方案,我們得出以下結論

方案打包後大小優點缺點
babel-polyfill 259K 完整模擬ES2015+環境 體積過大;污染全局對象和內置的對象原型
babel-runtime 63K 按需引入,打包體積小 不模擬實例方法
babel-preset-env(開啟useBuiltIns) 194K 按需引入,可配置性高 -

方案沒有絕對的優劣,在開發過程中還是要根據實際情況靈活運用。

 

幾個包的包含關係

   babel-polyfill僅僅是引用core-js、regenerator-runtime這兩個npm包。

   @babel/runtime包含兩個文件夾:helpers(定義了一些處理新的語法關鍵字的幫助函數)、regenerator(僅僅是引用regenerator-runtime這個npm包)。

   @babel/runtime-corejs2包含三個文件夾:core-js(引用core-js這個npm包)、helpers(定義了一些處理新的語法關鍵字的幫助函數)、regenerator(僅僅是引用regenerator-runtime這個npm包)。

   可以看出,@babel/runtime-corejs2≈@babel/runtime + babel-polyfill:

   @babel/runtime只能處理語法關鍵字,而@babel/runtime-corejs2還能處理新的全局變數(例如,Promise)、新的原生方法(例如,String.padStart );

   使用了@babel/runtime-corejs2,就無需再使用@babel/runtime了。

Tags: