AnnVoV / blog

24 stars 2 forks source link

ast 入门(从写babel插件来深入了解ast) #26

Open AnnVoV opened 5 years ago

AnnVoV commented 5 years ago

最终效果

先看效果,写了一个小例子,请点击查看 https://astexplorer.net/#/gist/49a1bb9cc81e392220ffd998ff47162c/4728dec0c4645e40c58426886bc2069c959544e4

前言

查找ast相关资料,看到别人引用资源里有一个ppt,《how writing a babel plugin is like... jQuery》看了一遍发现,真香!下面我就按照我的理解总结一下ppt里面的内容。通过了解如何开发一个babel插件,也能让我们更好地理解ast. 后面可以不看,重点推荐下面这两个资料:

ast 基础

在开始往下面看之前,为了对ast节点类型有更全面的认识,推荐阅读下这个同学写的文章:https://github.com/Pines-Cheng/blog/issues/53 Babylon 与 JavaScript 抽象语法树

What is Babel?

Babel is a javascript compiler。

Babel 是一个通用的,多用途的Javascript 编译器

整个ppt最犀利的观点就是:

Babel::javascript jQuery::DOM Babel turns the code you have into the code you need

dom 之于 jQuery 同 javascript 之于 Babel

Compile Overview

pic1 babel 中的几个核心模块:

Write a Babel Plugin

A Plugin is just a function

export default babel => {
  return {
    visitor:{...}
  }
}

AST Visitor Pattern

export default function(babel) {
  const t = babel.types;
  return {
    visitor: {
      BinaryExpression(path) {...},
      Identifier(path) {...},
      FunctionDeclaration(path) {...},
      ClassDeclaration(path) {...},
    }
  }
}

当Babel遍历ast时,会查看每一个节点,当发现visitor对象上有对应的方法时,会调用这个方法,并且把对应的上下文传递进去。

举个例子

var a = 2**3 // 源代码转换为
var a = Math.pow(2, 3); // 目标代码

上面通过babel 插件如何实现呢?

Step1 查看源代码ast 结构

我们利用https://astexplorer.net 这个网址可以查看到源代码对应的ast结构,让我们知道我们需要的各个节点类型。

pic2

Step2 获取源码对应结构

export default function(babel) {
  // 插件大体结构是一样的
  const t = babel.types;
  return {
    visitor: {
      // 2**3 是一个二元表达式
      BinaryExpression(path) {
        // console.log(path); 可以把对应路径打印出来看看
        if (path.node.operator !== '**') {
          return; // 如果节点对应的操作不是** 不做任何处理
        }
        const {left, right} = path.node; // 获取表达式左右两项
      }
    }
  }
}

Step3 对源码结构进行替换

最终我们希望把2**3 替换为一个函数调用Math.pow(2,3), 所以要看下Math.pow(2,3) 的ast 结构,同样利用上面那个网址 pic3

// babel-type 手册https://babeljs.io/docs/en/next/babel-types.html

t.callExpression(
  t.memberExpression( // 创建函数
    t.identifier('Math'),
    t.identifier('pow'),
  ),
  [left, right] // 入参
);

这样就生成了一个Math.pow(2, 3)的函数调用节点

step4 节点替换

export default function(label) {
  const t = babel.types;
  return {
    visitor: {
      BinaryExpression(path) {
        if (path.node.operator !== '**') {
          return;
        }
        path.replaceWith(t.callExpression(
          t.memberExpression(
            t.identifier('Math'),
            t.identifier('pow')
          ),
          [left, right]
        ));
      }
    }
  }
}

拓展

其实写babel插件的重点在于了解ast, 有了ast以后我们可以做很多事情,除了写bable插件,我们也可以去写eslint插件,本质是一样的,只是语法有些略微的区别。

module.exports = {
  meta: {
    docs: {
      description: "Disallow if statement without block",
      category: "Best Practices",
      recommended: true
    }
  },
  create(context) {
    return {
      IfStatement(node) {
        if(isBlock(node.consequent) && isBlock(node.alternate)) {
          return;
        }
        context.report({
          node,
          message: 'yo, y u non block?'
          fix: function(fixer) {
            // fix the code
          }
        })
        function isBlock(n) {
          return !n || n.type === 'BlockStatement';
        }
      }
    };

   //  更复杂的babel插件参考
   //  https://juejin.im/post/5a17d51851882531b15b2dfc
  }
};

参考资料: 1.how writing a babel plugin is like...(重要 非常棒) https://hzoo.github.io/babel-plugin-slides/assets/player/KeynoteDHTMLPlayer.html#0 2.Writing custom Babel and ESLint plugins http://slides.com/kentcdodds/a-beginners-guide-to-asts#/6 3.@babel/types babel的参考手册 4.通过开发 Babel 插件理解抽象语法树(AST) https://www.zcfy.cc/article/understanding-asts-by-building-your-own-babel-plugin 5.如何写一个eslint 插件(拓展) https://www.zcfy.cc/article/creating-an-eslint-plugin