很多初學者在剛接觸 babel 的時候,通常會看到這樣一個報錯信息:
ReferenceError: regeneratorRuntime is not defined
這個報錯表面上是由於 async function 語法被 babel 轉譯之后的代碼使用了 regeneratorRuntime 這個變量,但是這個變量在最終的代碼里未定義造成的報錯。
babel 在轉譯的時候,會將源代碼分成 syntax 和 api 兩部分來處理:
- syntax:類似於展開對象、optional chain、let、const 等語法
- api:類似於 [1,2,3].includes 等函數、方法
- 首先寫一個最簡單的 babel 配置文件:
{ "presets":[["@babel/preset-env"]] }
轉譯結果如下:
轉譯結果
上面說過,const 這種語法為 syntax,includes 這種方法為 api。可以看到,syntax 很輕松就轉好了,但是 api 並沒有做任何處理。babel 轉譯后的代碼如果在不支持 includes 這個方法的瀏覽器里運行,就會報錯。
2. babel 使用 polyfill 來處理 api。@babel/preset-env 中有一個配置選項 useBuiltIns,用來告訴 babel 如何處理 api。由於這個選項默認值為 false,即不處理 api,所以上面的代碼轉譯后沒有處理 includes 這個方法。
設置 useBuiltIns 的值為 "entry",同時在源代碼的最上方手動引入 @babel/polyfill 這個庫(該庫一共分為兩部分,第一部分是 core-js,第二部分是 regenerator-runtime。其中 core-js 為其他團隊開源的另一個獨立項目):
{ "presets": [ [ "@babel/preset-env", { "useBuiltIns": "entry", "debug": true } ] ] }
轉譯結果
可以看到,這種模式下,babel 會將所有的 polyfill 全部引入,這樣會導致結果的包大小非常大,而我們這里僅僅需要 includes 一個方法而已。
3. 正確的做法是使用按需加載,將 useBuiltIns 改為 "usage",babel 就可以按需加載 polyfill,並且不需要手動引入 @babel/polyfill:
{ "presets": [ [ "@babel/preset-env", { "useBuiltIns": "usage", "debug": true } ] ] }
轉譯結果
到這里,最開始的那個問題真正的原因就有了。babel 在轉譯 async function 的時候,生成的代碼里使用了 regeneratorRuntime 這個變量,而這個變量是放在 regenerator-runtime 這個 polyfill 庫中的,所以如果不修改 useBuiltIns 引入 polyfill,那么自然會報 undefined 錯誤,因為根本就沒有引入這個變量。
到目前為止,上面的 babel 配置還存在兩個問題
1. 從上面的轉譯結果可以看到,includes 這個 api 直接是 require 了一下,並不是另一種更符合直覺的方式:
var includes = require('xxx/includes')
所以 babel 的 polyfill 機制是,對於例如 Array.from 等靜態方法,直接在 global.Array 上添加;對於例如 includes 等實例方法,直接在 global.Array.prototype 上添加。這樣直接修改了全局變量的原型,有可能會帶來意想不到的問題。這個問題在開發第三方庫的時候尤其重要,因為我們開發的第三方庫修改了全局變量,有可能和另一個也修改了全局變量的第三方庫發生沖突,或者和使用我們的第三方庫的使用者發生沖突。公認的較好的編程范式中,也不鼓勵直接修改全局變量、全局變量原型。
2. babel 轉譯 syntax 時,有時候會使用一些輔助的函數來幫忙轉,比如:
class 語法中,babel 自定義了 _classCallCheck這個函數來輔助;typeof 則是直接重寫了一遍,自定義了 _typeof 這個函數來輔助。這些函數叫做 helpers。從上圖中可以看到,helper 直接在轉譯后的文件里被定義了一遍。如果一個項目中有100個文件,其中每個文件都寫了一個 class,那么這個項目最終打包的產物里就會存在100個 _classCallCheck 函數,他們的長相和功能一模一樣,這顯然不合理。
4. @babel/plugin-transform-runtime 這個插件的作用就是解決上面提到的兩個問題
先執行下面兩條命令安裝兩個庫:
yarn add @babel/plugin-transform-runtime -D yarn add @babel/runtime-corejs3
其中 @babel/plugin-transform-runtime 的作用是轉譯代碼,轉譯后的代碼中可能會引入 @babel/runtime-corejs3 里面的模塊。所以前者運行在編譯時,后者運行在運行時。類似 polyfill,后者需要被打包到最終產物里在瀏覽器中運行。
再修改配置:
{ "presets": [ [ "@babel/preset-env", { "useBuiltIns": "usage", "debug": true } ] ], "plugins": [ [ "@babel/plugin-transform-runtime", { "corejs": 3 // 指定 runtime-corejs 的版本,目前有 2 3 兩個版本 } ] ] }
從上圖可以看到,在引入了 transform-runtime 這個插件后:
- api 從之前的直接修改原型改為了從一個統一的模塊中引入,避免了對全局變量及其原型的污染,解決了第一個問題
- helpers 從之前的原地定義改為了從一個統一的模塊中引入,使得打包的結果中每個 helper 只會存在一個,解決了第二個問題
總結
babel 在轉譯的過程中,對 syntax 的處理可能會使用到 helper 函數,對 api 的處理會引入 polyfill。
默認情況下,babel 在每個需要使用 helper 的地方都會定義一個 helper,導致最終的產物里有大量重復的 helper;引入 polyfill 時會直接修改全局變量及其原型,造成原型污染。
@babel/plugin-transform-runtime 的作用是將 helper 和 polyfill 都改為從一個統一的地方引入,並且引入的對象和全局變量是完全隔離的,這樣解決了上面的兩個問題。
轉 https://zhuanlan.zhihu.com/p/147083132