前端编译原理——Babel 篇

2,536 阅读8分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 2 天,点击查看活动详情

Babel 的出身

babel 最开始叫 6to5,顾名思义是 es6 转 es5,但是后来随着 es 标准的演进,有了 es7、es8 等, 6to5 的名字已经不合适了,所以改名为了 babel。

babel 是巴别塔的意思,来自圣经中的典故:

当时人类联合起来兴建希望能通往天堂的高塔,为了阻止人类的计划,上帝让人类说不同的语言,使人类相互之间不能沟通,计划因此失败,人类自此各散东西。此事件,为世上出现不同语言和种族提供解释。这座塔就是巴别塔。

这很符合 babel 的转译器的定位。所以就命名为 Babel。

Babel 的用途

我们平时主要用 babel 来做 3 种事情:

1、转译 esnext、typescript、flow 等到目标环境支持的 js

用来把代码中的 esnext 的新的语法、typescript 和 flow 的语法转成基于目标环境支持的语法的实现。并且还可以把目标环境不支持的 api 进行 polyfill。

babel7 支持了 preset-env,可以指定 targets 来进行按需转换,转换更加的精准,产物更小。

2、一些特定用途的代码转换

babel 是一个转译器,暴露了很多 api,用这些 api 可以完成代码到 AST 的 parse,AST 的转换,以及目标代码的生成。

开发者可以用它来来完成一些特定用途的转换,比如函数插桩(函数中自动插入一些代码,例如埋点代码)、自动国际化等。

3、代码的静态分析

除了进行转换然后生成目标代码之外,也同样可以用于分析代码的信息,进行一些检查

  • linter 工具就是分析 AST 的结构,对代码规范进行检查。
  • api 文档自动生成工具,可以提取源码中的注释,然后生成文档。
  • type checker 会根据从 AST 中提取的或者推导的类型信息,对 AST 进行类型是否一致的检查,从而减少运行时因类型导致的错误。
  • 压缩混淆工具,这个也是分析代码结构,进行删除死代码、变量名混淆、常量折叠等各种编译优化,生成体积更小、性能更优的代码。
  • js 解释器,除了对 AST 进行各种信息的提取和检查以外,我们还可以直接解释执行 AST。

Babel 的编译流程

一般编译器(Compiler) 是指高级语言到低级语言的转换工具,特殊的,高级语言到高级语言的转换工具,被叫做转换编译器,简称转译器 (Transpiler)。

高级语言:有很多用于描述逻辑的语言特性,比如分支、循环、函数、面向对象等,接近人的思维,可以让开发者快速的通过它来表达各种逻辑。比如 c++、javascript。

低级语言:与硬件和执行细节有关,会操作寄存器、内存,具体做内存与寄存器之间的复制,需要开发者理解熟悉计算机的工作原理,熟悉具体的执行细节。比如汇编语言、机器语言。

babel 就是一个 Javascript Transpiler。

babel 编译流程.png

Babel 的 AST

什么是 AST?

通过不同的对象来保存不同的数据,并且按照依赖关系组织起来,这种数据结构就是抽象语法树(abstract syntax tree)。之所以叫抽象语法树是因为数据结构中省略掉了一些无具体意义的分隔符比如 ; { } 等。

AST 常见的语法

字面量、标识符、表达式、语句、模块语法、class 语法。

AST 的公共属性

  • type: AST 节点的类型
  • leadingComments、innerComments、trailingComments: 表示开始的注释、中间的注释、结尾的注释。因为每个 AST 节点中都可能存在注释,而且可能在开始、中间、结束这三种位置,通过这三个属性来记录和 Comment 的关联。
  • extra:记录一些额外的信息,用于处理一些特殊情况。

AST 可视化查看工具

当然,我们并不需要记什么内容对应什么 AST 节点,可以通过 axtexplorer.net 这个网站来直观的查看。

image.png

Babel 的 api

babel 的编译流程分为三步:parse、transform、generate,每一步都暴露了一些 api 出来:

  • parse 阶段有 @babel/parser,功能是把源码转成 AST
  • transform 阶段有 @babel/traverse,可以遍历 AST,并调用 visitor 函数修改 AST,修改 AST 自然涉及到 AST 的判断、创建、修改等,这时候就需要 @babel/types 了,当需要批量创建 AST 的时候可以使用 @babel/template 来简化 AST 创建逻辑
  • generate 阶段会把 AST 打印为目标代码字符串,同时生成 sourcemap,需要 @babel/generate 包

另外:

  • 中途遇到错误想打印代码位置的时候,使用 @babel/code-frame 包
  • babel 的整体功能通过 @babel/core 提供,基于上面的包完成 babel 整体的编译流程,并实现插件功能

@babel/parser

babel parser 叫 babylon,是基于 acorn 实现的,扩展了很多语法,可以支持 esnext、jsx、flow、typescript 等语法的解析,其中 jsx、flow、typescript 这些非标准的语法的解析需要指定语法插件。

require("@babel/parser").parse("code", {
    sourceType: “module”,
    plugins: [
        "jsx",
        "typescript"
    ]
});

@babel/traverse

遍历 AST,并在遍历过程中调用 visitor 函数进行 AST 的增删改,提供了 path 的 api

traverse(ast, {
    visitor: {
        Identifier (path, state) {},
        StringLiteral: {
            enter (path, state) {},
            exit (path, state) {}
        }
    }
})

image.png

// 进入 FunctionDeclaration 节点时调用
traverse(ast, {
    FunctionDeclaration: {
        enter(path, state) {}
    }
})

// 默认是进入节点时调用,和上面等价
traverse(ast, {
    FunctionDeclaration(path, state) {}
})

// 进入 FunctionDeclaration 和 VariableDeclaration 节点时调用
traverse(ast, {
    'FunctionDeclaration|VariableDeclaration'(path, state) {}
    })

// 通过别名指定离开各种 Declaration 节点时调用
traverse(ast, {
    Declaration: {
        exit(path, state) {}
    }
})

image.png

path 是遍历过程中的路径,会保留上下文信息,有很多属性和方法,比如:

  • path.node 指向当前 AST 节点
  • path.parent 指向父级 AST 节点
  • path.getSibling、path.getNextSibling、path.getPrevSibling 获取兄弟节点
  • path.get、path.set 获取和设置当前节点属性的 path

上述属性和方法是获取当前节点以及它的关联节点的。

  • path.scope 获取当前节点的作用域信息

上述属性可以获取作用域的信息。

  • path.isXxx 判断当前节点是不是 Xxx 类型
  • path.assertXxx 判断当前节点是不是 Xxx 类型,不是则抛出异常

上述 isXxx、assertXxx 系列方法可以用于判断 AST 类型。

  • path.insertBefore、path.insertAfter 插入节点
  • path.replaceWith、path.replaceWithMultiple、replaceWithSourceString 替换节点
  • path.remove 删除节点

上述方法可以对 AST 进行增删改。

  • path.skip 跳过当前节点的子节点的遍历
  • path.stop 结束后续遍历

上述方法可以跳过一些遍历。

@babel/types

遍历 AST 的过程中需要创建一些 AST 和判断 AST 的类型,这时候就需要 @babel/types 包。

t.ifStatement(test, consequent, alternate);
t.isIfStatement(node, opts);
t.assertIfStatement(node, opts);
t.isIdentifier(node, { name: "paths" }

@babel/template

通过 @babel/types 创建 AST 还是比较麻烦的,要一个个的创建然后组装,如果 AST 节点比较多的话需要写很多代码,这时候就可以使用 @babel/template 包来批量创建。

const ast = template(code, [opts])(args);
const ast = template.ast(code, [opts]);
const ast = template.program(code, [opts]);
const ast = template.expression(code, [opts]);
const ast = template.statement(code, [opts]);

const fn = template(`console.log(NAME)`);

const astArr = template.statements(code, [opts]);
const ast = fn({
    NAME: t.stringLiteral("guang"),
});

@babel/generator

AST 转换完之后就要打印成目标代码字符串,通过 @babel/generator 包的 api。

const { code, map } = generate(ast, { sourceMaps: true })

@babel/code-frame

当有错误信息要打印的时候,需要打印错误位置的代码,可以使用 @babel/code-frame。

const { codeFrameColumns } = require("@babel/code-frame");

try {
    throw new Error("xxx 错误");
} catch (err) {
    console.error(codeFrameColumns(`const name = guang`, {
        start: { line: 1, column: 14 }
    }, {
        highlightCode: true,
        message: err.message
    }));
}

@babel/core

@babel/core 包则是基于前面的包完成整个编译流程,从源码到目标代码,生成 sourcemap

transformSync(code, options); // => { code, map, ast }
transformFileSync(filename, options); // => { code, map, ast }
transformFromAstSync(
    parsedAst,
    sourceCode,
    options
); // => { code, map, ast }

transformAsync("code();", options).then(result => {})
transformFileAsync("filename.js", options).then(result => {})
transformFromAstAsync(parsedAst, sourceCode, options).then(result => {})

Version:0.9 StartHTML:0000000105 EndHTML:0000007702 StartFragment:0000000141 EndFragment:0000007662

小结

  • @babel/parser 对源码进行 parse,可以通过 plugins、sourceType 等来指定 parse 语法
  • @babel/traverse 通过 visitor 函数对遍历到的 ast 进行处理,分为 enter 和 exit 两个阶段,具体操作 AST 使用 path 的 api,还可以通过 state 来在遍历过程中传递一些数据
  • @babel/types 用于创建、判断 AST 节点,提供了 xxx、isXxx、assertXxx 的 api
  • @babel/template 用于批量创建节点
  • @babel/code-frame 可以创建友好的报错信息
  • @babel/generator 打印 AST 成目标代码字符串,支持 comments、minified、sourceMaps 等选项。
  • @babel/core 基于上面的包来完成 babel 的编译流程,可以从源码字符串、源码文件、AST 开始。

总结

  • babel 提供了 parse、transform、generate 的 api,我们可以基于 babel 插件来做一些代码转换或静态分析的功能
  • babel 可以支持自动国际化、自动埋点、自动生成文档(对业务有价值)
  • linter、type checker、压缩混淆(深入理解一些前端工具)
  • js 解释器(理解 js 引擎)
  • 入门编译原理

编译的本质是什么? 转换,转换有主要分为:

编译:从高级语言转成汇编语言,再到机器语言(cpu 指令集) 解释:先基于机器语言实现对一种中间代码(字节码/ast)的解释执行,然后只要把源码编译到这种中间代码就行 转译:源码转源码

前端领域主要涉及到转译(babel、tsc、terser等),解释(js 引擎),编译(wasm)