我眼中的Babel 7

1,901 阅读9分钟

前言

看着Babel的官方文档学习实在是困难,大概还是因为那年英语太差,所幸有很多大大们对文档进行了翻译。另外看了很多前辈们的文章,受益匪浅。但是纸上得来终觉浅,眼睛总是在欺骗我。当我以为我看懂了,闭上眼睛自省一下:“我会了什么?”这个时候才发现一切都是我以为的。所以又重新整理了自己的思路,并写了下来,方便回顾。

Babel是什么?

Babel 是一个 JavaScript 编译器

Babel 是一个工具链,主要用于将 ECMAScript 2015+ 版本的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。

Babel 能干什么?

  • 语法转换
  • 通过 Polyfill 方式在目标环境中添加缺失的特性 (通过 @babel/polyfill 模块)
  • 源码转换 (codemods)

通俗地讲,Babel 只是转移新标准引入的语法,例如 ES6 中的箭头函数、解构等。而新标准中新增的方法、函数等就需要通过 Polyfill 在目标环境中添加缺失的特性(即新标准中新增的方法、函数等)来解决。

Babel编译的过程分为三个阶段:

  • 解析:将代码字符串解析成抽象语法树。
  • 转换:对抽象语法树进行转换操作。
  • 输出:根据变换后的抽象语法树再生成代码字符串。

本文主要分析的是三个阶段中的 转换,大部分的内容参考自 Babel官网

Babel虽然可以开箱即用,但是如果什么都不配置,那么它会将代码解析之后再输出同样的代码。所以我们需要通过配置插件(Plugins)和预设(Preset)来转换我们的代码。

准备

为了更清楚的了解 Babel 是如何 “起作用” 的,我们可以进行以下的准备工作。

  • 创建demo

    新建文件夹babel-test,进入该目录下。使用 npm init -y 初始化,新建src/index.js文件,并添加文件内容const fn = () => 1;

  • 安装必要的依赖

    @babel/core是 Babel 的核心,包含了所有的核心 API。
    @babel/cli是命令行工具,为我们提供 babel 命令来编译文件。

    # 当前文章安装的版本:
    # @babel/core: ^7.9.0
    # @babel/cli: ^7.8.4
    npm install --save-dev @babel/core @babel/cli
    
  • 添加编译命令

    package.json文件中的的script字段下添加一项:

    {
      ...
      "scripts": {
        "trans": "babel src --out-dir lib"
      }
      ...
    }
    

    该命令行的作用是将babel-test/src目录下的每个JavaScript文件转换并输出到babel-test/lib目录。

目前package.json文件内容如下图所示:

此时执行npm run trans我们会发现lib/index.js中输出的内容和src/index.js是一致的,这是因为我们没有告诉Babel应该执行什么样的转换,所以下面就需要我们设置插件和预设来解决这个问题。

插件

插件用于转换我们的代码,分为两种:语法插件和转换插件。

  • 语法插件

    只允许Babel解析(parse)特定类型的语法(而不是转换)。

  • 转换插件

    转换插件将启用相应的语法插件,因此我们不必同时指定这两个插件。

    当我们启用转换插件时,会自动启用相应的语法插件进行解析,然后通过转换插件进行转换。想要详细了解有哪些转换插件可以看这里:插件

  • 插件的使用

    如果插件是在项目根目录下通过 npm 安装的,那我们可以输入插件的名称,babel 会自动检查它是否已经被安装到node_modules目录下。

    要将src/index.js中的箭头函数转换成普通函数,我们可以借助官方提供的转换插件@babel/plugin-transform-arrow-functions

    npm isntall --save-dev @babel/plugin-transform-arrow-functions
    

    新建bebal-test/.babelrc文件并添加上面安装好的插件,文件内容如下:

    {
      "plugins": ["@babel/plugin-transform-arrow-functions"]
    }
    

    配置完成后,再次执行npm run trans命令,我们可以看到lib/index.js文件中已经是我们想要的内容了。

    当然,在实际开发中,如果我们一个一个这样去配置转换插件,那也麻烦了。所幸Babel为我们提供了预设Preset

预设

Preset 可以作为 Babel 插件的组合。

官方针对我们常用的环境编写了一些Preset

  • @babel/preset-env
  • @babel/preset-flow
  • @babel/preset-react
  • @babel/preset-typescript

创建 Preset

如需创建preset,导出一份配置即可。

module.exports = function() {
  return {
    plugins: [
      "pluginA",
      "pluginB",
      "pluginC",
    ]
  };
}

preset 可以包含其他的 preset,以及带有参数的插件。

module.exports = () => ({
  presets: [
    require("@babel/preset-env"),
  ],
  plugins: [
    [require("@babel/plugin-proposal-class-properties"), { loose: true }],
    require("@babel/plugin-proposal-object-rest-spread"),
  ],
});

@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!(官网)

意思是:@babel/preset-env是一个灵活的预设,你不需要管理目标环境需要的语法转换浏览器polyfills,就可以使用最新的 JavaScript,同时也会让 JavaScript 打包后的文件更小。

那么@babel/preset-env的作用是什么呢?

  • 将 JavaScript 引入的新语法转换成ES5的语法。
  • 加载浏览器polyfills。

需要注意的是,@babel/preset-env它不支持stage-x插件。

安装

# @babel/preset-env: ^7.9.0
npm install --save-dev @babel/preset-env

修改.babelrc文件内容:

{
  "presets": [
    "@babel/preset-env"
  ]
}

栗子🌰

修改src/index.js中的内容如下所示:

Array.from('foo'); // ['f', 'o', 'o']

Array.from([1, 2, 3], x => x + x); // [2, 4, 6]

let promsie = new Promise();

执行编译命令npm run translib/index.js中的内容如下所示:

"use strict";

Array.from('foo'); // ['f', 'o', 'o']

Array.from([1, 2, 3], function (x) {
  return x + x;
}); // [2, 4, 6]

let promsie = new Promise();

通过上面两段代码的对比,我们可以看到只有原先的箭头函数发生了转换。而Array.from方法和Promise构造函数并没有发生转换。

小结:

因为@babel/preset-env转换的是语法,不包含新增的全局变量、方法等,所以需要加载浏览器polyfills来完善代码转换。(后面介绍完Polyfill会对.babelrc文件进行完善)

Browserslist 集成

@babel/preset-env会拿到我们指定的目标环境,检查这些映射表来编译一系列的插件并传给 Babel。针对基于浏览器的或Electron-based的项目,官方推荐使用.browserslistrc文件来指定目标环境。若是我们没有设置targets或ignoreBrowserslistConfig配置项,@babel/preset-env默认会使用.browserslistrc中的配置。

比如,当我们只想包括大于0.25%市场份额的浏览器的那些polyfill和代码转换:

  • options(关于@babel/preset-env的更多options配置

    {
      "presets": [
        [
          "@babel/preset-env",
          {
            "targets": "> 0.25%, not dead"
          }
        ]
      ]
    }
    
  • browserslist

    > 0.25%
    not dead
    
  • package.json

    {
      "browserslist": "> 0.25%, not dead"
    }
    

Polyfill

polyfill 的中文意思是垫片,用来垫平不同目标环境下的差异,让新的内置函数、实例方法等在低版本浏览器中也可以使用。

Babel 7.4.0 版本开始,@babel/polyfill 已经被废弃不推荐使用,支持直接导入 core-js/stable(polyfill ECMAScript 特性)和 regenerator-runtime/runtime(需要使用转换后的 generator 函数)(官网)。

core-jsregenerator-runtime将模拟完整的ES2015 +环境(不包含第4阶段的提议),在 JavaScript 代码执行前引入。

This means you can use new built-ins like Promise or WeakMap, static methods like Array.from or Object.assign, instance methods like Array.prototype.includes, and generator functions (provided you use the regenerator plugin). The polyfill adds to the global scope as well as native prototypes like String in order to do this.

意思是我们可以使用: 新的内置函数 如 Promise 和 WeakMap; 新的静态方法 如 Array.from 和 Object.assign; 新的实例方法 如 Array.prototype.includes和generator函数(前提是使用了 @babel/plugin-transform-regenerator 插件)。 polyfill将添加到全局范围以及本机原型(如String)中,以便执行此操作。

安装core-jsregenerator-runtime

# core-js: ^3.6.4;提供 es 新的特性。
# regenerator: ^0.14.4;应用代码中用到generator、async函数的话,提供对 generator 支持。
npm install --save core-js regenerator

安装完成后修改src/index.js文件。

import "core-js/stable";
import "regenerator-runtime/runtime";

Array.from('foo'); // ['f', 'o', 'o']

Array.from([1, 2, 3], x => x + x); // [2, 4, 6]

let promsie = new Promise();

执行npm run trans命令后,查看lib/index.js中的内容。

"use strict";

require("core-js/stable");

require("regenerator-runtime/runtime");

Array.from('foo'); // ['f', 'o', 'o']

Array.from([1, 2, 3], function (x) {
  return x + x;
}); // [2, 4, 6]

var promsie = new Promise();

此时我们的代码在低版本浏览器中已经能够正常运行了。

通过webpack打包后,发现包的大小为127kb。这是因为我们引入了全部的Polyfill导致压缩后包的体积变大,所以我们更希望按需引入,所幸Babel已经为我们提供了解决方案(webpack的配置文件在文末会贴出来)。

@babel/preset-env 提供了一个 useBuiltIns 参数,设置值为 usage 时,就只会包含代码需要的 polyfill 。设置该参数时,必须要同时设置 corejs,前面已经安装了这里就不重复提了。(core-js@2已经不会再添加新特性,新特性都会添加到 core-js@3)

优化

修改配置文件.babelrc

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

修改src/index.js

Array.from('foo'); // ['f', 'o', 'o']

Array.from([1, 2, 3], x => x + x); // [2, 4, 6]

let promsie = new Promise();

async function fn() {
  return 1
}

执行npm run trans命令后,查看lib/index.js中的内容。

"use strict";

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

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

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

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

require("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); }); }; }

Array.from('foo'); // ['f', 'o', 'o']

Array.from([1, 2, 3], function (x) {
  return x + x;
}); // [2, 4, 6]

var promsie = new Promise();

function fn() {
  return _fn.apply(this, arguments);
}

function _fn() {
  _fn = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee() {
    return regeneratorRuntime.wrap(function _callee$(_context) {
      while (1) {
        switch (_context.prev = _context.next) {
          case 0:
            return _context.abrupt("return", 1);

          case 1:
          case "end":
            return _context.stop();
        }
      }
    }, _callee);
  }));
  return _fn.apply(this, arguments);
}

从输出结果可以看到,我们已经实现了按需引入Polyfill,再次打包后发现包的大小已经变成了30kb,效果很是显著,但是这种使用方式始终存在污染全局环境的问题。

为了解决这个问题,Babel给我们提供了@babel-runtime。它将开发者依赖的全局内置对象等,抽取成单独的模块,并通过模块导入的方式引入,避免了对全局环境的污染。

@babel/runtime

@babel/runtime是一个包含 Babel modular runtime helpers 和 regenerator-runtime 的库。

Polyfill的区别:

  • Polyfill 会修改(覆盖)新增的内置函数、静态方法和实例方法。

  • @babel/runtime 不会,它只是引入一些 helper 函数,创造对应的方法。

安装@babel-runtime是代码运行时需要的依赖,所以需要作为生产依赖安装)

npm install --save @babel/runtime

有时Babel可能会在输出中注入一些跨文件的相同代码,因此可能会被重用。

栗子🌰

修改src/index.js中的内容:

class Parent  {}

执行编译命令npm run translib/index.js中的内容:

"use strict";

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

var Parent = function Parent() {
  _classCallCheck(this, Parent);
};

这意味着每个包含类的文件都将引入_classCallCheck,重复的代码注入必然导致包的变大。这个时候就需要使用插件@babel/plugin-transform-runtime

@babel/plugin-transform-runtime 用于构建过程的代码转换,而 @babel/runtime 是提供帮助方法的模块,这样就可以避免重复的代码注入。

@babel/plugin-transform-runtime

@babel/plugin-transform-runtime 是一个可以重复使用 Babel 注入的帮助程序,以节省代码大小的插件。

Babel使用很小的帮助器来完成例如Class的功能。默认情况下,它将被添加到需要它的每个文件中。有时不需要重复,特别是当我们的应用程序分布在多个文件中时。

这是@babel/plugin-transform-runtime插件的来源:所有帮助程序都将引用该模块,@babel/runtime以避免在编译后的输出中出现重复。运行时将被编译到我们的构建中。

该转换器的另一个目的是为我们的代码创建一个沙盒环境。如果直接引入core-js@babel/polyfill ,它提供了诸如内置插件Promise,Set和Map等,这些会污染全局环境。尽管这对于应用程序或命令行工具可能是可以的,但是如果我们的代码是要发布供他人使用的库,或者我们无法完全控制代码运行的环境,则将成为一个问题。

@babel/plugin-transform-runtime会将这些内置别名作为core-js 的别名,因此我们可以无缝使用它们,而无需使用polyfill。(官网)

安装

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

避免重复注入

修改.babelrc文件内容:

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

执行编译命令npm run trans

"use strict";

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

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

var Parent = function Parent() {
  (0, _classCallCheck2["default"])(this, Parent);
};

lib/index.js中输出的内容可以看出classCallCheck不是直接注入到代码中,而是从 @babel/runtime 中引入,这就避免了相同代码的重复注入。

避免全局污染

通过添加配置避免全局环境被污染。

安装@babel/runtime-corejs3

npm install --save @babel/runtime-corejs3

修改src/index.js

Array.from('foo'); // ['f', 'o', 'o']

Array.from([1, 2, 3], x => x + x); // [2, 4, 6]

let promsie = new Promise();

async function fn() {
  return 1
}

修改.babelrc

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

执行编译命令npm run translib/index.js中的内容:

"use strict";

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

var _regenerator = _interopRequireDefault(require("@babel/runtime-corejs3/regenerator"));

var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/asyncToGenerator"));

var _promise = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/promise"));

var _from = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/array/from"));

(0, _from["default"])('foo'); // ['f', 'o', 'o']

(0, _from["default"])([1, 2, 3], function (x) {
  return x + x;
}); // [2, 4, 6]

var promsie = new _promise["default"]();

function fn() {
  return _fn.apply(this, arguments);
}

function _fn() {
  _fn = (0, _asyncToGenerator2["default"])( /*#__PURE__*/_regenerator["default"].mark(function _callee() {
    return _regenerator["default"].wrap(function _callee$(_context) {
      while (1) {
        switch (_context.prev = _context.next) {
          case 0:
            return _context.abrupt("return", 1);

          case 1:
          case "end":
            return _context.stop();
        }
      }
    }, _callee);
  }));
  return _fn.apply(this, arguments);
}

从输出文件中可以看到,@babel/plugin-transform-runtime通过模块导入的方式引入所需的功能代码,避免了对全局环境的污染。

补充

Plugin和Preset的执行顺序

  • Plugin 在 Presets 前运行。
  • Plugin 的执行顺序是从前往后。
  • Preset 的执行顺序是从后往前。

webpack配置文件

  • 安装必要的依赖
# 本文使用的webpack相关的版本
# webpack-cli@3.3.11
# webpack@4.42.0
# clean-webpack-plugin@3.0.0
# babel-loader@8.1.0
npm install --save-dev webpack-cli webpack babel-loader clean-webpack-plugin
  • 新建babel-test/webpack.config.js文件
const path = require('path')
const webpack = require('webpack')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')

module.exports = {
  mode: 'production',
  entry: './lib/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].[hash].js'
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        loader: 'babel-loader'
      }
    ]
  },
  plugins: [
    new CleanWebpackPlugin()
  ]
}

  • 添加命令

    package.json文件中的的script字段下添加一项:

    {
      ...
      "scripts": {
        "build": "webpack --mode=production"
      }
      ...
    }
    
  • 执行命令

    npm run build
    

总结

纸上得来终觉浅,绝知此事要躬行。



参考链接
* Babel官网
* 不可错过的 Babel7 知识
* Babel 社区概览
* ...