天天看點

99% 開發者沒弄明白的 babel 知識

來源 | Alibaba F2E
99% 開發者沒弄明白的 babel 知識

困擾

作為工具開發者,babel 關聯問題是難繞過去的砍。

在 babel@6 時候,最常收到回報之一就是

regeneratorRuntime is not defined

而到了 babel@7,最常收到回報之一

Cannot find module 'core-js/library/fn/**'.

那是什麼問題導緻這些問題的出現呢,我覺得有一個 issue 特别能代表這一類的開發者。大家不要笑,我們内部一些基礎子產品也有這個問題

https://github.com/raisezhang/react-drag-listview/issues/44#issuecomment-751662527

總結來講:Babel 在編譯大家的代碼時候,會依據大家配置的 preset or plugin 注入一些子產品依賴,而這些子產品依賴是大家需要在

pkg.dependencies

裡面展現出來的,否則很可能出現的問題就是加載不到具體的檔案或者加載錯誤的版本的檔案。

根本的原因是什麼:其實大家對

  • @babel/preset-env

  • @babel/plugin-transform-runtime

  • @babel/runtime

  • core-js

  • @babel/polyfills

  • babel-polyfills

等等這些熟悉但又陌生的原因。

那今天我想大概和大家分享一下使用

babel@7

的心得,如有不對,歡迎大家及時指出。

預備知識

開講之前我們有必要先來看看各個包到底是幹啥的。

@babel/preset-env

babel@7

推出之際,babel 官方把 babel preset stage 以及 es2015 es2016 等等都廢棄了,取而代之的是

@babel/preset-env

@babel/preset-env is a smart preset that allows you to use the latest JavaScript without needing to micromanage which syntax transforms (and optionally, browser polyfills) are needed by your target environment(s). This both makes your life easier and JavaScript bundles smaller!

官方文檔:

https://babeljs.io/docs/en/babel-preset-env.html

通過官方文檔的描述,preset-env 主要做的是轉換 JavaScript 最新的 Syntax(指的是

const

let

...

等), 而作為可選項 preset-env 也可以轉換 JavaScript 最新的 API (指的是比如 數組最新的方法 filter 、includes,Promise 等等)。

這裡細心的同學估計發現了我刻意在強調 Syntax 和 API, 是的,babel 在實作編譯 或者我們在組合使用各個 preset 或者 plugin 時,其實有隐含這一層的關系的,同時這裡也有一些曆史背景,為了不給大家增加負擔,我們隻需要點到為止就好,需要了解的自行深挖也行。

說到這,我需要給大家一些 tips:

  • 在 babel@6 年代,我們使用的是 stage,那 stage 其實隻會翻譯 Syntax,而 API 則交給 babel-plugin-transform-runtime 或者 babel-polyfill 來實作。(這也是為什麼大家在老項目中可以看到有引入 babel-polyfill 的原因)
  • 在 babel@7 年代,我們廢棄了 stage,使用的 preset-env,同時他也可以提供 polyfill 的能力

綜上我想小夥伴們會有幾個困惑

  • preset-env 如何減小包體積的
  • 有 preset-env polyfill 能力了,為啥還要有 @babel/plugin-transform-runtime,這貨是必須的嗎?
  • 有了 preset-env polyfill 能力了,我還要 @babel/polyfill 嗎

要解釋清楚這幾個問題,首先需要大概知道 preset-env 的三個關鍵參數

targets(

https://babeljs.io/docs/en/babel-preset-env.html#targets)

:

Describes the environments you support/target for your project.

簡單講,該參數決定了我們項目需要适配到的環境,比如可以申明适配到的浏覽器版本,這樣 babel 會根據浏覽器的支援情況自動引入所需要的 polyfill。

useBuiltIns(

https://babeljs.io/docs/en/babel-preset-env.html#usebuiltins)

"usage" | "entry" | false, defaults to false

This option configures how @babel/preset-env handles polyfills.

這個參數決定了 preset-env 如何處理 polyfills。

false

: 這種方式下,不會引入 polyfills,你需要人為在入口檔案處

@import '@babel/polyfill';

但如上這種方式在

@[email protected]

之後被廢棄了,取而代之的是在入口檔案處自行 import 如下代碼

import 'core-js/stable';
import 'regenerator-runtime/runtime';
// your code           

不推薦采用 false,這樣會把所有的 polyfills 全部打入,造成包體積龐大

usage:

我們在項目的入口檔案處不需要 import 對應的 polyfills 相關庫。babel 會根據使用者代碼的使用情況,并根據 targets 自行注入相關 polyfills。

entry:

我們在項目的入口檔案處 import 對應的 polyfills 相關庫,例如

import 'core-js/stable';
import 'regenerator-runtime/runtime';
// your code           

此時 babel 會根據目前 targets 描述,把需要的所有的 polyfills 全部引入到你的入口檔案(注意是全部,不管你是否有用到進階的 API)

corejs:

String or { version: string, proposals: boolean }, defaults to "2.0". https://github.com/zloirock/core-js

注意 corejs 并不是特殊概念,而是浏覽器的 polyfill 都由它來管了。

舉個例子:

const one = Symbol('one');           

Babel>

"use strict";

require("core-js/modules/es.symbol.js");

require("core-js/modules/es.symbol.description.js");

require("core-js/modules/es.object.to-string.js");

var one = Symbol('one');           

這裡或許有人可能不太清楚,2 和 3 有啥差別,可以看看官方的文檔 core-js@3, babel and a look into the future(

https://github.com/zloirock/core-js/blob/master/docs/2019-03-19-core-js-3-babel-and-a-look-into-the-future.md)

簡單講 corejs-2 不會維護了,所有浏覽器新 feature 的 polyfill 都會維護在 corejs-3 上。

總結下:用 corejs-3,開啟

proposals: true

,proposals 為真那樣我們就可以使用 proposals 階段的 API 了。

小結

使用 preset-env 注入的 polyfill 是會污染全局的,但是如果是自己的應用其實是在可控的。

是以這裡推薦業務項目這麼使用

.babelrc

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": {
          "chrome": "58" // 按自己需要填寫
        },
        "useBuiltIns": "entry",
        "corejs": {
          "version": 3,
          "proposals": true
        }
      }
    ]
  ],
  "plugins": []
}           
import 'core-js/stable';
import 'regenerator-runtime/runtime';
// 入口檔案代碼           

這樣配置的原因是:

targets

下設定我們業務項目所需要支援的最低環境配置,

useBuiltIns

設定為

entry

,将最低環境不支援的所有

polyfill

都引入到入口檔案(即使你在你的業務代碼中并未使用)。這是一種兼顧最終打包體積和穩妥的方式,為什麼說穩妥呢,因為我們很難保證引用的三方包有處理好

polyfill

這些問題。當然如果你能充分保證你的三方依賴

polyfill

處理得當,那麼也可以把

useBuiltIns

usage

針對大衆普通項目,可能如上方式的配置(撇開個性化)應該夠用了,

但追求極緻的同學會有兩個問題:

問題一:還是會有一定程度的代碼重複,舉個例子:

import a from 'a';

export default a;           
"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.default = void 0;

var _a = _interopRequireDefault(require("a"));

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

var _default = _a.default;
exports.default = _default;           

_interopRequireDefault

這個方法,明顯是可以變成一個獨立子產品,這樣打包體積會變更小(再少也是愛)。

問題二:針對項目,polyfill 會污染全局可以接受,但是作為 Library 我更希望它不會污染全局環境

兩個都是好問題,那麼接下來就是

@babel/plugin-transform-runtime

的出場機會了。

@babel/plugin-transform-runtime

@babel/plugin-transform-runtime

(

https://babeljs.io/docs/en/babel-plugin-transform-runtime#corejs

)

官方描述是這樣的

A plugin that enables the re-use of Babel's injected helper code to save on codesize.

很明顯該插件的出現就是複用 babel 注入的關聯代碼。

具體

@babel/plugin-transform-runtime

做了什麼,官方也有明确的解釋,相信大家都能看明白:

The transform-runtime transformer plugin does three things:

Automatically requires @babel/runtime/regenerator when you use generators/async functions (toggleable with the regenerator option).

Can use core-js for helpers if necessary instead of assuming it will be polyfilled by the user (toggleable with the corejs option)

Automatically removes the inline Babel helpers and uses the module @babel/runtime/helpers instead (toggleable with the helpers option).

舉個例子:

import a from 'a';

export default a;           
"use strict";

var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.default = void 0;

var _a = _interopRequireDefault(require("a"));

var _default = _a.default;
exports.default = _default;           

這是不是解決了上面提到的問題一。

至于問題二,關于

polyfill

全局污染,不打算展開,因為涉及源碼講解,大家隻需要知道 通過

@babel/plugin-transform-runtime

插件實作的

polyfill

是不會影響全局的,是以更适合

Library

作者使用,但是需要注意的是,一旦采用

@babel/plugin-transform-runtime

,

@babel/preset-env

中的

targets

将會失效,言下之意,你的包會變大。

另外也肯定會有好奇寶寶

@babel/plugin-transform-runtime

開啟

corejs

并且

@babel/preset-env

也開啟

useBuiltIns

會咋樣,結論是:

polyfill

将會采用不污染全局的,且

targets

設定将會失效。

重要的問題講三遍

99% 開發者沒弄明白的 babel 知識

根據如上 option,@babel/runtime 要做為項目的 dependencies

如果是業務項目開發者:

@babel/plugin-transform-runtime

,建議關閉

corejs

polyfill

的引入由

@babel/preset-env

完成,即開啟

useBuiltIns

(如需其他配置,自行根據訴求配置)。

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "useBuiltIns": "entry",
        "corejs": {
          "version": 3,
          "proposals": true
        }
      }
    ]
  ],
  "plugins": [
    [
      "@babel/plugin-transform-runtime",
      {
        "corejs": false
      }
    ]
  ]
}           

并在入口檔案處 import 如下内容

import 'core-js/stable';
import 'regenerator-runtime/runtime';
// 入口檔案代碼           

如果是 Library 開發者:

@babel/plugin-transform-runtime

,建議開啟

corejs

polyfill

@babel/plugin-transform-runtime

引入。

@babel/preset-env

關閉

useBuiltIns

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

但心細的同學,肯定又發現了新的問題。

新的問題

為什麼

@babel/preset-env

不能使用不污染全局的

polyfill

;為什麼要使用不污染全局的

polyfill

就必須要使用

@babel/plugin-transform-runtime

,而與此同時我必須妥協掉

targets

帶來的體積優勢。

如何解決呢?

抱歉在現有的

babel

正式體系下還沒好辦法來解決這個問題,當然

babel

也意識到了這個問題,于是有了

babel-polyfills

注意是

babel-polyfills

不是

@babel/polyfills

,我們移到文章最後。

@babel/runtime

不需要深入研究它,請結合

@babel/plugin-transform-runtime

來看。

core-js

corejs(

@babel/preset-env

@babel/polyfills

已經在

[email protected]

廢棄,請結合

@babel/preset-env

babel-polyfills

babel-polyfills(

https://github.com/babel/babel-polyfills

這個庫的動機就是我們在

@babel/plugin-transform-runtime

小節下最後提出的問題:

Motivation
  • It wasn't possible to use @babel/preset-env's targets option with "pure" ponyfills, because @babel/plugin-transform-runtime is a completely separate package.
  • We forced our users to use core-js if they wanted a Babel integration. core-js is a good and comprehensive polyfill, but it doesn't fit the needs of all of our users.

但是目前這個庫處于

experimental

即試驗性的階段,按我對 babel 的了解,并不推薦大家目前在生産中引入,我們可以開放的心态保持關注即可。

至于想要嘗鮮的,官方也給了 更新方式(

https://github.com/babel/babel-polyfills/blob/main/docs/migration.md

)。

ps: 自己嘗鮮自己負責(我是求生欲極強的作者 o.o)

總結

這篇文章沒有 TLDR; 不管你是工具開發者、Library 開發者還是業務開發者,多一點耐心,好好把這篇文章提及的内容梳理一下,因為就作者觀察 99% 的開發者都還沒有弄明白。

99% 開發者沒弄明白的 babel 知識

繼續閱讀