babel 7 全套

babel 官方在 2018-08-27 发布了文章,babel 7 正式发布,距离 babel 6 相隔3年。

包含内容(官网cp):

  1. upgrade tool
  2. JavaScript 配置文件 babel.config.js
  3. 选择性的配置 overrides
  4. TC39 提案 支持 @babel/proposals
  5. jsx、typescript、flow 的支持
  6. Babel 辅助函数的变化
  7. 自动 Polyfill (试验) useBuiltins

详细内容可见:Babel 7 发布

此篇文章主要介绍 babel 7 相关 presetsplugins@babel 下其他所有的 packages、babel 其他工具、以及 babel 插件相关,即 babel 全套(包含部分流程上的源码链接)

至于 AST 抽象语法树相关可见另一文 抽象语法树-AST-与-编译器-Compiler 内容。

transform or polyfill

ES 相关功能可分为:

  1. 新的写法、运算符、async/await、const、let、class 等老浏览器无法识别的一类
  2. Object.assign 等可通过 现有 JavaScript 代码封装实现(例如 core-js / polyfill)的语法糖

要注意的是,babel 作为转换工具,@babel/core, 大部分(也可以理解为全部) babel plugins 只会作用于第1类。

像 第2类 的处理,需要 @babel/polyfill (已废弃,替代品见下文),或者 @babel/preset-env 设定 useBuiltIns 进行,参见:babel-preset-env index.js L190

具体需要用到 polyfill 的功能,可见

babel 概览

babel 仓库 packages

详见: babel monorepo

babel 7 升级后,相应 babel 包全部统一至 @babel namespace 下。

可以理解为,babel 自身的能力,全部在 @babel namespace 下,而像 babel-loadergulp-babel 此类其他工具的插件,则仍然作为单独的 package

plugins

从上方的仓库包内,或者 babel 官网 plugins 上,可以看到非常多的 plugins

这里我们先关注 transform/ proposal 的插件。其对应的作用,就是将抽象语法树 A(es 2015 及以上等等) 转换为 抽象语法树 B(es 5),在上一文 抽象语法树-AST-与-编译器-Compiler 中,称为 visitor

presets

babel 6 支持 preset-es2015 这类年份已经成规范的内容,以及 preset-stage-0 这类未确定的草案的内容。

而 babel 7 统一:

  1. preset-es2015 这类年份已经成规范的内容统一为 @babel/preset-env,因为规范已落地,可以放心提前使用,提前了解 JS 新规范特性,也减少大家的配置负担(不需要关心到底是 es2015、es2016 还是 es2017)
  2. preset-stage-0 这类未确定的草案,移出 preset。因为草案未落地,使用了这些最新的特性,可能对大家未来没有太大帮助。减少未确定的语法的干扰

当下 presets 官方支持的有:

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

而如果我们还是想要使用到一些草案性质的语法、方法,在 babel monorepo 列表中,可以看到很多 plugin-proposal- 前缀的,可以使用对应的 plugin 进行引入

另一方面,我们如果查看 @babel/preset-envpackage.json 文件

1
2
3
4
5
// etc...
"@babel/plugin-proposal-object-rest-spread": "^7.5.5",
"@babel/plugin-proposal-optional-catch-binding": "^7.2.0",
"@babel/plugin-proposal-unicode-property-regex": "^7.4.4",
// etc...

也就更清楚,presets 的含义,就是 “预设” 的 plugins,进行的相应组装

注意:Plugin 会运行在 Preset 之前,Plugin 会从前到后顺序执行,Preset 的顺序则是从后向前。

babel 转换流程

以此段代码为例

1
2
const a = 1
console.log([1].includes(a))
1
2
3
"scripts": {
"build": "babel src --out-dir dist"
}

@babel/cli

  1. commander 解析
  2. dir 文件
  3. util.compile
  4. 默认 / 相关配置项
  5. 调用 @babel/core transformFile

基于 babel 配置 / 命令行配置,使用 @babel/core 进行编译、输出

@babel/core

  1. transformFile 读取文件,运行 runAsync
  2. transformation/index.js runSync
  3. normalizeFile
  4. transformFile
  5. generateCode

@babel/parser

  1. 上方的 normalizeFile 对应的调用 @babel/parser

@babel/traverse

  1. 上方的 transformFile 对应的合并 plugin vistors,调用 @babel/traverse

@babel/generator

  1. 上方的 generateCode 对应的调用 @babel/generator

概括

@babel/core transform / transformFile ... 方法,包含了 @babel/parser,@babel/traverse,@babel/generator 连串调用。

@babel/preset-env 配置项

useBuiltIns && corejs

useBuiltIns: ‘entry’

1
2
3
4
// 必须在开头引入
import '@babel/polyfill'
const a = 1
console.log([1].includes(a))
corejs: 2
1
2
3
4
5
6
// 编译后,@babel/polyfill 被拆分成 N 个包
require("core-js/modules/es6.array.copy-within");
// *** 省略 200+行 ***
require("regenerator-runtime/runtime");
var a = 1;
console.log([1].includes(a))
corejs: 3
1
2
3
4
// 没变化
require("@babel/polyfill");
var a = 1;
console.log([1].includes(a));

因为 @babel/polyfill 只是 core-js 2.x + regenerator-runtime 的组合,因此其无法被处理出 core-js 3

corejs: 3 + useBuiltIns: 'entry' 的话,就会报警告: @babel/polyfill is deprecated. Please, use required parts of core-js and regenerator-runtime/runtime separately

如果将原代码更改为:

1
2
3
4
import 'core-js'
import 'regenerator-runtime/runtime'
const a = 1
console.log([1].includes(a))
1
2
3
4
5
6
// 编译后,core-js 被拆分成 N 个包
require("core-js/modules/es.symbol");
// *** 省略 500+行 ***
require("regenerator-runtime/runtime");
var a = 1;
console.log([1].includes(a))

useBuiltIns: ‘usage’

1
2
3
4
// 可以省略
// import '@babel/polyfill'
const a = 1
console.log([1].includes(a))
corejs: 2
1
2
3
4
5
6
// 多了这两行
require("core-js/modules/es7.array.includes");
require("core-js/modules/es6.string.includes");
// import '@babel/polyfill'
var a = 1;
console.log([1].includes(a));
corejs: 3
1
2
3
4
require("core-js/modules/es.array.includes");
// import '@babel/polyfill'
var a = 1;
console.log([1].includes(a));

概括

  1. @babel/polyfill 已被弃用,建议使用 core-jsregenerator-runtime/runtime 代替。因 @babel/polyfill 就是它俩组成,用来模拟完整的 ES2015+ 环境。确实没必要再包一层
  2. corejs 3 比 2 更完善
  3. 根据具体需要使用 useBuiltIns: 'usage'useBuiltIns: 'entry' ( usage 风险:npm 的 dependencies 包进行业务开发,babel 默认是不会检测 依赖包的代码的。 也就是说,如果某个需要 polyfill 的特性,依赖包使用了但是业务代码没有使用,会引起未可知的 BUG)

参考

  1. Polyfill 方案的过去、现在和未来
  2. Babel 社区概览

其他配置项

  1. 详见:@babel/preset-env docs

@babel namespace 下其他工具

弃用

@babel/polyfill

已废弃,使用 import 'core-js'; import 'regenerator-runtime/runtime' 代替

babel 其他运行方式

@babel/node

  1. 以 child_process 形式
  2. @babel/core transform,与上方 transformFile 流程类似
  3. node repl 调用node vm 运行
  4. 才疏学浅,也不好解释更多。最关键:不要在生产环境运行。 开发环境大概可以减少一点测试时间,类似 ts-node,属于偷懒用法 / 做法,用处不大
  5. 详见:@babel/node docs

@babel/register

  1. 通过 pirates 工具,给 node require 加了 hook
  2. 在文件顶部使用 require('@babel/register') 后,后续 require("./my-plugin.xxx") 都会经过 babel.transfrom 后,得到编译后的代码再运行
  3. 文档:@babel/register
  4. 类似 @babel/node,偷懒用法 / 做法,最关键:不要在生产环境运行

@babel/standalone —— 浏览器 / 特定环境

  1. 一个完整的 babel.js / babel.min.js 文件,用来进行 原代码 transform 等功能
  2. 文档:@babel/standalone
  3. 一般情况下,用处不大,也用不上。除非是需要特有环境下运行的,例如:JSFiddle, JS Bin, the REPL on the Babel site

parser 阶段语法检查

@babel/highlight

  1. 使用 js-tokens(有趣的是,其内部用的是 另一个老牌解析器 esprima) 分词
  2. 使用 命令行高亮
  3. 具体用法见:@babel/highlight

@babel/code-frame

  1. 使用 @babel/highlight,大致是优化了错误代码的显示,加了代码下方的箭头 / 错误信息。具体用法见:@babel/code-frame docs

编译结果优化

@babel/plugin-transform-runtime

例如:

1
class A {}

如果不使用此插件,编译后内容

1
2
3
4
5
6
7
"use strict";

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

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

如果是多个文件,那么每个文件都会有 _classCallCheck 方法,如果再把这些文件合并在一起,_classCallCheck 就会有 N 次定义,以此类推,还有其他各种函数。

如果使用了插件

1
2
3
4
5
6
7
plugins: [
['@babel/transform-runtime', {
// corejs: false, use @babel/runtime
// corejs: 2, use @babel/runtime-corejs2
corejs: 3, // use @babel/runtime-corejs3
}],
]

对应的编译后内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// corejs false
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
var A = function A() {
(0, _classCallCheck2["default"])(this, A);
};

// corejs 2
var _interopRequireDefault = require("@babel/runtime-corejs2/helpers/interopRequireDefault");
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/classCallCheck"));
var A = function A() {
(0, _classCallCheck2["default"])(this, A);
};

// corejs 3
var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault");
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/classCallCheck"));
var A = function A() {
(0, _classCallCheck2["default"])(this, A);
};

就仍然会是 require 形式引用。最终 webpack、browserify 等工具进行打包时, 就不会有 N 个重复的

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

  1. 文档见:@babel/plugin-transform-runtime docs
  2. 看起来上方,corejs 的配置最终编译后内容没什么差距,但实际上 Symbol 等使用,编译后结果会有一些差异。因此还是推荐根据 @babel/env 配置 的 corejs 版本,相应的也 进行 @babel/transform-runtime 插件的 corejs 配置。

@babel/runtime, @babel/runtime-corejs2, @babel/runtime-corejs3

针对 @babel/envcorejs 配置

1
2
3
4
['@babel/env', {
useBuiltIns: 'usage',
corejs: 3,
}]

对应的 runtime 版本,仅用于 @babel/transform-runtime 插件内部

@babel/plugin-external-helpers

功能 与 @babel/plugin-transform-runtime 类似 或者 说有冲突,推荐使用 @babel/plugin-transform-runtime,此工具也就没必要了。

其需要配合 @babel/clibabel-external-helpers 命令行工具使用,参考阅读:babel-external-helpers

babel 关于移除 @babel/plugin-external-helpers 的讨论 - Remove babel-plugin-external-helpers in favor of modular helpers

@babel/helpers

像前文提到的,会有 _classCallCheck 产生:

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

其是通过 @babel/helpers helpers.js,经过 @babel/template 转换成 AST 后,插入的。

而像 @babel/plugin-transform-runtime,实际上有一个 build-dist.js@babel/helpers helpers.js 里面的这些 helper,一个个的编译成单独的文件放在 @babel/runtime helpers 目录下,相应处理后,此部分原来被添加的代码片段,就变成了

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

其他 helpers 同理

其他 - 解析 / 转换 / 插件开发相关

  • @babel/types
  • @babel/template
  • @babel/helper-xxxx-xxxx
  • @babel/plugin-proposal-xxxx-xxxx
  • @babel/plugin-syntax-xxxx-xxxx
  • @babel/plugin-transform-xxxx-xxxx

此部分内容,为避免此文太长(不看),单独放到 babel 7 插件开发相关 一文内。

其他 babel 工具

还好用

用处不大 / 再见了您

  • ember-cli-babel
  • broccoli-babel-transpiler
  • babelify
  • babel-brunch
  • grunt-babel
  • generator-babel-plugin

其他 presets or plugins

其他参考