使用 Babel 进行抽象语法树操作 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
famanoder
V2EX    前端开发

使用 Babel 进行抽象语法树操作

  •  
  •   famanoder
    famanoder 2019-07-22 00:46:58 +08:00 3850 次点击
    这是一个创建于 2273 天前的主题,其中的信息可能已经有所发展或是发生改变。

    什么是抽象语法树

    wiki: 抽象语法树( Abstract Syntax Tree,AST ),或简称语法树( Syntax tree ),是源代码语法结构的一种抽象表示。 它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。 之所以说语法是“抽象”的,是因为这里的语法并不会表示出真实语法中出现的每个细节。

    在我们前端,可以通过 Javascript 解析器将我们程序的源代码映射成为一棵语法树,而树的每个节点对应着代码里的一种结构;比如表达式,声明语句,赋值语句等都会被映射为语法树上的一个节点,进而我们就可以通过操作语法树上的节点来控制我们的源代码;初步了解时可能感觉这个概念本身就比较抽象,但是基于它的应用我们却一点都不陌生,比如:单文件组件 .vue 文件的解析、为我们转码 ES6 语法的 babeljs、为我们压缩混淆代码的 UglifyJS 等等;

    亮剑 Babeljs

    Babel is a Javascript compiler. Use next generation Javascript, today.

    Babel是一个Javascript编译器,他能让你现在就开始使用未来版的Javascript。曾经的Javascript由于语言本身的设计缺陷,饱受程序员们的诟病,如今随着ES语言规范的制定与发展,加上Typescript的横空出世,Javascript开始逐年霸占最流行语言的榜首;Babel是推动这一进程的重要推手,他能将ES6/7/8/9/10转换为浏览器能够兼容的Javascript,从现在开始,您就可以使用最新的ES语法规范,甚至处于草案阶段的规范;而这都是一切都是前所未有的!

    接下来我们将使用Babel提供的相关AST操作的模块来“改头换面”我们的代码;

    • @babel/parser通过该模块来解析我们的代码生成AST抽象语法树;

    • @babel/traverse通过该模块对AST节点进行递归遍历;

    • @babel/types通过该模块对具体的AST节点进行进行增、删、改、查;

    • @babel/generator通过该模块可以将修改后的AST生成新的代码;

    无中生有

    Talk is cheap, show me your code!

    现在假设我们在代码里使用了一个叫log的方法,如:log('Hello, world!');,但是我们并没有声明或定义该方法,一般情况下我们的代码将会报一个log is not defined的错误,现在我们通过操作ASTlog修改为console.log,这样我们的代码就不会报错了;

    astexplorer.net网站里我们可以在线将代码转为AST,如下图:log('Hello, world!')AST树形结构图;

    一般情况下,我们从body层级看起,其下的每一层级都有一个type字段,该字段非常重要,直接影响我们该如何对该语句或表达式进行操作,具体请看后面讲到的@babel/types

    拿到代码,我们首先通过@babel/parser生成如上图所示结构的AST

    const {parse} = require('@babel/parser'); const codes = "log('Hello, world!');"; const ast = parse(codes, { sourceType: "module" }); 

    对照AST分析我们需要做什么,新手强烈推荐使用astexplorer.net,比如在这里,我们的需求是将log函数转换为console.log,通过在线AST解析,我们清楚的看到了log对应的AST节点类型为Identifier,同样的,我们换成console.log('Hello, world!');,可以看到console.log对应的AST节点类型为MemberExpression,所以,我们的需求变为将此处的Identifier变为MemberExpression,不要想当然的以为直接把type属性改个值就 ok 了,接下来看看,如何将Identifier类型的节点修改为MemberExpression类型的节点;

    轮到@babel/types上场了,前面两步,我们的焦点主要在AST节点的type字段上,事实上,每一个type@babel/types里都有一个同名的方法(首字母小写)用来创建该类型的节点,比如创建Identifier类型的节点,我们可以使用t.identifier方法;

    好了,先来创建MemberExpression类型的console.log,此处对于新手会比较棘手,推荐好好观察在线的AST树进行反推,找到目标节点的type,接着在@babel/types文档里搜相应type的方法;如图,我们在文档里搜到memberExpression方法的定义,接着开始创建console.log节点;

    const t = require('@babel/types'); function createMemberExpression() { return t.memberExpression( t.identifier('console'), t.identifier('log') ); } 

    如上,我们就可以通过createMemberExpression方法来生成console.log来替换log了,问题来了,如何替换?

    当然,替换之前,我们需要在AST树上找到对应的节点,通过@babel/traverse我们可以对AST树的节点进行遍历;

    const {default: traverse} = require('@babel/traverse'); traverse(ast, visitor); 

    visitor是一个由各种type或者是enterexit组成的对象,由此确定在遍历的过程中匹配到某种类型的节点后该如何操作,如我们的需求是将Identifier类型的log节点替换为MemberExpression类型的console.log,我们可以这样定义visitor

    const visitor = { Identifier(path) { const {node} = path; if(node && node.name === 'log') { path.replaceWith(createMemberExpression()); path.stop(); } } } 

    通过traverse方法我们可以定义各种类型节点的操作方式,回调函数的path参数提供了丰富的增、删、改、查以及类型断言的方法,比如replaceWith/remove/find/isMemberExpression

    最后,我们将修改后的AST转换为Javascript代码:

    const {code} = generate(ast, { /* options */ }, codes); 

    到这一步,我们就已经可以将代码里的log方法替换为console.log了;举一反三,我们是不是可以放开一下想象力:自己定义某种有意思或者创造性的语法规范,然后通过AST操作变换成常规的Javascript

    以上例子的代码汇总为:

    const t = require('@babel/types'); const {parse} = require('@babel/parser'); const {default: traverse} = require('@babel/traverse'); const {default: generate} = require('@babel/generator'); const codes = "log('Hello, world!');"; const ast = parse(codes, { sourceType: "module" }); const visitor = { Identifier(path) { const {node} = path; if(node && node.name === 'log') { path.replaceWith(createMemberExpression()); path.stop(); } } } traverse(ast, visitor); const {code} = generate(ast, { /* options */ }, codes); console.log(code); // console.log('Hello, world!'); function createMemberExpression() { return t.memberExpression( t.identifier('console'), t.identifier('log') ); } 

    总结

    以上, 通过babeljs提供的相关模块对AST操作进行了初步的实践; 2019 年已过一半,今年无疑跨端应用火了,比如,Taro、uni-app 等,而这些框架的成功统统离不开的就是对AST的熟练掌握;所以呢,还等什么,现在上车还来得及,如果你对前端报有天马行空的想象,那么我想了解AST将会助你一臂之力!

    目前尚无回复
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     2737 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 22ms UTC 11:32 PVG 19:32 LAX 04:32 JFK 07:32
    Do have faith in what you're doing.
    ubao snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86