Javascript抽象语法树下篇(实践篇)

做者:陈晓强html

上篇已经对AST基础作了介绍,本篇介绍AST的运用vue

AST应用的三个要点

  1. 须要一个解析器,将代码转换为AST
  2. 须要一个遍历器,可以遍历AST,并可以方便的对AST节点进行增删改查等操做
  3. 须要一个代码生成器,可以将AST转换为代码

esprima与babel

经常使用的知足上述3个要点的工具包有两个,一个是esprima,一个是babelnode

esprima相关包及使用以下webpack

const esprima = require('esprima');   // code => ast
const estraverse = require('estraverse'); //ast遍历
const escodegen = require('escodegen'); // ast => code
let code = 'const a = 1';
const ast = esprima.parseScript(code);
estraverse.traverse(ast, {
    enter: function (node) {
        //节点操做
    }
});
const transformCode = escodegen.generate(ast);
复制代码

babel相关包及使用以下git

const parser = require('@babel/parser');  //code => ast
const traverse = require('@babel/traverse').default; // ast遍历,节点增删改查,做用域处理等
const generate = require('@babel/generator').default; // ast => code
const t = require('@babel/types'); // 用于AST节点的Lodash式工具库,各节点构造、验证等
let code = 'const a = 1';
let ast = parser.parse(sourceCode);
traverse(ast, {
  enter (path) { 
    //节点操做
  }
})
const transformCode = escodegen.generate(ast);
复制代码

目前babel无论是从生态上仍是文档上比esprima要好不少,所以推荐你们使用babel工具,本文示例也使用babel来作演示。github

使用babel工具操做AST

如上一章节所示web

  • @babel/parser用于将代码转换为AST
  • @babel/traverse用于对AST的遍历,包括节点增删改查、做用域等处理
  • @babel/generator 用于将AST转换成代码
  • @babel/types 用于AST节点操做的Lodash式工具库,各节点构造、验证等

更多api详见babel手册[1]小程序

下面经过简单案例来介绍如何操做AST,注意案例只是示例,因为篇幅对部分边界问题只会注释说明,实际开发过程当中须要考虑周全。api

案例1:去掉代码中的console.log()

实现代码微信

const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const generate = require('@babel/generator').default;
const t = require('@babel/types');
let sourceCode = ` function square(n) { console.log(n); console.warn(n); return n * n; } `
let ast = parser.parse(sourceCode);
traverse(ast, {
 CallExpression(path) {
  let { callee } = path.node;
  if (callee.type === ‘MemberExpression’ && callee.object.name === ‘console’ && callee.property.name === ‘log’ ) {
   path.remove(); // 注意考虑对象挂载的识别,如global.console.log(),此时remove后剩下global.,会致使语法错误,此时能够判断父节点类型来排除
  }
 }
})
console.log(generate(ast).code);
复制代码

处理结果

function square(n) {
- console.log(n);
  console.warn(n);
  return n * n;
}
复制代码

此案例涉及知识点

  1. 如何经过 traverse遍历特定节点
  2. 识别出console.log()在规范中属于函数调用表达式,节点类型为CallExpression
  3. console.log自己即callee是在对象console上的一个方法,所以console.log是一个成员表达式,类型为MemberExpression
  4. MemberExpression根据规范有一个object属性表明被访问的对象,有一个property表明访问的成员。
  5. 经过path.remove()api能够对节点进行删除。
  6. 能够经过https://astexplorer.net/ 来辅助对代码节点的识别。注意选择babylon7,即babe7,对应@babel/parser

案例2:变量混淆

实现代码

const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const generate = require('@babel/generator').default;
const t = require('@babel/types');
let sourceCode = ` function square(number) { console.warn(number); return number * number; } `
let ast = parser.parse(sourceCode);
traverse(ast, {
  FunctionDeclaration(path) {
    let unia = path.scope.generateUidIdentifier("a");
    path.scope.rename("number",unia.name);
 }
})

console.log(generate(ast).code);
复制代码

处理结果

-function square(number) {
+ function square(_a) {
- console.warn(number);
+ console.warn(_a);
- return number * number;
+ return _a * _a;
}
复制代码

此案例涉及知识点

  1. path.scope保存了当前做用域的相关信息
  2. 能够经过api对做用域内的变量名进行批量修改操做
  3. 经过path.scope能够得到当前做用域惟一标识符,避免变量名冲突

案例3:转换箭头函数并去掉未使用参数

实现代码

const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const generate = require('@babel/generator').default;
const t = require('@babel/types');
let sourceCode = ` new Promise((resolve,reject)=>{ setTimeout(()=>{ resolve(1); },200) }); `
let ast = parser.parse(sourceCode);
traverse(ast, {
  ArrowFunctionExpression (path) { 
    let { id, params, body } = path.node;
    for(let key in path.scope.bindings){   //注意考虑箭头函数的this特性,若发现函数体中有this调用,则须要在当前做用域绑定其父做用域的this
      if(!path.scope.bindings[key].referenced){
        params = params.filter(param=>{
          return param.name!==key;
        })
      }
    }
  path.replaceWith(t.functionExpression(id, params, body)); 
  }
})

console.log(generate(ast).code);
复制代码

处理结果

-new Promise((resolve,reject)=>{
+new Promise(function(resolve){
- setTimeout(()=>{
+ setTimeout(function(){
    resolve(1);
  },200)
});
复制代码

此案例涉及知识点

  1. 箭头函数节点:ArrowFunctionExpression
  2. 经过path.scope能够识别变量引用状况,是否有被引用,被哪些路径引用
  3. 经过@babel/types能够很方便的构建任意类型节点
  4. 经过path.replaceWith()能够进行节点替换

案例4:京东购物小程序的Tree-shaking

删掉小程序中的冗余代码,部分实现代码示例以下

const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const generate = require('@babel/generator').default;
const t = require('@babel/types');
let sourceCode = ` export function square (x) { return x * x; } export function cube (x) { return x * x * x; } `
let ast = parser.parse(sourceCode);
traverse(ast, {
  ExportNamedDeclaration (path) {
    let unused = ['cube']   // 借助webpack,咱们能得到导出的方法中,哪些是没有被使用过的
    let { declaration = {} } = path.node;
    if (declaration.type === 'FunctionDeclaration') {
      unused.forEach(exportItem => {
        // references=1表示仅有一次引用,即export的引用,没有在别处调用
        if (declaration.id.name === exportItem && path.scope.bindings[exportItem].references === 1) {
          path.remove();
        }
      });
    }
  }
})

console.log(generate(ast).code);
复制代码

处理结果

export function square (x) {
    return x * x;
}
-export function cube (x) {
- return x * x * x;
-}
复制代码

此案例涉及知识点

  1. export节点:ExportNamedDeclaration

案例5:将代码转换成svg流程图

此案例是git上一个比较有意思的开源项目,经过AST将代码转换为svg流程图,详见js-code-to-svg-flowchart[2]

能够体验一下:demo[3]

经过以上示例,能够看到经过AST咱们能够对代码任意蹂躏,作出不少有意思的事情

AST在其余语言的应用

除了Javascript,其余语言如HTML、CSS、SQL等也有普遍的AST应用。以下图,能够在这里找到对应语言的解析器,开启AST之门。

其余AST

结语

在上述AST网站中,能够看到HTML的解析器有个vue选项,读过vue源码的同窗应该知道vue模板在转换成HTML以前会先将模板转换成AST而后生成render function进而生成VirtualDOM。咱们平时开发对AST使用比较少,但其实处处都能见到AST的影子:babel、webpack、eslint、taro等等。但愿能抛砖引玉,使同窗们在各自团队产出更多基于AST的优秀工具、项目。

References
[1] babel手册
[2] js-code-to-svg-flowchart
[3] demo


若是你以为这篇内容对你有价值,请点赞,并关注咱们的官网和咱们的微信公众号(WecTeam):

WecTeam
相关文章
相关标签/搜索