—-> 深度好文

https://mp.weixin.qq.com/s/plJewhUd0xDXh3Ce4CGpHg

2、安装依赖包

  1. npm install --save-dev @babel/core @babel/cli @babel/preset-env复制代码

后面会介绍这些包的作用,先看用法

增加 babel 命令来编译 src 目录下的文件到 dist 目录:

  1. {
  2. "name": "demo",
  3. "version": "1.0.0",
  4. "description": "",
  5. "main": "src/index.js",
  6. "scripts": {
  7. "babel": "babel src --out-dir dist",
  8. "test": "echo \"Error: no test specified\" && exit 1"
  9. },
  10. "keywords": [],
  11. "author": "",
  12. "license": "ISC",
  13. "devDependencies": {
  14. "@babel/cli": "^7.8.4",
  15. "@babel/core": "^7.9.0",
  16. "@babel/preset-env": "^7.9.0"
  17. }
  18. }复制代码

3、增加 Babel 配置文件

在工程的根目录添加 babel.config.js 文件,增加 Babel 编译的配置,没有配置是不进行编译的。

  1. const presets = [
  2. [
  3. '@babel/env',
  4. {
  5. debug: true
  6. }
  7. ]
  8. ]
  9. const plugins = []
  10. module.exports = { presets, plugins }复制代码

上例中 debug 配置是为了打印出 Babel 工作时的日志,可以方便的看来,Babel 转化了哪些语法。

  1. presets 主要是配置用来编译的预置,plugins 主要是配置完成编译的插件,具体的含义后面会讲

  2. 推荐用 Javascript 文件来写配置文件,而不是 JSON 文件,这样可以根据环境来动态配置需要使用的 presets 和 plugins

  1. const presets = [
  2. [
  3. '@babel/env',
  4. {
  5. debug: true
  6. }
  7. ]
  8. ]
  9. const plugins = []
  10. if (process.env["ENV"] === "prod") {
  11. plugins.push(...)
  12. }
  13. module.exports = { presets, plugins }复制代码

4、编译的结果

配置好后,我们运行 npm run babel 命令,可以看到 dist 文件夹下生成了 index.js 文件,内容如下所示:

  1. // src/index.js
  2. const add = (a, b) => a + b
  3. // dist/index.js
  4. "use strict";
  5. var add = function add(a, b) {
  6. return a + b;
  7. };复制代码

可以看到,ES6 的 const 被转化为 var ,箭头函数被转化为普通函数。同时打印出来如下日志:

  1. > babel src --out-dir dist
  2. @babel/preset-env: `DEBUG` option
  3. Using targets:
  4. {}
  5. Using modules transform: auto
  6. Using plugins:
  7. proposal-nullish-coalescing-operator {}
  8. proposal-optional-chaining {}
  9. proposal-json-strings {}
  10. proposal-optional-catch-binding {}
  11. transform-parameters {}
  12. proposal-async-generator-functions {}
  13. proposal-object-rest-spread {}
  14. transform-dotall-regex {}
  15. proposal-unicode-property-regex {}
  16. transform-named-capturing-groups-regex {}
  17. transform-async-to-generator {}
  18. transform-exponentiation-operator {}
  19. transform-template-literals {}
  20. transform-literals {}
  21. transform-function-name {}
  22. transform-arrow-functions {}
  23. transform-block-scoped-functions {}
  24. transform-classes {}
  25. transform-object-super {}
  26. transform-shorthand-properties {}
  27. transform-duplicate-keys {}
  28. transform-computed-properties {}
  29. transform-for-of {}
  30. transform-sticky-regex {}
  31. transform-unicode-regex {}
  32. transform-spread {}
  33. transform-destructuring {}
  34. transform-block-scoping {}
  35. transform-typeof-symbol {}
  36. transform-new-target {}
  37. transform-regenerator {}
  38. transform-member-expression-literals {}
  39. transform-property-literals {}
  40. transform-reserved-words {}
  41. transform-modules-commonjs {}
  42. proposal-dynamic-import {}
  43. Using polyfills: No polyfills were added, since the `useBuiltIns` option was not set.
  44. Successfully compiled 1 file with Babel.复制代码

四、Babel 工作原理

在了解了如何使用后,我们一起来探寻一下编译背后的事情,同时会熟悉 Babel 的组成和进阶用法。

1、Babel 工作流程

前面提到 Babel 其实就是一个纯粹的 JavaScript 的编译器,任何一个编译器工作流程大致都可以分为如下三步:

  • Parser 解析源文件
  • Transfrom 转换
  • Generator 生成新文件

Babel 也不例外,如下图所示:babel 7 - 图1babel 7 - 图2

因为 Babel 使用是 acorn 这个引擎来做解析,这个库会先将源码转化为抽象语法树 (AST),再对 AST 作转换,最后将转化后的 AST 输出,便得到了被 Babel 编译后的文件。

那 Babel 是如何知道该怎么转化的呢?答案是通过插件,Babel 为每一个新的语法提供了一个插件,在 Babel 的配置中配置了哪些插件,就会把插件对应的语法给转化掉。插件被命名为 @babel/plugin-xxx 的格式。

2、Babel 组成

babel 7 - 图3

(1)@babel/preset-env

上面提到过 @babel/preset-* 其实是转换插件的集合,最常用的就是 @babel/preset-env,它包含了 大部分 ES6 的语法,具体包括哪些插件,可以在 Babel 的日志中看到。如果源码中使用了不在 @babel/preset-env 中的语法,会报错,手动在 plugins 中增加即可。

例如 ES6 明确规定,Class 内部只有静态方法,没有静态属性。但现在有一个提案提供了类的静态属性,写法是在实例属性的前面,加上 static 关键字。

  1. // src/index.js
  2. const add = (a, b) => a + b
  3. class Person {
  4. static a = 'a';
  5. static b;
  6. name = 'morrain';
  7. age = 18
  8. }复制代码

image.gifimage.gif编译时就会报如下错误:
babel 7 - 图6babel 7 - 图7

根据报错的提示,添加 @babel/plugin-proposal-class-properties 即可。

  1. npm install --save-dev @babel/plugin-proposal-class-properties复制代码
  1. // babel.config.js
  2. const presets = [
  3. [
  4. '@babel/env',
  5. {
  6. debug: true
  7. }
  8. ]
  9. ]
  10. const plugins = ['@babel/plugin-proposal-class-properties']
  11. module.exports = { presets, plugins }复制代码

image.gifimage.gif@babel/preset-env 中还有一个非常重要的参数 targets,最早的时候我们就提过,Babel 转译是按需的,对于环境支持的语法可以不做转换的。就是通过配置 targets 属性,让 Babel 知道目标环境,从而只转译环境不支持的语法。如果没有配置会默认转译所有 ES6 的语法。
**

  1. // src/index.js
  2. const add = (a, b) => a + b
  3. // dist/index.js 没有配置targets
  4. "use strict";
  5. var add = function add(a, b) {
  6. return a + b;
  7. };复制代码

image.gifimage.gif按如下配置 targets
**

  1. // babel.config.js
  2. const presets = [
  3. [
  4. '@babel/env',
  5. {
  6. debug: true,
  7. targets: {
  8. chrome: '58'
  9. }
  10. }
  11. ]
  12. ]
  13. const plugins = ['@babel/plugin-proposal-class-properties']
  14. module.exports = { presets, plugins }复制代码

image.gifimage.gif编译后的结果如下:

  1. // src/index.js
  2. const add = (a, b) => a + b
  3. // dist/index.js 配置targets chrome 58
  4. "use strict";
  5. const add = (a, b) => a + b;复制代码

image.gifimage.gif可以看到 const 和箭头函数都没有被转译,因为这个版本的 chrome 已经支持了这些特性。可以根据需求灵活的配置目标环境。
**

为后方便后续的讲解,把 targets 的配置去掉,让 Babel 默认转译所有语法。

(2)@babel/polyfill

polyfill 直译是垫片的意思,又是 Babel 里一个非常重要的概念。先看下面几行代码:

  1. // src/index.js
  2. const add = (a, b) => a + b
  3. const arr = [1, 2]
  4. const hasThreee = arr.includes(3)
  5. new Promise()复制代码

image.gifimage.gif按之前的方法,执行 npm run babel 后,我们惊奇的发现,Array.prototype.includes 和 Promise 竟然没有被转译!

  1. // dist/index.js
  2. "use strict";
  3. var add = function add(a, b) {
  4. return a + b;
  5. };
  6. var arr = [1, 2];
  7. var hasThreee = arr.includes(3);
  8. new Promise();复制代码

image.gifimage.gif原来 Babel 把 ES6 的标准分为 syntax 和 built-in 两种类型。syntax 就是语法,像 const、=> 这些默认被 Babel 转译的就是 syntax 的类型。而对于那些可以通过改写覆盖的语法就认为是 built-in,像 includes 和 Promise 这些都属于 built-in。而 Babel 默认只转译 syntax 类型的,对于 built-in 类型的就需要通过 @babel/polyfill 来完成转译。@babel/polyfill 实现的原理也非常简单,就是覆盖那些 ES6 新增的 built-in。示意如下:

Object.defineProperty(Array.prototype, 'includes',function(){
  ...
})复制代码

image.gifimage.gif由于 Babel 在 7.4.0 版本中宣布废弃 @babel/polyfill ,而是通过 core-js 替代,所以本文直接使用 core-js 来讲解 polyfill 的用法。

  • 安装 core-js

    npm install --save core-js复制代码
    
  • 注意 core-js 要使用 —save 方式安装,因为它是需要被注入到源码中的,在执行代码前提供执行环境,用来实现 built-in 的注入

  • 配置 useBuiltIns
    在 @babel/preset-env 中通过 useBuiltIns 参数来控制 built-in 的注入。它可以设置为 ‘entry’、’usage’ 和 false 。默认值为 false,不注入垫片。

设置为 ‘entry’ 时,只需要在整个项目的入口处,导入 core-js 即可。

// src/index.js
import 'core-js'

const add = (a, b) => a + b

const arr = [1, 2]
const hasThreee = arr.includes(3)
new Promise()

// dist/index.js
"use strict";

require("core-js/modules/es7.array.includes");
require("core-js/modules/es6.promise");
//
// ……  这里还有很多
//
require("regenerator-runtime/runtime");
var add = function add(a, b) {
  return a + b;
};
var arr = [1, 2];
var hasThreee = arr.includes(3);
new Promise();复制代码
  • 编译后,Babel 会把目标环境不支持的所有 built-in 都注入进来,不管是不是用到,这有一个问题,对于只用到比较少的项目来说完全没有必要,白白增加代码,浪费包体大小。

设置为 ‘usage’ 时,就不用在项目的入口处,导入 core-js了,Babel 会在编译源码的过程中根据 built-in 的使用情况来选择注入相应的实现。

// src/index.js
const add = (a, b) => a + b

const arr = [1, 2]
const hasThreee = arr.includes(3)
new Promise()

// dist/index.js
"use strict";

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

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

require("core-js/modules/es7.array.includes");

var add = function add(a, b) {
  return a + b;
};

var arr = [1, 2];
var hasThreee = arr.includes(3);
new Promise();复制代码
  • 配置 corejs 的版本

当 useBuiltIns 设置为 ‘usage’ 或者 ‘entry’ 时,还需要设置 @babel/preset-env 的 corejs 参数,用来指定注入 built-in 的实现时,使用 corejs 的版本。否则 Babel 日志输出会有一个警告。
最终的 Babel 配置如下:

// babel.config.js
const presets = [
  [
    '@babel/env',
    {
      debug: true,
      useBuiltIns: 'usage',
      corejs: 3,
      targets: {}
    }
  ]
]
const plugins = ['@babel/plugin-proposal-class-properties']

module.exports = { presets, plugins }复制代码

(3)@babel/plugin-transform-runtime

在介绍 @babel/plugin-transform-runtime 的用途之前,先前一个例子:

// src/index.js
const add = (a, b) => a + b

const arr = [1, 2]
const hasThreee = arr.includes(3)
new Promise(resolve=>resolve(10))

class Person {
  static a = 1;
  static b;
  name = 'morrain';
  age = 18
}

// dist/index.js
"use strict";

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

require("core-js/modules/es.object.define-property");

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

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

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

function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }

var add = function add(a, b) {
  return a + b;
};

var arr = [1, 2];
var hasThreee = arr.includes(3);
new Promise(function (resolve) {
  return resolve(10);
});

var Person = function Person() {
  _classCallCheck(this, Person);

  _defineProperty(this, "name", 'morrain');

  _defineProperty(this, "age", 18);
};

_defineProperty(Person, "a", 1);

_defineProperty(Person, "b", void 0);复制代码

image.gifimage.gif在编译的过程中,对于 built-in 类型的语法通过 require(“core-js/modules/xxxx”) polyfill 的方式来兼容,对于 syntax 类型的语法在转译的过程会在当前模块中注入类似 _classCallCheck 和 _defineProperty 的 helper 函数来实现兼容。对于一个模块而言,可能还好,但对于项目中肯定是很多模块,每个模块模块都注入这些 helper 函数,势必会造成代码量变得很大。

而 @babel/plugin-transform-runtime 就是为了复用这些 helper 函数,缩小代码体积而生的。当然除此之外,它还能为编译后的代码提供一个沙箱环境,避免全局污染。

使用 @babel/plugin-transform-runtime

  • ①安装
    npm install --save-dev @babel/plugin-transform-runtime
    npm install --save @babel/runtime复制代码
    

image.gifimage.gif其中 @babel/plugin-transform-runtime 是编译时使用的,安装为开发依赖,而 @babel/runtime 其实就是 helper 函数的集合,需要被引入到编译后代码中,所以安装为生产依赖

  • ②修改 Babel plugins 配置,增加@babel/plugin-transform-runtime
    ```javascript // babel.config.js const presets = [ [ ‘@babel/env’, {
    debug: true,
    useBuiltIns: 'usage',
    corejs: 3,
    targets: {}
    
    } ] ] const plugins = [ ‘@babel/plugin-proposal-class-properties’, [ ‘@babel/plugin-transform-runtime’ ] ]

module.exports = { presets, plugins }复制代码


- 之前的例子,再次编译后,可以看到,之前的 helper 函数,都变成类似require("@babel/runtime/helpers/classCallCheck") 的实现了。

```javascript
// dist/index.js
"use strict";

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

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

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

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

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

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

var add = function add(a, b) {
  return a + b;
};

var arr = [1, 2];
var hasThreee = arr.includes(3);
new Promise(function (resolve) {
  return resolve(10);
});

var Person = function Person() {
  (0, _classCallCheck2["default"])(this, Person);
  (0, _defineProperty2["default"])(this, "name", 'morrain');
  (0, _defineProperty2["default"])(this, "age", 18);
};

(0, _defineProperty2["default"])(Person, "a", 1);
(0, _defineProperty2["default"])(Person, "b", void 0);复制代码
  • 配置 @babel/plugin-transform-runtime

到目前为止,对于 built-in 类型的语法还是通过 require(“core-js/modules/xxxx”) polyfill 的方式来实现的,例如为了支持 Array.prototype.includes 方法,需要 require
(“core-js/modules/es.array.includes”) 在 Array.prototype 中添加 includes 方法来实现的,但这会导致一个问题,它是直接修改原型的,会造成全局污染。如果你开发的是独立的应用问题不大,但如果开发的是工具库,被其它项目引用,而恰好该项目自身实现了 Array.prototype.includes 方法,这样就出了大问题!而 @babel/plugin-transform-runtime 可以解决这个问题,只需要配置 @babel/plugin-transform-runtime 的参数 corejs。该参数默认为 false,可以设置为 2 或者 3,分别对应 @babel/runtime-corejs2 和 @babel/runtime-corejs3。
把 @babel/plugin-transform-runtime 的 corejs 的值设置为3,把 @babel/runtime 替换为 @babel/runtime-corejs3。
去掉 @babel/preset-env 的 useBuiltIns 和 corejs 的配置,去掉 core-js。因为使用 @babel/runtime-corejs3 来实现对 built-in 类型语法的兼容,不用再使用 useBuiltIns了。
**

npm uninstall @babel/runtime
npm install --save @babel/runtime-corejs3
npm uninstall core-js复制代码
// babel.config.js
const presets = [
  [
    '@babel/env',
    {
      debug: true,
      targets: {}
    }
  ]
]
const plugins = [
  '@babel/plugin-proposal-class-properties',
  [
    '@babel/plugin-transform-runtime',
    {
      corejs: 3
    }
  ]
]

module.exports = { presets, plugins }


// dist/index.js
"use strict";

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

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

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

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

var _includes = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/instance/includes"));

var add = function add(a, b) {
  return a + b;
};

var arr = [1, 2];
var hasThreee = (0, _includes["default"])(arr).call(arr, 3);
new _promise["default"](function (resolve) {
  return resolve(10);
});

var Person = function Person() {
  (0, _classCallCheck2["default"])(this, Person);
  (0, _defineProperty2["default"])(this, "name", 'morrain');
  (0, _defineProperty2["default"])(this, "age", 18);
};

(0, _defineProperty2["default"])(Person, "a", 1);
(0, _defineProperty2["default"])(Person, "b", void 0);复制代码

image.gifimage.gif可以看到 Promise 和 arr.includes 的实现已经变成局部变量,并没有修改全局上的实现。

3、Babel polyfill 实现方式的区别

截至目前为止,对于 built-in 类型的语法的 polyfill,一共有三种方式:

  • 使用 @babel/preset-env ,useBuiltIns 设置为 ‘entry’
  • 使用 @babel/preset-env ,useBuiltIns 设置为 ‘usage’
  • 使用 @babel/plugin-transform-runtime

前两种方式支持设置 targets ,可以根据目标环境来适配。useBuiltIns 设置为 ‘entry’ 会注入目标环境不支持的所有 built-in 类型语法,useBuiltIns 设置为 ‘usage’ 会注入目标环境不支持的所有被用到的 built-in 类型语法。注入的 built-in 类型的语法会污染全局。

第三种方式目前不支持设置 targets,所以不会考虑目标环境是否已经支持,它是通过局部变量的方式实现了所有被用到的 built-in 类型语法,不会污染全局。

针对第三种方式不支持设置 targets 的问题,Babel 正在考虑解决,目前意向的方案是通过 Polyfill provider 来统一 polyfill 的实现:

  • 废弃 @babel/preset-env 中 useBuiltIns 和 corejs 两个参数,不再通过 @babel/preset-env 实现 polyfill。
  • 废弃 @babel/plugin-transform-runtime 中的 corejs 参数,也不再通过 @babel/plugin-transform-runtime 来实现 polyfill。
  • 增加 polyfills 参数,类似于现在 presets 和 plugins,用来取代现在的 polyfill 方案。
  • 把 @babel/preset-env 中 targets 参数,往上提一层,和 presets、plugins、polyfills 同级别,并由它们共享。

这个方案实现后,Babel 的配置会是下面的样子:

// babel.config.js
const targets = [
  '>1%'
]
const presets = [
  [
    '@babel/env',
    {
      debug: true
    }
  ]
]
const plugins = [
  '@babel/plugin-proposal-class-properties'
]
const polyfills = [
  [
    'corejs3',
    {
      method: 'usage-pure'
    }
  ]
]

module.exports = { targets, presets, plugins, polyfills }复制代码

image.gifimage.gif配置中的 method 值有 ‘entry-global’、’usage-global’、’usage-pure’ 三种。

  • ‘entry-global’ 等价于 @babel/preset-env 中的 useBuiltIns: ‘entry’
  • ‘usage-global’ 等价于 @babel/preset-env 中的 useBuiltIns: ‘usage’
  • ‘usage-pure’ 等价于 @babel/plugin-transform-runtime 中的 corejs