Babel分享

1,629 阅读9分钟

babel的介绍

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

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 来实现的。

代码的静态分析

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

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

Babel 转译流程

babel 是 source to source 的转换,整体编译流程分为三步:

  • parse:通过 parser 把源码转成抽象语法树(AST)
  • transform:遍历 AST,调用各种 transform 插件对 AST 进行增删改
  • generate:把转换后的 AST 打印成目标代码,并生成 sourcemap

image.png

这三步都做了什么?

parse阶段

parse 阶段的目的是把源码字符串转换成机器能够理解的 AST,这个过程分为词法分析、语法分析。 比如 let name = 'guang'; 这样一段源码,我们要先把它分成一个个不能细分的单词(token),也就是 letname='guang',这个过程是词法分析,按照单词的构成规则来拆分字符串成单词。

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

image.png

transform阶段

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

image.png

generate阶段

generate 阶段会把 AST 打印成目标代码字符串,并且会生成 sourcemap。不同的 AST 对应的不同结构的字符串。

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

Abstract Syntax Tree

常见的 AST 节点

1: Literal

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

image.png 代码中的字面量很多,babel 就是通过 xxLiteral 来抽象这部分内容的。

2: Identifier

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

来尝试分析一下,下面这一段代码里面有多少 Identifier?

const 「name」 = 'haha'; function「say」(「name」) { console.log(「name」); } const 「obj」 = { 「name」: 'guang' }

3:Statement

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

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

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

for (let key in obj) {} forInStatement

{} blockStatement

label: console.log(); LabeledStatement

4:Declaration

声明语句是一种特殊的语句,它执行的逻辑是在作用域内声明一个变量、函数、class、import、export 等。

const a = 1; VariableDeclaration

function b(){} FunctionDeclaration

class C {} ClassDeclaration

import d from 'e'; ImportDeclaration

5:Expression

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

[1,2,3] ArrayExpression

a = 1 AssignmentExpression

1 + 2 BinaryExpression 二元表达式

function(){} FunctionExpression

6:Class

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

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

class HAHA extends Person{ name = 'haha'; constructor() {} eat() {} }

class HAHA extends Person{
    name = 'haha';
    constructor() {}
    eat() {}
}

image.png

Babel 的 api的详解

1:@babel/parser

babel parser 叫 babylon,可以支持 es next(现在支持到 es2020)、jsx、flow、typescript 等语法的解析,其中 jsx、flow、typescript 这些非标准的语法的解析需要指定语法插件。

它提供了有两个 api:parse 和 parseExpression。两者都是把源码转成 AST,不过 parse 返回的 AST 根节点是 File(整个 AST),parseExpression 返回的 AST 根节点是是 Expression(表达式的 AST),粒度不同。

function parse(input: string, options?: ParserOptions): File function 
parseExpression(input: string, options?: ParserOptions): Expression

parse 的内容是什么:

  • plugins: 指定jsx、typescript、flow 等插件来解析对应的语法
  • sourceType: 指定是否支持解析模块语法,有 module、script、unambiguous 3个取值,module 是解析 es module 语法,script 则不解析 es module 语法,当作脚本执行,unambiguous 则是根据内容是否有 import 和 export 来确定是否解析 es module 语法。

其实最常用的 option 就是 plugins、sourceType 这两个,比如要 parse tsx 模块,那么就可以这样来写

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

parse 出的 AST 由 @babel/traverse 来遍历和修改,babel traverse 包提供了 traverse 方法:

function traverse(parent, opts)

常用的就前面两个参数,parent 指定要遍历的 AST 节点,opts 指定 visitor 函数。babel 会在遍历 parent 对应的 AST 时调用相应的 visitor 函数。

遍历过程

visitor 对象的 value 是对象或者函数:

  • 如果 value 为函数,那么就相当于是 enter 时调用的函数。
  • 如果 value 为对象,则可以明确指定 enter 或者 exit 时的处理函数。
visitor: { 
    Identifier (path, state) {}, 
    StringLiteral: { 
        enter (path, state) {}, 
        exit (path, state) {}
    } 
}

enter 时调用是在遍历当前节点的子节点前调用,exit 时调用是遍历完当前节点的子节点后调用。

可以为单个节点的类型,也可以是多个节点类型通过 | 连接,还可以通过别名指定一系列节点类型。

// 进入 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) {} } 
})

具体的别名有哪些在babel-types 的类型定义可以查。

path参数

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

  • path.node 指向当前 AST 节点

  • path.get、path.set 获取和设置当前节点属性的 path

  • path.parent 指向父级 AST 节点

  • path.getSibling、path.getNextSibling、path.getPrevSibling 获取兄弟节点

  • path.find 从当前节点向上查找节点 这些属性和方法是获取当前节点以及它的关联节点的

  • path.insertBefore、path.insertAfter 插入节点

  • path.replaceWith、path.replaceWithMultiple、replaceWithSourceString 替换节点

  • path.remove 删除节点 这些方法可以对 AST 进行增删改

  • path.skip 跳过当前节点的子节点的遍历

  • path.stop 结束后续遍历 这俩方法可以跳过一些遍历

state参数

第二个参数 state 则是遍历过程中在不同节点之间传递数据的机制,插件会通过 state 传递 options 和 file 信息,我们也可以通过 state 存储一些遍历过程中的共享数据。

@babel/types

遍历 AST 的过程中需要创建一些 AST 和判断 AST 的类型,这时候就需要 @babel/types 包。 举例来说,如果要创建IfStatement就可以调用

t.ifStatement(test, consequent, alternate);

而判断节点是否是 IfStatement 就可以调用 isIfStatement 或者 assertIfStatement

t.isIfStatement(node, opts);
t.assertIfStatement(node, opts);

opts 可以指定一些属性是什么值,增加更多限制条件,做更精确的判断。

t.isIdentifier(node, { name: "paths" })

isXxx 会返回 boolean 表示结果,而 assertXxx 则会在类型不一致时抛异常。

@babel/template

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

这个包有这些 api:

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

如果是根据模版创建整个 AST,那么用 template.ast 或者 template.program 方法,这俩都是直接返回 ast 的,template.program 返回的 AST 的根节点是 Program。

如果模版中有占位符,那么就用 template 的 api,在模版中写一些占位的参数,调用时传入这些占位符参数对应的 AST 节点。

const fn = template(`console.log(NAME)`); 
const ast = fn({ 
    NAME: t.stringLiteral("ha")
});

或者

const fn = template(`console.log(%%NAME%%)`);
const ast = fn({
    NAME: t.stringLiteral("ha")
});

@babel/generator

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

function (ast: Object, opts: Object, code: string): {code, map}

options 中常用的是 sourceMaps,开启了这个选项才会生成 sourcemap

const { code, map } = generate(ast, { sourceMaps: true })
格式化输出的选项:
nametypedefaultdescription
commentsbooleantrueShould comments be included in output
minifiedbooleanfalseShould the output be minified
sourceMapsbooleanfalseEnable generating source maps

@babel/code-frame

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

const result = codeFrameColumns(rawLines, location, { /* options */ });

options 可以设置 highlighted (是否高亮)、message(展示啥错误信息)。