V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
famanoder
V2EX  ›  前端开发

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

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

    什么是抽象语法树

    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   ·   实用小工具   ·   4098 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 30ms · UTC 10:18 · PVG 18:18 · LAX 02:18 · JFK 05:18
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.