Show me the code,babel 7 最佳實踐!

前言

本文首發於 github 博客
如對你有幫助是個人榮幸,你的 star 是對我最大的支持!javascript

你們都知道 babel 是兼容對 ES6 支持不完善的低版本瀏覽器的轉換編譯器。vue

而 babel 其實主要作的只有兩件事情:java

  • 語法轉換
  • 新 API 的 polyfill 兼容

那麼廢話少說,咱們直接點,直接說說常見幾個場景下兼容舊版瀏覽器的方案。react

跳過直接看結論webpack

實踐方案

polyfill.io

若是你的工程是用的語法是 ES5,可是用了一些 ES6+ 的API特性,那麼能夠直接引入:git

<script src="https://cdn.polyfill.io/v2/polyfill.min.js"></script>
複製代碼

來兼容 Web 應用不支持的 API。es6

原理大概是 polyfill.io 會讀取每一個請求的User-Agent標頭,並返回適合請求瀏覽器的polyfill。具體的還能夠本身指定加載哪些 特性的 polyfill,具體想了解更多的你們能夠看看 官方文檔github

優勢:每一個瀏覽器的設備加載的 polyfill 都不同,最新的徹底兼容ES6棟瀏覽器基本加載的 polyfill 大小爲0。web

缺點:vue-cli

  1. 必須先進行語法轉換,用了 async 語法在新瀏覽器上能夠運行,可是在舊版瀏覽器就直接拋出錯誤了。
  2. 不能按照代碼所用到的新特性按需進行 polyfill,也就是說即使你的 Web 應用只用到了 es6.array.from 特性,polyfill.io 依然可能會把該瀏覽器全部不支持的特性(如:es6.promise,es6.string.includes等特性)所有加載進來。

@babel/preset-env 按需加載

上面提到了 polyfill.io 的一個缺點是沒法按需引入,那麼如今就介紹下 babel7 @babel/preset-env

@babel/preset-env 默認根據 .browserslist 所填寫的須要兼容的瀏覽器,進行必要的代碼語法轉換和 polyfill

// .babelrc.js
module.exports = {
    presets: [
        [
            "@babel/preset-env",
            
            {
                "modules": false, // 模塊使用 es modules ,不使用 commonJS 規範,具體看文末附錄
                "useBuiltIns": 'usage', // 默認 false, 可選 entry , usage
            }
        ]
    ]
}
複製代碼

此處重點介紹一下其新推出的 useBuiltIns 選項:

  1. false : 不啓用polyfill, 若是在業務入口 import '@babel/polyfill', 會無視 .browserslist 將全部的 polyfill 加載進來。
    image

    polyfill 所有加載進來有 284 個特性包
  2. entry : 啓用,須要手動 import '@babel/polyfill' 才生效(不然會拋出錯誤:regeneratorRuntime undefined), 根據 .browserslist 過濾出 須要的 polyfill (相似 polyfill.io 方案)
    image

    使用entry根據browserslist(ie>10)加載進來的有 238 個特性包
  3. usage : 不須要手動import '@babel/polyfill'(加上也無妨,編譯時會自動去掉), 且會根據 .browserslist + 業務代碼使用到的新 API 按需進行 polyfill
    image

    使用usage根據browserslist(ie>10)+代碼用到的,加載進來的只有 51 個特性包

    usage 風險項:因爲咱們一般會使用不少 npm 的 dependencies 包來進行業務開發,babel 默認是不會檢測 依賴包的代碼的。

    也就是說,若是某個 依賴包使用了 Array.from, 可是本身的業務代碼沒有使用到該API,構建出來的 polyfill 也不會有 Array.from, 如此一來,可能會在某些使用低版本瀏覽器的用戶出現 BUG。

    因此避免這種狀況發生,通常開源的第三方庫發佈上線的時候都是轉換成 ES5 的。


上面提到的 useBuiltIns:'usage' 彷佛已經很完美解決咱們的須要了,可是咱們構建的時候發現:

// es6+ 源碼:
const asyncFun = async ()=>{
  await new Promise(setTimeout, 2000)
  
  return '2s 延時後返回字符串'
}
export default asyncFun
複製代碼

根據上述的 useBuiltIns:'usage' 配置編譯後:

import "core-js/modules/es6.promise";
import "regenerator-runtime/runtime";

function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } }

function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; }

var asyncFun =
/*#__PURE__*/
function () {
  var _ref = _asyncToGenerator(
  /*#__PURE__*/
  regeneratorRuntime.mark(function _callee() {
    return regeneratorRuntime.wrap(function _callee$(_context) {
      while (1) {
        switch (_context.prev = _context.next) {
          case 0:
            _context.next = 2;
            return new Promise(setTimeout, 2000);

          case 2:
            return _context.abrupt("return", '2s 延時後返回字符串');

          case 3:
          case "end":
            return _context.stop();
        }
      }
    }, _callee, this);
  }));

  return function asyncFun() {
    return _ref.apply(this, arguments);
  };
}();

export default asyncFun;

複製代碼

上述代碼中,咱們看到,asyncGeneratorStep, _asyncToGenerator 這兩個函數是被內聯進來,而不是 import 進來的。

也就是說,若是你有多個文件都用到了 async,那麼每一個文件都會內聯一遍 asyncGeneratorStep, _asyncToGenerator 函數

這代碼明顯是重複了,那麼有什麼方法能夠進行優化呢? 答案是 @babel/plugin-transform-runtime

@babel/plugin-transform-runtime

babel 在每一個須要的文件的頂部都會插入一些 helpers 代碼,這可能會致使多個文件都會有重複的 helpers 代碼。 @babel/plugin-transform-runtime 的 helpers 選項就能夠把這些模塊抽離出來

// .babelrc.js
module.exports = {
    "plugins": [
        [
            "@babel/plugin-transform-runtime",
            {
                "corejs": false, // 默認值,能夠不寫
                "helpers": true, // 默認,能夠不寫
                "regenerator": false, // 經過 preset-env 已經使用了全局的 regeneratorRuntime, 再也不須要 transform-runtime 提供的 不污染全局的 regeneratorRuntime
                "useESModules": true, // 使用 es modules helpers, 減小 commonJS 語法代碼
            }
        ]
    ],
    presets: [
        [
            "@babel/preset-env",
            
            {
                "modules": false, // 模塊使用 es modules ,不使用 commonJS 規範 
                "useBuiltIns": 'usage', // 默認 false, 可選 entry , usage
            }
        ]
    ]
}
複製代碼
// 添加新配置後編譯出來的代碼
import "core-js/modules/es6.promise";
import "regenerator-runtime/runtime";
import _asyncToGenerator from "@babel/runtime/helpers/esm/asyncToGenerator";

var asyncFun =
/*#__PURE__*/
function () {
  var _ref = _asyncToGenerator(
  /*#__PURE__*/
  regeneratorRuntime.mark(function _callee() {
    return regeneratorRuntime.wrap(function _callee$(_context) {
      while (1) {
        switch (_context.prev = _context.next) {
          case 0:
            _context.next = 2;
            return new Promise(setTimeout, 2000);

          case 2:
            return _context.abrupt("return", '2s 延時後返回字符串');

          case 3:
          case "end":
            return _context.stop();
        }
      }
    }, _callee, this);
  }));

  return function asyncFun() {
    return _ref.apply(this, arguments);
  };
}();

export default asyncFun;
複製代碼

能夠看到,已經沒有了內聯的 helpers 代碼,大功告成。

總結

若是沒有什麼特殊的需求,使用 babel 7 的最佳配置是:

  1. 首先安裝依賴包: npm i -S @babel/polyfill @babel/runtime && npm i -D @babel/preset-env @babel/plugin-transform-runtime

  2. 配置 .babelrc.js

// .babelrc.js
module.exports = {
    "plugins": [
        [
            "@babel/plugin-transform-runtime",
            {
                "corejs": false, // 默認值,能夠不寫
                "helpers": true, // 默認,能夠不寫
                "regenerator": false, // 經過 preset-env 已經使用了全局的 regeneratorRuntime, 再也不須要 transform-runtime 提供的 不污染全局的 regeneratorRuntime
                "useESModules": true, // 使用 es modules helpers, 減小 commonJS 語法代碼
            }
        ]
    ],
    presets: [
        [
            "@babel/preset-env",
            {
                "modules": false, // 模塊使用 es modules ,不使用 commonJS 規範 
                "useBuiltIns": 'usage', // 默認 false, 可選 entry , usage
            }
        ]
    ]
}
複製代碼

PS: 若是想要瞭解更多有關 @babel/preset-env 和 @babel/plugin-transform-runtime 的選項配置用途,能夠參考個人我的總結

思考與探索(Modern Build)

上述的方案,其實還一直隱藏着一個不算問題的問題,那就是若是使用最新的瀏覽器,其實不須要任何的語法轉換和polyfill。

那麼參考下上述的 polyfill 方案,能不能實現若是低版本瀏覽器,就使用usage方案按需 transform + polyfill 的代碼,若是是較新瀏覽器,就不進行任何的語法轉換和 polyfill 呢?

必須能!

參考這篇文章 deploying es2015 code in production today,其中提出了基於 script 標籤的 type="module"nomodule 屬性 區分出當前瀏覽器對 ES6 的支持程度。

具體原理體如今,對於如下代碼:

<script type="module" src="main.js"></script>
<script nomodule src="main.legacy.js"></script>
複製代碼

支持 ES Module 的瀏覽器可以識別 type="module"nomodule,會加載 main.js 忽略 main.legacy.js

還未支持 ES module 的瀏覽器則偏偏相反,只會加載main.legacy.js

那麼怎麼實現優化就很清晰了:

  1. 經過配置上述 babel 最佳實踐的,給這類的代碼文件的 script 標間加上 nomodule 屬性
  2. 經過配置 @babel/preset-env 的選項 target.esmodules = true,不轉換全部的語法也不添加 polyfill,生成 ES6+ 的能被現代瀏覽器識別解析的代碼,並給這類代碼文件的 script 標籤加上 type="module"

vue-cli 3.0 官方提供 modern build 功能

create-react-app 預計在下一個版本3.0的迭代中才實現。 現階段實現須要本身寫 webpack 插件來實現 module/nomodule 插入

相關文章
相關標籤/搜索