babel 历史

404 阅读10分钟

历史

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

babel /ˈbeɪbl/ 是巴别塔的意思,来自圣经中的典故:

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

这很符合 babel 的转译器的定位。 17e20660a6ad417ba53b2676824a0e39_tplv-k3u1fbpfcp-no-mark_1280_960_0_0.awebp

babel 的用途

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

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

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

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

一些特定用途的代码转换

babel 是一个转译器,暴露了很多 api,用这些 api 可以完成代码到 AST 的 parse,AST 的转换,以及目标代码的生成,开发者可以用它来来完成一些特定用途的转换

  • 比如函数插桩(函数中自动插入一些代码,例如埋点代码)
  • 自动国际化
  • default import 转 named import 等
  • 现在比较流行的小程序转译工具 taro,就是基于 babel 的 api 来实现的
  • prettier体系中,用于格式化js文件的功能,用的就是babel 的 api
  • 这次bazaar-code-mod的场景
  • ...

代码的静态分析

对代码进行 parse 之后,能够进行转换,是因为通过 AST 的结构能够理解代码。理解了代码之后,除了进行转换然后生成目标代码之外,也同样可以用于分析代码的信息,进行一些检查。

linter 工具

就是分析 AST 的结构,对代码规范进行检查。 最常见的就是 eslint,但是eslint支持除babel parser之外的其他parser,默认的是

api 文档自动生成工具

可以提取源码中的注释,然后生成文档。比如jsdoc

type checker

会根据从 AST 中提取的或者推导的类型信息,对 AST 进行类型是否一致的检查,从而减少运行时因类型导致的错误。

压缩混淆工具

这个也是分析代码结构,进行删除死代码、变量名混淆、常量折叠等各种编译优化,生成体积更小、性能更优的代码。

js 解释器

除了对 AST 进行各种信息的提取和检查以外,我们还可以直接解释执行 AST。也就是在js 运行时,递归遍历ast节点,根据不同的节点类型,执行对应的操作。

babel 转译 esnext 的流程

编译器和转译器

编译的定义就是从一种编程语言转成另一种编程语言。主要指的是高级语言到低级语言。

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

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

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

babel 就是一个 Javascript Transpiler。

babel 的编译流程

整体编译流程分为三步:

  • parse
    • @babel/parser 把源码字符串转成抽象语法树(AST)
    • 推荐 astexplorer.net/ 这个站点查看解析后ast节点
  • transform
    • @babel/transform 调用各种 transform 插件对 AST 节点进行增删改
    • 这里用到了访问器设计模式的思想,每个插件只需要声明自己想要访问的节点,就可以进行对节点的操作。
  • generate
    • @babel/generator 遍历修改后的ast节点,生成目标代码,并生成 sourcemap

为什么会分为这三步

有没有想过,为什么 babel 的编译流程会分 parse、transform、generate 这 3 步呢?

源码是一串按照语法格式来组织的字符串,人能够认识,但是计算机并不认识,想让计算机认识就要转成一种数据结构,通过不同的对象来保存不同的数据,并且按照依赖关系组织起来,这种数据结构就是抽象语法树(abstract syntax tree)。之所以叫抽象语法树是因为数据结构中省略掉了一些无具体意义的分隔符比如 ; { } 等。有了 AST,计算机就能理解源码字符串的意思,而理解是能够转换的前提,所以编译的第一步需要把源码 parse 成 AST。

转成 AST 之后就可以通过修改 AST 的方式来修改代码,这一步会遍历 AST 并进行各种增删改,这一步也是 babel 最核心的部分。

经过转换以后的 AST 就是符合要求的代码,就可以再转回字符串,转回字符串的过程中把之前删掉的一些分隔符再加回来。

简单总结一下就是:为了让计算机理解代码需要先对源码字符串进行 parse,生成 AST,把对代码的修改转为对 AST 的增删改,转换完 AST 之后再打印成目标代码字符串。

这三步都做了什么?

我们知道了为什么 babel 要分为这样的 3 步,那这 3 步具体都做了什么呢?

parse

parse 阶段的目的是把源码字符串转换成机器能够理解的 AST,这个过程分为词法分析、语法分析。

比如 let name = 'guang'; 这样一段源码,我们要先把它分成一个个不能细分的单词(token),也就是 letname='guang',这个过程是词法分析,按照单词的构成规则来拆分字符串。

之后要把 token 进行递归的组装,生成 AST,这个过程是语法分析,按照不同的语法结构,来把一组单词组合成对象。

transform

transform 阶段是对 parse 生成的 AST 的处理,会进行 AST 的遍历,遍历的过程中处理到不同的 AST 节点会调用注册的相应的 visitor 函数,visitor 函数里可以对 AST 节点进行增删改,返回新的 AST(可以指定是否继续遍历新生成的 AST)。这样遍历完一遍 AST 之后就完成了对代码的修改。

generate

generate 阶段会把 AST 打印成目标代码字符串,并且会生成 sourcemap。不同的 AST 对应的不同结构的字符串。比如 IfStatement 就可以打印成 if(test) {} 格式的代码。这样从 AST 根节点进行递归打印,就可以生成目标代码的字符串。

sourcemap 记录了源码到目标代码的转换关系,通过它我们可以找到目标代码中每一个节点对应的源码位置。

AST 介绍

babel 编译的第一步是把源码 parse 成抽象语法树 AST (Abstract Syntax Tree),后续对这个 AST 进行转换。(之所以叫抽象语法树是因为省略掉了源码中的分隔符、空格、colon、comma、semicolon等内容)

AST 也是有标准的,JS parser 的 AST 大多是 estree 标准,从 SpiderMonkey 的 AST 标准扩展而来。babel 的整个编译流程都是围绕 AST 来的,这一节我们来学一下 AST。

@babel/parsr 解析后的ast百分之九九符合estree标准,就少数几个不符和,babeljs.io/docs/en/bab…

熟悉了 AST,也就是知道转译器和 JS 引擎是怎么理解代码的,对深入掌握 Javascript 也有很大的好处。

AST 的公共属性

每种 AST 都有自己的属性,但是它们也有一些公共属性:

  • type: AST 节点的类型
  • start、end:start 和 end 代表该节点对应的源码字符串的开始和结束下标,不区分行列。
  • loc: 是一个对象,有 line 和 column 属性分别记录开始和结束行列号。
  • leadingComments、innerComments、trailingComments: 表示开始的注释、中间的注释、结尾的注释,因为每个 AST 节点中都可能存在注释,而且可能在开始、中间、结束这三种位置,通过这三个属性来记录和 Comment 的关联。
  • extra:记录一些额外的信息,用于处理一些特殊情况。

AST 可视化查看工具

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

这个网站可以查看代码 parse 以后的结果,但是如果想查看全部的 AST 可以在babel parser 仓库里的 AST 文档里查,或者直接去看 @babel/types 的 typescript 类型定义

常见的 AST 节点

AST 是对源码的抽象,字面量、标识符、表达式、语句、模块语法、class 语法都有各自的 AST。

File & Comment

babel 的 AST 最外层节点是 File

  • program 最重要的节点,存放整个程序代码的节点
  • comments 用于存放整个文件的所有的注释节点
  • tokens 用于存放parser词法分析的结果

注释分为块注释和行内注释,对应 CommentBlock 和 CommentLine 节点。

Program & directive

program 是代表整个程序的节点,有如下重要属性

  • sourceType: 表示文件代码的类型,有 module 和 script 两种,一般都是module。不同的类型下,有些节点是不允许出现的,比如在script类型下,代码中不能出现 import 和 export 的语法。
  • body 属性代表程序体,存放 statement 数组,就是具体执行的语句的集合。
  • directives 存放 Directive 节点数组,比如"use strict" 这种指令会使用 Directive 节点表示。

Literal

Literal 是字面量的意思,比如 let name = 'xxx'中,'xxx'就是一个字符串字面量 StringLiteral;相应的还有数字字面量 NumericLiteral,布尔字面量 BooleanLiteral,字符串字面量 StringLiteral,正则表达式字面量 RegExpLiteral,BigIntLiteral,NullLiteral等

Identifier

Identifer 是标识符的意思,变量名、属性名、参数名等各种声明和引用的名字,都是Identifer。 JS 中的标识符只能包含字母或数字或下划线或美元符号$,且不能以数字开头。这是 Identifier 的词法特点。

题外话:通过eslint可以配置id-match规则,通过指定正则字符串,达到自定义效果。比如 'id-match': ['error', '^[A-Za-z_$]\\w*(?<![0-9]{3,})$'], 结尾的最后三个字符不能都是数字。

下面这段代码中,哪些是Identifier

const name = 'xxx';
function say(name) { 
    console.log(name); 
} 
const obj = { name: 'xxx' }

Statement

statement 是语句,它是可以独立执行的单位,比如 break、continue、debugger、return 或者 if 语句、while 语句、for 语句,还有声明语句,表达式语句等。我们写的每一条可以独立执行的代码都是语句。

语句末尾一般会加一个分号分隔,或者用换行分隔。

下面这些我们经常写的代码,每一行都是一个 Statement:

debugger; 
throw Error();

function say(name) {
  console.log(name);
  return;
}

try {} catch(e) {} finally{} 

for (let key in obj) {
  break;
  continue;
}

for (let i = 0; i < 10; i ++) {}
for (let key of obj) {}
while (true) {} 
do {} while (true) 
switch (v) {
    case 1: break;
    default: ;
} 

Declaration

声明语句是一种特殊的语句,它执行的逻辑是在作用域内声明一个变量。

比如下面这些声明语句:

const a = 1;
let b = 123;
function b() {}
class C {}

import d from 'e';
export default a;
export { a };
export * from 'e';

Expression

表达式,特点是执行完以后有返回值,这是和语句 (statement) 的区别。

[1,2,3]
-1
a = 11 + 2
() => {}
function(){}
class{}
a
this
super
a::b

细心的同学可能会问 identifier 和 super 怎么也是表达式呢?

其实有的节点可能会是多种类型,identifier、super 有返回值,符合表达式的特点,所以也是 expression。

ExpressionStatement

表达式语句,代表这个表达式是被当成语句执行的,也就是在Expression节点外面包裹一层 ExpressionStatement 节点,所以就可以作为语句单独执行了。

我们判断 AST 节点是不是某种类型要看它是不是符合该种类型的特点,比如语句的特点是能够单独执行,表达式的特点是有返回值。

有的表达式可以单独执行,符合语句的特点,所以也是语句,比如赋值表达式、数组表达式等,不会报错;

[1,2,3];
a = 11 + 2;

但有的表达式不能单独执行,比如匿名函数表达式和匿名class 表达式单独执行会报错

function(){};
class {}

如果想不报错,需要和其他部分一起构成一条语句,比如组成赋值语句

a = function() {}
console.log(class {});

class

class 的语法也有专门的 AST 节点来表示。

整个 class 的内容是 ClassBody,属性是 ClassProperty,方法是ClassMethod(通过 kind 属性来区分是 constructor 还是 method)。

ClassDeclaration

Class AA {};

ClassExpression

const a = class {}