实现⼀个真正的babel插件(不仅仅是替换字符)及ast操作原
理
babel作为当前源码编译的重要⼯具,有着很重要的地位。babel编译的核⼼流程是,先把代码解析为AST语法树,再遍历AST语法树并执⾏操作,最后根据规则⽣成代码。流程不复杂,复杂的是如何操作AST语法树,以及如何编写babel的插件。
⽹上有很多帖⼦在讲如何编写babel插件,但是讲的都⽐较浅显,看过之后并不能真正意义上去编写babel插件。在实际的项⽬中,我们需要的插件不仅仅是替换字符串或者打印出什么那么简单,接下来本⽂会实现⼀个含有表达式⽣成,节点类型分析,逻辑判断的babel插件。
业务需求:
在代码require('test')之后加上.default,实现module模块和es6中export的兼容。
说明:这个需求场景是我在升级项⽬的时候遇到的,在升级babel后,项⽬中require('test')之类的会出现报错,经查得到是模块规范未统⼀,需要加.default后不报错。由于项⽬中有太多的地⽅使⽤该场景,所以考虑采⽤增加babel插件的⽅法解决该bug。
编写插件之前:
先说下关于ast语法树的定义和操作相关
ast语法树是由许多节点(node)组成的,node之前的关系使⽤path表⽰,path是⼀个可操作的⼤的对象,有很多⽅法集成在上⾯。node 有许多属性,⽐如type,start,end等。node可以通过defineType(args)⽣成,具体的可以参考babel的官⽅⽂档,这⾥不再详细介绍。
ast遍历时采⽤的是树的深度优先遍历(深度优先遍历参见我的另⼀篇⽂章,)。
babel中常⽤的库和⼯具类:
@babel/parr 将源代码解析成 AST。
@babel/generator 将AST解码⽣ js代码。
@babel/core 包括了整个babel⼯作流,也就是说在@babel/core⾥⾯我们会使⽤到@babel/parr、transformer[s]、以及@babel/generator。
@babel/code-frame ⽤于⽣成错误信息并且打印出错误原因和错误⾏数。(其实就是个console⼯具类)
@babel/helpers 也是⼯具类,提供了⼀些内置的函数实现,主要⽤于babel插件的开发。
@babel/runtime 也是⼯具类,但是是为了babel编译时提供⼀些基础⼯具库。作⽤于transformer[s]阶段,当然这是⼀个⼯具库,如果要使⽤这个⼯具库,还需要引⼊@babel/plugin-transform-runtime,它才是transformer[s]阶段⾥⾯的主⾓。
@babel/template 也是⼯具类,主要⽤途是为parr提供模板引擎,更加快速的转化成AST
@babel/traver 也是⼯具类,主要⽤途是来便利AST树,也就是在@babel/generator过程中⽣效。
@babel/types 也是⼯具类,主要⽤途是在创建AST的过程中判断各种语法的类型。
@babel/code-frame 为全局错误捕获⼯具类
@babel/core
├──输⼊字符串
├── @babel/parr
│└── @babel/template
│└── @babel/types
├── AST
├── transformer[s]
│└── @babel/helpers
├── AST
├── @babel/generator
│└── @babel/traver
访问AST时的visitor对象
编写插件主要是编写visitor对象,即告诉遍历ast时要访问哪些类型的代码,以及对这些代码要做的操作。
⽐如:在访问对应的节点时,接收两个对象path,state(全局状态, 局部状态)
const MyVisitor = {
Identifier(path,state) {
console.log("找到你要访问的字符标识,要进⾏什么操作");
swat}conjunction
};
鬼的英文
在访问者中,会有进⼊和离开的属性,写法是这样的:
const MyVisitor = {
Identifier: {
enter() {
console.log("Entered!");
},
exit() {
console.log("Exited!");
}
}
};
path对象
这⾥不做详细讲解,只讨论下aver()⽅法,以及给visitor传参数
实例:
const updateParamNameVisitor = {
Identifier(path) {
if (de.name === this.paramName) {//this.paramName局部state
}
}
};
const MyVisitor = {
FunctionDeclaration(path) {
const param = de.params[0];
const paramName = param.name;
param.name = "x";
}
};
从上⾯的例⼦还可以看出visitor是可以嵌套的。
实际编写中⼀般使⽤的是@babel/traver
操作代码@babel/types
@babel/types内集成了很多⽅法,具体参见:这⾥介绍下总体概念,不详细介绍.
⽐如:const t = reauire(‘@babel/types')
t.isIndentifier和t.indentifier,前者是判断isIndentifier,后者是⽣成⼀个indentifier。⽣成代码@babel/generator
const result = generate(Ast, {
retainLines: fal,
compact: "auto",
conci: fal,
quotes: "double",
},code)
⽣成的是⼀个对象 { code, map,... }
开始编写插件:
1,初始化
翻译机器
创建⽂件夹babel-plugin-require-to-default-from,进⼊执⾏npm init
i want rock插件包命名为@babel/plugin-require-to-default-from
2,环境搭建,安装包并引⼊
const babylon = require('@babel/parr')
法国高考题const traver = require("@babel/traver").default
const generate = require("@babel/generator").default
const t = require('@babel/types')
3,创建AST
const code = `
const test = require('test')
`
const Ast = babylon.par(code,{
sourceType:"module",
//plugins:["exportDefaultFrom"]//这⾥是要⽤到的插件,⽂中插件未⽤到
})
4,遍历并操作AST
traver(Ast,{
enter(path){
de.type === 'CallExpression' && de.callee.name == "require"){
//判断require是否本⾝包含default
if(!(de.type === 'MemberExpression' && de.property.name === 'default')){
// t.memberExpression(object, property, computed, optional)
const node_new = t.memberExpression(
t.callExpression(
t.identifier('request'),
[t.identifier(`'${de.arguments[0].value}'`)]
),
t.identifier('default')
)
}
}
}
})
这⾥使⽤的是@babel遍历ast。
在该环节踩了很多坑,主要原因是ast节点类型有很多,并没有⽂档详细的介绍什么节点是什么类型,最后采⽤代码调试解决。
调试如图:
同样的⽅法可以知道require是CallExpression类型
在调试过程中,还可以看到类型的属性。
托福听力的弦外之音
365翻译上⾯的代码兼容了require().default⾃⾝带有default的功能5,⽣成代码
const result = generate(Ast, {
retainLines: fal,
compact: "auto",
conci: fal,
quotes: "double",
},code)
console.log(result)
generate的api参见官⽅⽂档
6,按照插件api封装
export default function({types:t}){
return {
visitor:{
enter(path){
...//放⼊上⾯的代码
}
}
}
}
总结:china girl
到此⼀个babel插件的雏形就编写完成了,babel插件编写⽤到的知识⽐较琐碎的,需要对各个api详细掌握才能有所⽤,有所思。
在编写该插件的过程中,遇到了很多坑,到⼀个⼀个解决,收获了很多。在接触⼀样新知识,还是要仔细研究官⽅⽂档,再加上扎实的基础,就能有所收获。
>新东方泡泡