跳到主要内容

Babel 的内置功能(上)

babel 最开始的名字叫 6to5,主要是做 es6 到 es5 语法的转换和 polyfill,后来在 4.0 时改名为了 babel。虽然从 6to5 改名到了 babel,但是做的事情并没有变,依然是从高版本语法和 api 转换成低版本的语法并自动 polyfill 缺少的 api。

babel 是怎么实现这些功能的呢?

从插件到 preset​

要实现转换,第一步要明确转换什么: 划定一个集合放要转换的特性,再划定一个集合放转换到的目标特性,两者建立一一映射关系。就确定了我们要做哪些转换。

exponentiation operator​

比如乘方运算符,我们会用 Math.pow 来实现

let x = 10 ** 2;

x **= 3;

转换为

let x = Math.pow(10, 2);

x = Math.pow(x, 3);

class​

再比如 class,我们会用 function、prototype 来实现

class Test {
constructor(name) {
this.name = name;
}

logger() {
console.log("Hello", this.name);
}
}

转换为

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

var Test = (function () {
function Test(name) {
_classCallCheck(this, Test);

this.name = name;
}

Test.prototype.logger = function logger() {
console.log("Hello", this.name);
};

return Test;
})();

每一个语法都可以这样转换为低版本的语法,那把所有的这种高版本语法写的代码转换为低版本的,那不就实现了编译了么。

但是只是转换并不能解决所有问题,涉及到某个对象的 api,比如 Array.prototype.find,这种 api 的兼容并不是需要转换语法,而是要在环境中注入我们实现的 api,也就是 polyfill (垫片)。

所以我们做的事情除了语法转换外,还有 api 的 polyfill。

先说语法转换。

我们要转换哪些语法呢?

babel 插件需要转换的语法包括 es 标准语法、proposal 阶段的语法,还有 react、flow、typescript 等特有语法。

es 标准语法​

我们知道,TC39 是制定 javascript 语言标准的组织,每年都会公布加入到语言标准的特性,es2015、es2016、es2017 等。这些是我们要转换的语言特性范围。

在 babel6 时,分别用 preset-es2015、 preset-es2016 等来维护相应的 transform plugin,但在 babel7 的时候就改为 preset env 了。

proposal 阶段的语法​

babel 要转换的不只是加入标准的特性,语言特性从提出到标准会有一个过程,分为几个阶段。

  • 阶段 0 - Strawman: 只是一个想法,可能用 babel plugin 实现
  • 阶段 1 - Proposal: 值得继续的建议
  • 阶段 2 - Draft: 建立 spec
  • 阶段 3 - Candidate: 完成 spec 并且在浏览器实现
  • 阶段 4 - Finished: 会加入到下一年的 es20xx spec

这些还未加入到语言标准的特性也是要支持的。

react、flow、typescript​

只是转换 javascript 本身的 es spec 和 proposal 的特性特性并不够,现在我们开发的时候 jsx、typescript、flow 这些都是会用的,babel 肯定也得支持。

这些转换对应的 plugin 分别放在不同 preset 里: preset-jsx、preset-typescript、preset-flow。

我们要转换的范围又大了一些。

上面是插件要转换的语言特性,babel7 内置的实现这些特性的插件分为 syntax、transform、proposal 3类。

syntax plugin​

syntax plugin 是在 parserOptions 中放入一个 flag 让 parser 知道要 parse 什么语法,最终的 parse 逻辑还是 babel parser(babylon) 实现的。

一般 syntax plugin 都是这样的:

import { declare } from "@babel/helper-plugin-utils";

export default declare((api) => {
api.assertVersion(7);

return {
name: "syntax-function-bind",

manipulateOptions(opts, parserOpts) {
parserOpts.plugins.push("functionBind");
},
};
});

这些插件的目的就是让 parser 能够正确的解析对应的语法成 AST。

transform plugin​

transform plugin 是对 AST 的转换,各种 es20xx 语言特性、typescript、jsx 等的转换都是在 transform plugin 里面实现的。

有的时候需要结合 syntax plugin 和 transform plugin, 比如 typescript 的语法解析要使用 @babel/plugin-syntax-typescript 在 parserOptions 放入解析 typescript 语法的选项,然后使用 @babel/plugin-transform-typescript 来转换解析出的 typescript 对应的 AST 的转换。

平时我们一般使用 @babel/preset-typescript,它对上面两个插件做了封装。

proposal plugin​

未加入语言标准的特性的 AST 转换插件叫 proposal plugin,其实他也是 transform plugin,但是为了和标准特性区分,所以这样叫。

完成 proposal 特性的支持,有时同样需要 综合 syntax plugin 和 proposal plugin,比如 function bind (:: 操作符)就需要同时使用 @babel/plugin-syntax-function-bind 和 @babel/plugin-proposal-function-bind。

总之,babel 的内置的 plugin,就 @babel/plugin-syntax-xxx, @babel/plugin-transform-xxx、@babel/plugin-proposal-xxx 3种。

这样的 plugin 还是很多的,所以又设计了 preset。

preset​

用于不同的目的需要不同的 babel 插件,所以 babel 设计了 preset

  • 不同版本的语言标准支持: preset-es2015、preset-es2016 等,babel7 后用 preset-env 代替
  • 未加入标准的语言特性的支持: 用于 stage0、stage1、stage2 的特性,babel7 后单独引入 proposal plugin
  • 用于 react、jsx、flow 的支持:分别封装相应的插件为 preset-react、preset-jsx、preset-flow,直接使用对应 preset 即可

preset 就是插件的集合,但是它可以动态确定包含的插件,比如 preset-env 就是根据 targets 来确定插件。

插件和插件之间自然有一些公共的代码,这部分放在 helper 里:

helper​

每个特性的实现用一个 babel 插件实现,当 babel 插件多了,自然会有一些共同的逻辑。这部分逻辑怎么共享呢?

babel 设计了插件之间共享逻辑的机制,就是 helper。

helper 分为两种:

  • 一种是注入到 AST 的运行时用的全局函数
  • 一种是操作 AST 的工具函数,比如变量提升这种通用逻辑
注入到 AST 的全局函数​

注入到 AST 的运行时用的全局函数,比如

class Guang {}

会被转换成

function _classCallCheck(instance, Constructor) {
//...
}

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

这里的 _classCallCheck 就是 helper。

这类 helper 数量比较多,babel7 有 80 多个,都在 @babel/helpers 里面。在插件里使用的话,直接调用 this.addHelper,会在顶层作用域声明对应的 helper,然后返回对应的 identifier。

var transformObjectSetPrototypeOfToAssign = declare(function (api) {
api.assertVersion(7);
return {
name: "transform-object-set-prototype-of-to-assign",
visitor: {
CallExpression: function CallExpression(path) {
if (
path.get("callee").matchesPattern("Object.setPrototypeOf")
) {
path.node.callee = this.addHelper("defaults");
}
},
},
};
});

其实一般我们也用不到,主要是 babel 内部用的。

这种 helper 是用于用低版本特性实现高版本特性的,比如用 function 和 prototype 实现 class.

内部用 template 实现的:

除了编译的时候注入 helper 以外,runtime 包里也要包含这些 helper。

因为我们可以把 helper 注入到 AST,也可以抽离成从 runtime 包引入的形式:

比如这样:

除了用于注入同样的 AST 的 helper,还有一些公共逻辑的 helper:

操作 AST 的工具函数​

操作 AST 的工具函数,比如变量提升自己实现的话还是比较麻烦的,这种通用逻辑可以封装到 helper 里,然后插件里直接用:

const hoistVariables = require('@babel/helper-hoist-variables').default;

cosnt plugin = function () {
visitor: {
VariableDeclaration(path) {
hoistVariables(path.parentPath, (id) => {
path.scope.parent.push({
id: path.scope.generateUidIdentifier(id.name)
});
return id;
}, 'const' );
}
}
}

当输入为

function func() {
const a = 1;
const b = 2;
}

输出为

var _a, _b;

function func() {
a = 1;
b = 2;
}

我们借助 @babel/helper-hoist-variables 轻松实现了变量提升的逻辑。

再举一个例子

const importModule = require('@babel/helper-module-imports');

cosnt plugin = function ({ template }) {
visitor: {
Program(path) {
const reactIdentifier = importModule.addDefault(path, 'lodash',{
nameHint: '_'
});
path.node.body.push(template.ast(`const get = _.get`));
}
}
}

会在代码中加入模块引入和变量声明的代码

var _ = _interopRequireDefault(require("lodash")).default;

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

const get = _.get;

我们借助 @babel/helper-module-imports 可以很轻松的引入一个模块,通过 named import、default import 或者 namespace import 的方式。

这类 helper 的特点是需要手动引入对应的包,调用 api,而不是直接 this.addHelper 就行。

说了这么多,其实 helper 一般我们也不会用到,知道它是做啥的就行。

babel helpers 是用于 babel plugin 逻辑复用的一些工具函数,分为用于注入 runtime 代码的 helper 和用于简化 AST 操作 的 helper两种。

第一种都在 @babel/helpers 包里,直接 this.addHelper(name) 就可以引入, 而第二种需要手动引入包和调用 api。

前面提到了有的 api 会运行时引入,那 runtime 包里具体有啥呢?

babel runtime​

babel runtime 里面放运行时加载的模块,会被打包工具打包到产物中,下面放着各种需要在 runtime 使用的函数,包括三部分:regenerator、corejs、helper。

  • corejs 这就是新的 api 的 polyfill,分为 2 和 3 两个版本,3 才实现了实例方法的polyfill

  • regenerator 是 facebook 实现的 aync 的 runtime 库,babel 使用 regenerator-runtime来支持实现 async await 的支持。

  • helper 是 babel 做语法转换时用到的函数,比如 _typeof、_extends 等

babel 做语法转换和 api 的 polyfill,需要自己实现一部分 runtime 的函数,就是 helper 部分。

有的也没有自己实现,用的第三方的,比如 regenerator 是用的 facebook 的。api 的 polyfill 也是用的 core-js 的,babel 对它们做了整合。

因为 async await 这种特性的实现还是比较复杂的,标准 api 的实现的跟进也需要花精力,所以 babel 直接用了社区的实现。

总结​

我们知道了 babel 内置的 plugin 分为了 transform、proposal、syntax 三种,也知道了 preset 就是插件的集合。

插件之间的可复用的 AST 操作逻辑,需要注入的公共代码都在 helper 里。

除了注入到 AST 外,还有一部分是从 runtime 包引入的。runtime 包分为 helper、regenerator、core-js 3部分。后面两个都是社区的实现。

知道了 preset、helper、runtime 都是什么,那 babel 是怎么基于这些来实现语法转换和 api polyfill 的功能的呢?

下节我们继续聊。