前言
babel在我们工作中必不可少,但是很多情况下项目都继承了,大部分同学进知道简单的配置,其中的一些代码编译流程及原理,ast源码树等不是很清楚,本文是haorooms前端博客babel代码编译流程系列课程,本套课程主要记录ast源码解析,babel编译流程,babel应用及插件书写等等。
babel中的ast
babel 编译的第一步是把源码 parse 成抽象语法树 AST (Abstract Syntax Tree),后续对这个 AST 进行转换。(之所以叫抽象语法树是因为省略掉了源码中的分隔符、注释等内容)
AST 也是有标准的,JS parser 的 AST 大多是 estree 标准,从 SpiderMonkey 的 AST 标准扩展而来。babel 的整个编译流程都是围绕 AST 来的。
常见的 AST 节点
1、Literal 是字面量的意思,比如 let name = 'haorooms'中,'haorooms'就是一个字符串字面量 StringLiteral,相应的还有数字字面量 NumericLiteral,布尔字面量 BooleanLiteral,字符串字面量 StringLiteral,正则表达式字面量 RegExpLiteral 等。
2、Identifer 是标识符的意思,变量名、属性名、参数名等各种声明和引用的名字,都是Identifer。我们知道,JS 中的标识符只能包含字母或数字或下划线(“_”)或美元符号(“$”),且不能以数字开头。这是 Identifier 的词法特点。
例如下面代码:
const name = 'haorooms';
function say(name) {
console.log(name);
}
const obj = {
name: 'haorooms'
}
name,say,obj包括传入参数name等都是Identifer。
3、statement 是语句,它是可以独立执行的单位,比如 break、continue、debugger、return 或者 if 语句、while 语句、for 语句,还有声明语句,表达式语句等。我们写的每一条可以独立执行的代码都是语句。
如下都是statement
break;//breakStatement
continue;//continueStatement
return;//returnStatement
debugger;
throw Error();
{}
try {} catch(e) {} finally{}
for (let key in obj) {}
for (let i = 0;i < 10;i ++) {}
while (true) {}
do {} while (true)
switch (v){case 1: break;default:;}
label: console.log();
with (a){}
4、Declaration声明语句是一种特殊的语句,它执行的逻辑是在作用域内声明一个变量、函数、class、import、export 等。
比如下面这些声明语句:
const a = 1; // variableDeclaration
function b(){} // FunctionDeclaration
class C {} // classDeclaration
import d from 'e'; //importDeclaration
export default e = 1; //exportDefaultDeclaration
export {e};// exportNamedDeclaration
export * from 'e';// exportAllDeclaration
5、expression 是表达式,特点是执行完以后有返回值,这是和语句 (statement) 的区别。
下面是一些常见的表达式:
[1,2,3]
a = 1
1 + 2;
-1;
function(){};
() => {};
class{};
a;
this;
super;
a::b;
6、program 是代表整个程序的节点,它有 body 属性代表程序体,存放 statement 数组,就是具体执行的语句的集合。还有 directives 属性,存放Directive 节点,比如"use strict" 这种指令会使用 Directive 节点表示。
7、File & Comment 是babel 的 AST 最外层节点是 File,它有 program、comments、tokens 等属性,分别存放 Program 程序体、注释、token 等,是最外层节点。
/**haorooms bable **/ commnetBlock
// haorooms bable commentline
注释分为块注释和行内注释,对应 CommentBlock 和 CommentLine 节点。
babel 的编译流程
babel 是 source to source 的转换,整体编译流程分为三步:
parse:通过 parser 把源码转成抽象语法树(AST)
transform:遍历 AST,调用各种 transform 插件对 AST 进行增删改
generate:把转换后的 AST 打印成目标代码,并生成 sourcemap
babel的api
@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 小案例
讲完了 babel 的编译流程、AST、api 之后,我们已经可以做一些有趣的事情了。
我们经常会打印一些日志来辅助调试,但是有的时候会不知道日志是在哪个地方打印的。希望通过 babel 能够自动在 console.log 等 api 中插入文件名和行列号的参数,方便定位到代码。
首先把 parse、transform、generate 的框架搭好:
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const generate = require('@babel/generator').default;
const sourceCode = `console.log(1);`;
const ast = parser.parse(sourceCode, {
sourceType: 'unambiguous'
});
traverse(ast, {
CallExpression(path, state) {
}
});
const { code, map } = generate(ast);
console.log(code);
parser 需要指定代码是不是包含 import、export 等,需要设置 moduleType 为 module 或者 script,我们干脆设置为 unambiguous,让它根据内容是否包含 import、export 来自动设置 moduleType。
搭好框架之后,我们先设计一下要转换的代码:
const ast = parser.parse(sourceCode, {
sourceType: 'unambiguous',
plugins: ['jsx']
});
const targetCalleeName = ['log', 'info', 'error', 'debug'].map(item => `console.${item}`);
traverse(ast, {
CallExpression(path, state) {
const calleeName = generate(path.node.callee).code;
if (targetCalleeName.includes(calleeName)) {
const { line, column } = path.node.loc.start;
path.node.arguments.unshift(types.stringLiteral(`filename: (${line}, ${column})`))
}
}
});
小结
本节babel就先写到这里,后面会写babel插件如何书写,及更详细的babel教程,欢迎关注收藏haorooms前端博客。