Babel 的工程化实现

介绍

Babel 是一款将未来的 JavaScript 语法编译成过去语法的 Node.js 工具。本文从 2019 年 11 月的 master 分支源码入手,介绍 Babel 在解决这类问题时是如何划分模块。

Babel 的模块划分

[图片上传失败...(image-b1785-1578232227395)]

其中 babel-loader 隶属于 webpack,不在 Babel 主仓库。

框架层

常见的编译器

常见的解析器有 acorn、@babel/parser (babylon)、flow、traceur、typescript、uglify-js 等,各自的 AST 语法树大致相同。

@babel/parser 的实现

关键词说明

  • Literal:字面量。包括:Boolean、Number、String。
  • Identifier:识别量。包括变量名、undefined、null 等。
  • Val:值。常分为左值和右值。左值表示一个可以被赋值的节点,如:[a] 等,左值往往是 Pattern、Identifier 等类型。右值表示一个代表具体值的节点,如:b.c 等,右值往往是 Expression、Identifier、Literal 等类型。左值与右值之间通过等号联结,代表赋值表达式,如:[a] = b.c。
  • Declaration:赋值。
  • Expression:表达式。常用来表示右值。常见的 Expression 有:MemberExpression、BinaryExpression、UnaryExpression、AssignmentExpression、CallExpression 等。
  • Statement:语句。往往由 Expression 组合而成。常见的 Statement 有:ExpressionStatement。
  • Program:程序。所有代码在一个 Program 下,一个 Program 包含多个并列的 Statement。
let c = 0;
while (a < 10) {
  const b = a % 2;
  if (b == 0) {
    c++;
  }
}
console.log(c);

上面的这段代码通过 @babel/parser 解析后得到的 AST 语法树如下:

[图片上传失败...(image-9dab40-1578232227395)]

@babel/parser 的 9 层继承

[图片上传失败...(image-76f30f-1578232227395)]

  • Parser:初始化
  • StatementParser:解析语句,拼装成 program,代码大约有 2100+ 行
  • ExpressionParser:解析表达式,代码大约有 2400+ 行
  • LValParser:左值处理,将节点变为可以被赋值的节点。如:ArrayExpression 转为 ArrayPattern
  • NodeUtils:AST 节点操作,如复制等
  • UtilParser:工具函数,如判断行末等
  • Tokenizer:词法分析,大约有 1400+ 行
  • LocationParser:文件位置信息
  • CommentsParser:解析注释
  • BaseParser:插件能力

大部分模块代码量在百行左右,其中 StatementParser、ExpressionParser 和 Tokenizer 有较多复杂逻辑。

@babel/traverse

提供遍历 AST 语法树的能力,如:

traverse(ast, {
  FunctionDeclaration: function(path) {
    path.node.id.name = "x";
  }
});

traverse(ast, {
  enter(path) {
    if (path.isIdentifier({ name: "n" })) {
      path.node.name = "x";
    }
  }
});

path 对象上有下面的属性和方法:

  • 属性
    • node:节点
    • parent:父节点
    • parentPath:父节点的 path
    • scope:作用域
  • 方法
    • get:获取子节点属性
    • findParent:向父节点搜寻节点
    • getSibling:获取兄弟路径
    • getFunctionParent:获取包含该节点最近的父路径,并且是 function
    • getStatementParent:获取最近的 Statement 类型的父节点
    • replaceWith:用 AST 节点替换该节点
    • replaceWithMultiple:用多个 AST 节点替换该节点
    • replaceWithSourceString:用源码解析后的 AST 节点替换该节点
    • insertBefore:在该节点前插入兄弟节点
    • insertAfter:在该节点后插入兄弟节点
    • remove:删除节点
    • pushContainer:将 AST 节点 push 到节点的属性里面,类似数组操作

@babel/generator

将 AST 转为代码文本。示例用法:

import { parse } from '@babel/parser';
import generate from '@babel/generator';

const ast = parse('class Example {}');
generate(ast); // => { code: 'class Example {}' }

可以生成 source map。

import { parse } from '@babel/parser';
import generate from '@babel/generator';

const code = 'class Example {}';
const ast = parse(code);

const output = generate(ast, { sourceMaps: true, sourceFileName: code }); // => { code: 'class Example {}', rawMappings: ... }
// or
const output = generate(ast, { sourceMaps: true, sourceFileName: 'source.js' }, code); // => { code: 'class Example {}', rawMappings: ... }

还可以合并多个文件,同时生成 source map。

import { parse } from '@babel/parser';
import generate from '@babel/generator';

const a = 'var a = 1;';
const b = 'var b = 2;';
const astA = parse(a, { sourceFilename: 'a.js' });
const astB = parse(b, { sourceFilename: 'b.js' });
const ast = {
  type: 'Program',
  body: [...astA.program.body, ...astB.program.body]
};

const { code, map } = generate(ast, { sourceMaps: true }, {
  'a.js': a,
  'b.js': b
});

@babel/core

主要提供 transform 和 parse 相关的 API。

transform 的流程主要是 parse -> traverse -> generate。

parse 主要提供对 @babel/parser 的封装。

实现层

@babel/plugin

@babel/plugin-syntax-x

通过插件开关打卡语法解析能力。@babel/parser 中判断了 plugin 开关,实现了这些语法解析能力。如 @babel/plugin-syntax-jsx:

parserOpts.plugins.push("jsx");

@babel/plugin-transform-x

实现语法的转换。如 @babel/plugin-transform-exponentiation-operator:

export default {
  name: "transform-exponentiation-operator",
  visitor: build({
    operator: "**",
    build(left, right) {
      return t.callExpression(
        t.memberExpression(t.identifier("Math"), t.identifier("pow")),
        [left, right],
      );
    },
  }),
}

@babel/plugin-proposal-x

支持草案级别的语法转换。如 @babel/plugin-proposal-numeric-separator:

export default {
  name: "proposal-numeric-separator",
  inherits: syntaxNumericSeparator,

  visitor: {
    CallExpression: replaceNumberArg,
    NewExpression: replaceNumberArg,
    NumericLiteral({ node }) {
      const { extra } = node;
      if (extra && /_/.test(extra.raw)) {
        extra.raw = extra.raw.replace(/_/g, "");
      }
    },
  },
}

@babel/preset-x

提供各类组合好的 plugins、syntax 和 helpers。

常用的是 @babel/preset-env,结合 browserslist 设置代码的兼容性。

@babel/polyfill

从 Babel 7.4.0 起废弃,推荐使用 core-js 和 regenerator-runtime。其中 core-js 提供了 ECMAScript 的所有兼容代码,regenerator-runtime 提供了 async、generator 等函数的执行环境。

@babel/helpers

定义了 Babel 运行环境的辅助函数。如在 class 模块前插入 classCallCheck 的 helper。

plugin 内的函数调用方式:

export default {
  visitor: {
    ClassExpression(path) {
        this.addHelper("classCallCheck");
      // ...
  }
};

生成的代码中将包含 classCallCheck:

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

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

@babel/runtime

提供 Babel 的运行环境,包括 regenerator-runtime。运行环境会提供一些辅助代码,如:

使用 @babel/helpers 的情况:

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

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

使用 @babel/plugin-transform-runtime 可以把这些代码复用起来:

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

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

@babel/runtime 源码中没有内容,依赖构建脚本将 @babel/helpers 中的代码复制过去。

除了这个运行环境之外,Babel 还提供了 @babel/runtime-corejs2 和 @babel/runtime-corejs3,分别是基于 core-js v2 和 v3 提供的运行环境。可以在 @babel/plugin-transform-runtime 的 corejs 参数中设置使用的运行环境。

辅助层

@babel/types

提供基础的类型值,创建类型的函数,便于 @babel/plugin、@babel/parser 等使用。

const binaryExpression = t.binaryExpression('+', t.numericLiteral(1), t.numericLiteral(2))

@babel/code-frame

打印出错位置。示例代码:

import { codeFrameColumns } from '@babel/code-frame';

const rawLines = `class Foo {
  constructor()
}`;
const location = { start: { line: 2, column: 16 } };
codeFrameColumns(rawLines, location);

输出的结果是:

  1 | class Foo {
> 2 |   constructor()
    |                ^
  3 | }

@babel/highlight

面向控制台输出有颜色的代码片段。

import highlight from "@babel/highlight";

const code = `class Foo {
  constructor()
}`;
highlight(code);                                                // => "\u001b[36mclass\u001b[39m \u001b[33mFoo\u001b[39m {\n  constructor()\n}"

展示在控制台上:

@babel/highlight

@babel/template

模板引擎。

import template from "@babel/template";
import generate from "@babel/generator";
import * as t from "@babel/types";

const buildRequire = template(`
  var %%importName%% = require(%%source%%);
`);

const ast = buildRequire({
  importName: t.identifier("myModule"),
  source: t.stringLiteral("my-module"),
});

generate(ast).code                                          // => var myModule = require('my-module');

@babel/helper-x

Babel 的辅助函数,包含常用操作、测试函数等,内容比较庞杂。

应用层

@babel/cli

在命令行编译。

babel script.js # 输出编译的结果

@babel/standalone

在浏览器编译。如:Babel 官网等会用到。

<div id="input"></div>
<div id="output"></div>
<button id="transform">转换</button>
<!-- 加载 @babel/standalone -->
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script>
document.getElementById('transform').addEventListener('click', function() {
    const input = document.getElementById('input').value;
    const output = Babel.transform(input, { presets: ['es2015'] }).code;
    document.getElementById('output').value = output;
});
</script>

@babel/standalone 也会自动编译和执行 <script type="text/babel"></script><script type="text/jsx"></script> 中的代码。

<div id="output"></div>
<!-- 加载 @babel/standalone -->
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<!-- ES2015 代码会被编译执行 -->
<script type="text/babel">
const getMessage = () => "Hello World";
document.getElementById('output').innerHTML = getMessage();
</script>

@babel/node

提供在命令行执行高级语法的环境。@babel/cli 只转换,不执行,@babel/node 会执行。不适合生产环境使用。

babel-node -e script.js # script.js 里面可以使用高级语法

@babel/register

提供在 Node.js 运行环境内编译和执行高级语法。不适合生产环境使用。

require("@babel/register")();
require("./script.js");                     // script.js 里面可以使用高级语法

常见的语法转换结果

Array.from

// input
Array.from([1, 2, 3])

// output
var _array_from_ = require('@babel/runtime-corejs3/core-js-stable/array/from');
_array_from_([1, 2, 3]);

JSX

// input
<div className="text">{content}</div>

// output
React.createElement('div', { className: 'text' }, content);

class

// input
class Example extends Component { constructor(props) { super(props) } }

// output
var _inherits_ = require('@babel/runtime-corejs3/helpers/interits');
var _class_call_check_ = require('@babel/runtime-corejs3/helpers/classCallCheck');
var _possible_constructor_return_ = require('@babel/runtime-corejs3/helpers/possibleConstructorReturn');
var _get_prototype_of_ = require('@babel/runtime-corejs3/helpers/getPrototypeOf');
var _create_class_ = require('@babel/runtime-corejs3/helpers/createClass');

var Example = function (_Component) {
  _inherits_(Example, _Component);

  function Example(props) {
    _class_call_check_(this, Example);

    return _possible_constructor_return_(this, _get_prototype_of_(Example).call(this, props));
  }

  _create_class_(Example, []);

  return Example;
}(Component);

参考资料

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容

  • 混乱的一天结束了,没有特别的成就感。晚高峰的六号线上,吉米扬小声絮絮叨叨地说着丧气话:吉米扬啊吉米扬,瞅瞅你今天都...
    八表哥殿下阅读 330评论 0 0
  • 她从东莞回家 过年 坐26小时的火车 倒汽车,走山路 下雪了,红梅盛开 山口的雕像是父亲 伸出干枯的手 攥紧女儿 ...
    孟小繁阅读 157评论 0 0
  • 《龙族》里面最喜欢的人就是路明非,他不是富二代,家里也没有钱。他不像恺撒生来便是贵族。也不像楚子航一直有人仰慕。他...
    笕寂阅读 423评论 0 0
  • [图片出自苏海明教练] 为什么说选择呢? 我小时候,我经常听我奶奶说三国,说诸葛亮怎么样怎么样啊!然后对三国的历史...
    李晓兴I积极随缘阅读 437评论 2 0