前言
不管是浏览器解析JS代码
的还是适配浏览器的兼容性、开发各种打包插件,基本都离不开babel
的身影,babel
的原理其实没有我们想象中的的那么复杂,只是它做的事情比较繁多和繁杂,原理还是挺简单的,下面,我们一一来梳理一下babel的原理
Babel是什么
babel 最开始叫 6to5,顾名思义是 es6 转 es5,但是后来随着 es 标准的演进,有了 es7、es8 等, 6to5 的名字已经不合适了,所以改名为了 babel。
babel实际上就是转译器,它可以将ESnext、Typescript、flow转换成浏览器所能支持的JS代码,它也可以做一些静态分析和代码转换,比如type check和js解释器,现在最流行的taro就是通过修改babel转换后的AST来来实现的。
Babel探究Babel的原理
babel将代码转换成AST抽象语法树后会暴露出很多api,这些api可以让让我们操作这个AST抽象语法树,从而实现我们的需求。
babel刚开始会将需要转译的代码进行语法分析和词法分析,所谓的词法分析就是,例如 let name =1in,把这段代码拆分成 let、name、=、1in,就是词法分析,把代码拆分成不可再拆分的token,就是词法分析,所谓的语法分析就是通过拆分后的token组成AST抽象语法树,就是语法分析。
随后,在转换的AST的抽象语法树中,我们可以通过babel暴露出来的api操纵AST抽象语法树,修改AST抽象语法树,生成新的抽象语法树,然后我们在通过新的抽象语法树去生成新的代码,这就是babel的转换原理。
babel转译的每个阶段都有对应的packge,我们只需要下载对应的packge引用api就可以根据自己的需求实现转译。
关于babel的api和ast抽象语法树的类型分别有哪些和描述,大家可以通过babel官网去查看,这里就不过多赘述
下面,我们做一个小案例,来体验一下babel的厉害之处。
我们在代码中经常使用console.log去调试代码,但是我们一旦console.log过多的时候,就不知道console.log是在哪一行输出的,所以我们可以用babel来帮助我们记录一下console.log是在哪一行的。
js
//也就是 转译前
console.log(111)
//转译后
console.log('line column',111)
明白需求后,我们动手。
我们先使用astexplorer.net/来看一下console.log的抽象语法树是什么样子的。
我们可以看到,函数表达式的AST属于CallExression。
那我们要做的是在遍历 AST 的时候对 console.log、console.info 等 api 自动插入一些参数,也就是要通过 visitor 指定对 CallExpression 的 AST 做一些修改。
CallExrpession 节点有两个属性,callee 和 arguments,分别对应调用的函数名和参数, 所以我们要判断当 callee 是 console.xx 时,在 arguments 的数组中中插入一个 AST 节点。
js
const parser = require('@babel/parser');
// 因为 `@babel/parser` 等包都是通过 es module 导出的,
// 所以通过 commonjs 的方式引入有的时候要取 default 属性。
const traverse = require('@babel/traverse').default;
const generate = require('@babel/generator').default;
const types = require('@babel/types');
const ast = parser.parse(sourceCode, {
//这里有这个选项是因为我们不知道使用ES module导出的还是commonjs导出的,所以我们用
// unambiguous让它自己判断
sourceType: 'unambiguous',
// 因为可能会用到jsx语法,所以这里引入jsx插件
plugins: ['jsx']
});
const targetCalleeName = ['log', 'info', 'error', 'debug'].map(item => `console.${item}`);
traverse(ast, {
CallExpression(path, state) {
//通过path.node.callee拿到调用的函数名,然后用genrate的code把它转换成字符串
const calleeName = generate(path.node.callee).code;
if (targetCalleeName.includes(calleeName)) {
//从path.node.loc.start中拿到行号和列号
const { line, column } = path.node.loc.start;
path.node.arguments.unshift(types.stringLiteral(`filename: (${line}, ${column})`))
}
}
});
以上就是babel利用AST抽象语法树转译的原理,原理其实很简单,不接触的话,是会有些恐惧。复杂的转译会有难度,但是类似这种简单实用的转译还是可以做到举一反三的效果。