Skip to content

babel 工作原理及插件机制 #5

@Weiting-Zhang

Description

@Weiting-Zhang

Babel 是一个 JavaScript 编译器,可以将目前浏览器不支持的 JS 语法或者像 JSX,Flow 这种对 JS 的扩展编译成兼容性良好的代码。之前自己只是停留在学会配置上,这篇文章将试着通过一个简单的 babel 插件的实现理解一下 babel 的工作机制。

一、编译器

维基百科对编译器的解释是将用某种编程语言写成的源代码(原始语言),转换成另一种编程语言(目标语言)。从架构上来分通常可分为前端(front end)、中端(middle end)、后端(back end)。
2880px-compiler_design svg

  • 前端:针对特定的源代码进行语法和语义检查,并最终生成一个源代码的中间层表示(IR,intermediate representation )。编译器前端通常又可以拆成三部分来看:词法分析(lexical analysis or lexing/tokenization), 语法分析(syntax analysis or scanning/parsing), 和语义分析( semantic analysis)。词法分析是将源程序转化为标记序列,包括扫描阶段和计算阶段;语法分析则解析转化好的标记序列并构建出解析树;语义分析则负责把语义信息加到构建好的解析树中,生成符号表,进行类型检查等

xxx_scanner_and_parser_example_for_c

  • 中端:分析(Analysis)和优化(Optimization)
  • 后端:针对特定机器的优化(Machine dependent optimizations)和目标代码生成(Code generation)

二、babel 的工作过程

1. 总览

babel 对 JS 的转化也类似,它将源码转换 AST 之后(前端),通过遍历 AST树,对树做一些修改(中端),然后再将 AST 转成 目标代码(后端)。

babel

2. Parse

babel 使用的解析器是 babylon,fork 自 acorn,babylon 现已经迁移到 babel 的 packages 中的 babel-parser 进行管理,可以将一段 JS code 解析为一个 AST(抽象语法树),是 babel-core 的核心依赖之一,使用方法如下:

// 将代码解析成 AST
const babylon = require('babylon')
const ast = babylon.parse('const a = 3; a + 5');
console.log(ast);

结果如下(program 属性是生成的 AST):

image

形象一点的 AST 如下:
ast

所有的 AST 都会以 Pragram 作为根节点,包含程序中所有的顶级语句。上面的例子中只有两个:

  • 一个 VariableDeclaration ,由一个 VariableDeclaratorNumericLiteral "3" 赋值给 Identifier "a"
  • 一个 ExpressionStatement,由一个 BinaryExpression 组成,表示 了Identifier "a",operator "+" 和另一个 NumericLiteral "5".

babel 关于 AST 的详细定义可见:babel-parser 。现实中的 AST 组成通常相当复杂,可以通过 https://astexplorer.net/ 这个 AST 直出工具来帮助自己理解一段程序解析出的 AST。

3. transform & generate

babylon 生成 AST 之后,便可以通过遍历 AST 做一些 transform,babel 遍历 AST 使用的是 babel-traverse ,使用方法如下:

const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const generate = require('@babel/generator').default

const code = `function square(n) {
  return n * n
}`;
const ast = parser.parse(code);
traverse(ast, {
  enter(path) {
    if (path.isIdentifier({ name: "n" })) {
      path.node.name = "x"
    }
  }
});

// 代码重新生成
const output = generate(ast)
console.log(output.code)

通过节点的遍历可以操作生成的 AST,在遍历一个节点时,存在 enterexit 两个时刻,enter 是在进入节点的回调函数,这时节点的子节点还没触达;exit 是在离开节点时,这时遍历子节点已完成。访问节点可以通过 pathpath 不等同于节点,其中重要的属性和方法如下:
path

上面的例子中使用 path 的 isIdentifier 判断节点性质和名称(这些构造、验证以及变换 AST 节点的方法其实来源于 babel-types ) ,并修改了节点的属性,通过 traverse 后的 AST 所有之前 name 为 "n" 的 Identifier 都会变成 "x",如上所示,babel 将 AST 生成目标代码的工具是 babel/generator ,重新生成 code 的结果如下:

- function square(n) {
-     return n * n
- }
+ function square(x) {
+     return x * x
+ }

三. 实现一个简易的 babel 插件

babel 的一个插件即是定义如何转换当前的一个节点,可以参考官方文档中提供的一个示例,这个插件的作用是将所有 Identifier 的名称逆转:

// my-plugin.js
module.exports = function () {
  return {
    visitor: {
      Identifier(path) {
        const name = path.node.name;
        // reverse the name: JavaScript -> tpircSavaJ
        path.node.name = name.split("").reverse().join("");
      }
    }
  };
}

插件需要返回的是一个 vistor 对象定义访问遍历 AST 时的 node 节点的操作,之所以使用这个术语是遵循访问者模式 的概念。 vistor 中的每个函数接受两个参数,pathstate 。编写代码验证插件的作用:

// index.js
const myPlugin = 'hello world';
// .babelrc
{
  "plugins": [
    [
      "./my-plugin"
    ]
  ]
}

命令行执行:

babel index.js

输出:

const nigulPym = 'hello world';

可以看到自定义的 my-pluginIdentifier 的名称逆转了。这是一个极简的示例,详细的如何对 AST 进行访问、转化操作可以参考 babel-handbook

四、参考链接/拓展阅读:

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions