babel和ast(实现简单的babel插件)

410 阅读8分钟

AST简介

抽象语法树(abstract syntax trees),就是将代码转换成的⼀种抽象的树形结构,通常是 JSON描述。AST 并不是哪个编程语⾔特有的概念,在前端领域,⽐较常⽤的 AST ⼯具如esprima,babel(babylon ) [中文]的解析模块,其他如 Vue ⾃⼰实现的模板解析器。本⽂主要以 babel 为例对 AST 的原理进⾏浅析,通过实践掌握如何利⽤ AST 掌握代码转换的能⼒。

  • 推荐⼯具: AST 在线学习tokens 在线分析
  • 插件集合(注意,前端 AST 并不仅针对 JavaScriptCSSHTML ⼀样具有相应的解析⼯具,JavaScript 重点关注)
功能插件常用方法
ast 解析esprima @babel/parser recast.parse
ast 遍历estraverse @babel/traverserecast.visit
⽣成代码escodegen @babel/generatorrecast.print recast.prettyPrint

代码的编译流程

1648137137(1).jpg

我们把上述过程分为三部分:解析(parse),转换(transform),⽣成(generate),其中 scanner 部分叫做词法(syntax)分析,parser 部分叫做语法(grammar)分析。显然,词法分析的结果是 tokens,语法分析得到的就是 AST。

const esprima = require('esprima');
var program = 'const answer = 42';
//词法分析
const token = esprima.tokenize(program);
console.log(token)
//语法分析
const ast = esprima.parse(program);
console.log(JSON.stringify(ast, null, '  '));
//词法分析输出
[
  { type: 'Keyword', value: 'const' }, // keyword 关键词
  { type: 'Identifier', value: 'answer' }, // Identifier 标识符
  { type: 'Punctuator', value: '=' }, // Punctuator 标点符号
  { type: 'Numeric', value: '42' } // Numeric 数字
]
//语法分析输出
{
  "type": "Program",
  "body": [
    {
      "type": "VariableDeclaration", //变量声明
      "declarations": [
        {
          "type": "VariableDeclarator",
          "id": {
            "type": "Identifier",
            "name": "answer"
          },
          "init": {
            "type": "Literal", //直译
            "value": 42,
            "raw": "42"
          }
        }
      ],
      "kind": "const"
    }
  ],
  "sourceType": "script"
}
  • 可⻅词法分析,旨在将源代码按照⼀定的分隔符(空格/tab/换⾏等)、注释进⾏分割,并将各个部分进⾏分类构造出⼀段 token 流。
  • ⽽语法分析,则基于 tokens 将源码语义化、结构化为⼀段 JOSN 描述(AST)。反之,如果给出⼀段代码的描述信息,我们也是可以还原源码的。理论上,描述信息发⽣变化,⽣成的源码的对应信息也会发⽣变化。所以我们可以通过操作 AST 达到修改源码信息的⽬的,辅以⽂件的创建接⼝,这也是 babel 打包⽣成代码的基本原理。了解到这⼀层,便能想象 ES6 => ES5ts => jsuglifyJS、样式预处理器、eslint、代码提示等⼯具的⼯作⽅式了。

AST 的节点类型

在操作 AST 过程中,源码部分集中在 Program 对象的 body 属性下,每个节点有着统⼀固定的格式: @babel/core 依赖了 parsertraversegenerator 模块,所以安装 @babel/core 即可。下⽂均以 babel 作为⽰例⼯具,其他⼯具类似,不再赘述。

const babel = require('@babel/core');
const code = `
 import React from 'react';  function add(a, b) {  return a + b;  }  let str = 'hello';  `;
const ast = babel.parse(code, {
    sourceType: 'module'
});
console.log(ast.program.body);

//输出
[
  Node {
    type: 'ImportDeclaration',
    start: 2,
    end: 28,
    loc: SourceLocation {
      start: [Position],
      end: [Position],
      filename: undefined,
      identifierName: undefined
    },
    specifiers: [ [Node] ],
    source: Node {
      type: 'StringLiteral',
      start: 20,
      end: 27,
      loc: [SourceLocation],
      extra: [Object],
      value: 'react'
    }
  },
  Node {
    type: 'FunctionDeclaration',
    start: 30,
    end: 68,
    loc: SourceLocation {
      start: [Position],
      end: [Position],
      filename: undefined,
      identifierName: undefined
    },
    id: Node {
      type: 'Identifier',
      start: 39,
      end: 42,
      loc: [SourceLocation],
      name: 'add'
    },
    generator: false,
    async: false,
    params: [ [Node], [Node] ],
    body: Node {
      type: 'BlockStatement',
      start: 49,
      end: 68,
      loc: [SourceLocation],
      body: [Array],
      directives: []
    }
  },
  Node {
    type: 'VariableDeclaration',
    start: 70,
    end: 88,
    loc: SourceLocation {
      start: [Position],
      end: [Position],
      filename: undefined,
      identifierName: undefined
    },
    declarations: [ [Node] ],
    kind: 'let'
  }
]

以上⾯的代码为例,如果想在函数返回语句之前加⼊⼀⾏语句 console.log('函数执⾏完成') ,最朴素的做法是这样:

const babel = require('@babel/core');
const code = `
  import React from 'react';
  function add(a, b) {
    const decrease = () => {
      return c;
    }
    return a + b;
  }
  let str = 'hello';
`;
const ast = babel.parse(code, {
  sourceType: 'module'
});
const CONSOLE_AST = babel.template.ast(`console.log('函数执行完成');`);
function insertConsoleBeforeReturn(body) {
  body.forEach(node => {
    if (node.type === 'FunctionDeclaration') { // 函数关键字声明形式
      console.log(node,node.body.body)
      const blockStatementBody = node.body.body;
      if (blockStatementBody && blockStatementBody.length) {
        const index = blockStatementBody.findIndex(n => n.type === 'ReturnStatement');
        if (~index) {
          // 函数体存在语句且最后⼀条语句是 return (假设 return 就是最后的语句)
          blockStatementBody.splice(index, 0, CONSOLE_AST); // 直接修改 ast, 前插⼀个节 点
        }
      }
    }
  });
}
insertConsoleBeforeReturn(ast.program.body)
console.log(babel.transformFromAstSync(ast).code);

//node节点
Node {
  type: 'FunctionDeclaration',
  start: 32,
  end: 125,
  loc: SourceLocation {
    start: Position { line: 3, column: 2, index: 32 },
    end: Position { line: 8, column: 3, index: 125 },
    filename: undefined,
    identifierName: undefined
  },
  id: Node {
    type: 'Identifier',
    start: 41,
    end: 44,
    loc: SourceLocation {
      start: [Position],
      end: [Position],
      filename: undefined,
      identifierName: 'add'
    },
    name: 'add'
  },
  generator: false,
  async: false,
  params: [
    Node {
      type: 'Identifier',
      start: 45,
      end: 46,
      loc: [SourceLocation],
      name: 'a'
    },
    Node {
      type: 'Identifier',
      start: 48,
      end: 49,
      loc: [SourceLocation],
      name: 'b'
    }
  ],
  body: Node {
    type: 'BlockStatement',
    start: 51,
    end: 125,
    loc: SourceLocation {
      start: [Position],
      end: [Position],
      filename: undefined,
     identifierName: undefined
    },
    body: [ [Node], [Node] ], //包含了返回函数ReturnStatement
    directives: []
  }
}
//node.body.body
[
  Node {
    type: 'VariableDeclaration',
    start: 57,
    end: 103,
    loc: SourceLocation {
      start: [Position],
      end: [Position],
      filename: undefined,
      identifierName: undefined
    },
    declarations: [ [Node] ],
    kind: 'const'
  },
  Node {
    type: 'ReturnStatement',
    start: 108,
    end: 121,
    loc: SourceLocation {
      start: [Position],
      end: [Position],
      filename: undefined,
      identifierName: undefined
    },
    argument: Node {
      type: 'BinaryExpression',
      start: 115,
      end: 120,
      loc: [SourceLocation],
      left: [Node],
      operator: '+',
      right: [Node]
    }
  }
]
//转化后的代码
import React from 'react';
function add(a, b) {
  const decrease = () => {
    return c;
  };
  console.log('函数执行完成');
  return a + b;
}
let str = 'hello';

虽然⼿动操作 AST 满⾜了当前的需求,但是诸如箭头函数,类或对象的⽅法、没有 return 语句或省略 return 关键字的函数、表达式声明的函数、IIFE、语句内又嵌套的函数……上述⽅法都是没有考虑的,所以不推荐⼿动实现。

由此可⻅,处理 AST 的过程就是对不同节点类型遍历和操作的过程,为简化操作,babel 提供了专⻔的接⼝,我们只需要提供相应类型的处理⽅法(visitor)即可。还是上⾯的需求(好⼀点的是所有的return 语句都会处理,即使是嵌套的函数):

babel.traverse(ast, {
  ReturnStatement(path) {
    path.insertBefore(CONSOLE_AST);
  }
});

//输出
import React from 'react';
function add(a, b) {
  const decrease = () => {
    console.log('函数执行完成');
    return c;
  };
  console.log('函数执行完成');
  return a + b;
}
let str = 'hello';

traverse ⽅法帮我们处理了 ast 的遍历过程,对于不同节点的处理只需要维护⼀份 types 对应的⽅法即可。进⼀步的,构造 CONSOLE_AST 节点也有⼏种⽅式。先使⽤在线⼯具将 console.log('函数执⾏完成'); 结构化(如果你已经⼗分熟悉这个过程,可以跳过):

{
  "type": "Program",
  "start": 0,
  "end": 22,
  "body": [
    {
      "type": "ExpressionStatement",
      "start": 0,
      "end": 22,
      "expression": {
        "type": "CallExpression",
        "start": 0,
        "end": 21,
        "callee": {
          "type": "MemberExpression",
          "start": 0,
          "end": 11,
          "object": {
            "type": "Identifier",
            "start": 0,
            "end": 7,
            "name": "console"
          },
          "property": {
            "type": "Identifier",
            "start": 8,
            "end": 11,
            "name": "log"
          },
          "computed": false,
          "optional": false
        },
        "arguments": [
          {
            "type": "Literal",
            "start": 12,
            "end": 20,
            "value": "函数执行完成",
            "raw": "'函数执行完成'"
          }
        ],
        "optional": false
      }
    }
  ],
  "sourceType": "module"
}
  • 基础⽅式——使⽤ @babel/types 来构造语句
const t = require('@babel/types');
const generate = require('@babel/generator').default;
const CONSOLE_AST = t.expressionStatement(
  t.callExpression(
    t.memberExpression(
      t.identifier('console'),
      t.identifier('log')
    ),
    [t.stringLiteral('函数执行完成')],
  )
);
console.log(JSON.stringify(CONSOLE_AST, null, '  '), '\n\n', generate(CONSOLE_AST).code);
//输出
{
  "type": "ExpressionStatement",
  "expression": {
    "type": "CallExpression",
    "callee": {
      "type": "MemberExpression",
      "object": {
        "type": "Identifier",
        "name": "console"
      },
      "property": {
        "type": "Identifier",
        "name": "log"
      },
      "computed": false,
      "optional": null
    },
    "arguments": [
      {
        "type": "StringLiteral",
        "value": "函数执行完成"
      }
    ]
  }
}

console.log("\u51FD\u6570\u6267\u884C\u5B8C\u6210");
  • 终极简化版——模板 API,也是上⾯表格提前给出来的⽅式:
const template = require('@babel/template').default;
// 或 const template = require('@babel/core').template;
 const CONSOLE_AST = template.ast(
 `console.log('函数执⾏完成')`
 );
 console.log(CONSOLE_AST);
 
 //输出
 {
  type: 'ExpressionStatement',
  expression: {
    type: 'CallExpression',
    callee: {
      type: 'MemberExpression',
      object: [Object],
      property: [Object],
      computed: false,
      loc: undefined
    },
    arguments: [ [Object] ],
    loc: undefined
  },
  loc: undefined
}

AST 与 babel 插件

官⽅插件

随着 ECMAScript 的发展,不断涌出⼀些新的语⾔特性(如管道操作符、可选链操作符、控制合并操作符……),也包括但不限于 JSX 语法等。遇到 babel 本身的解析引擎模块不能识别新特性的问题,可以由插件来处理。

const babel = require('@babel/core')
const code = `
  const square = x => x ** 2;
  const sum = a => a + 2;
  const list = 5 |> square(^^) |> sum(^^);
`;
const ast = babel.parse(code, {

  sourceType: 'module',

}); 
console.log(babel.transform(code).code);

运⾏上⾯的代码会直接报错,源码(第 5 ⾏)使⽤的管道操作符处于提案中,需要借助插件来解析:

  1. @babel/parser 模块 + 内联配置(记得安装 @babel/plugin-proposal-pipeline-operator )解析
const ast = babel.parse(code, {
  sourceType: 'module',
  plugins:[
    ["pipelineOperator", {
      "proposal": "hack",
      "topicToken": "^^"
    }]
  ]
}); 

2.@babel/core模块 + ⽂件 babel.config.json 解析(babel 会⾃动到项⽬⽬录查找最近的)babel配置文件

 {
     "plugins": [
         ["@babel/plugin-proposal-pipeline-operator", {
             "proposal": "hack",
             "topicToken": "^^"
         }]
     ]
 }
 //输出
const square = x => x ** 2;
const sum = a => a + 2;
const list = sum(square(5));

同理,其他插件通过相同的⽅式使⽤。

  • 当项⽬需要⽀持的语⾔特性越来越多, plugins 需要逐⼀添加,为了解决插件的管理与依赖问题,通过提供常⽤的环境配置。因此 总能看到这样的配置 (React 项⽬):
{
     "presets": ["@babel/preset-env", "@babel/preset-react"]
     "plugin": []
}
  1. 先执⾏完所有 plugins,再执⾏ presets
  2. 多个 plugins,按照声明次序顺序执⾏。
  3. 多个 presets,按照声明次序逆序执⾏。

自定义插件

以上⾯的源码为例,实现变量标识的重命名,源码及转换逻辑:

//.my-plugin.js
module.exports = function (babel) {

  return {

    visitor: {

      VariableDeclaration(path, state) {

        path.node.declarations.forEach(each => {

          path.scope.rename(

            each.id.name,

            path.scope.generateUidIdentifier("uid").name

          );

        });

      }

    },

  }

}

//babel.config.json
{

  "plugins": [

     ["./my-plugin"],

    ["@babel/plugin-proposal-pipeline-operator", {

      "proposal": "hack",

      "topicToken": "^^"

    }]

  ]

}
//输出
const _uid = x => x ** 2;

const _uid2 = a => a + 2;

const _uid3 = _uid2(_uid(5));

自定义try-catch插件

const babel = require('@babel/core')
// babel-loader   : test:/\.[tj]sx?/
const code = `
    async function Async1() {
      await fetch();
    }
    async function Async2() {
      const { data } = await fetch();
      return data;
    }
    const Async3 = async function () {
      const { code } = await fetch();
      return code;
    }`
console.log(babel.transform(code).code);

//try-catch.js
module.exports = function ({ types: t, template }) {
  return {
    name: 'auto-try-catch',
    visitor: {
      AwaitExpression(path) {
        const shouldSkip = path.findParent(p => p.isTryStatement());
        if (!shouldSkip) {
          const blockStatement = path.findParent(p => p.isBlockStatement());
          if (blockStatement) {
            /* blockStatement.replaceWith(
              t.blockStatement([
                t.tryStatement(
                  blockStatement.node,
                  t.catchClause(
                    t.identifier('err'),
                    t.blockStatement([
                      t.expressionStatement(
                      t.callExpression(
                        t.memberExpression(
                          t.identifier('console'),
                          t.identifier('error')
                        ),
                        [t.identifier('err')],
                      )
                    )]
                  )
                  )
                )]
              )
            ) */
            blockStatement.replaceWith(
              t.blockStatement([
                template.ast(`
                  try ${blockStatement.toString()}
                  catch (err) {
                    console.error(err);
                  }
                `)
              ])
            )
          }
        }
      }
    }
  };
}
//输出
async function Async1() {
  try {
    await fetch();
  } catch (err) {
    console.error(err);
  }
}
async function Async2() {
  try {
    const {
      data
    } = await fetch();
    return data;
  } catch (err) {
       console.error(err);
  }
}
const Async3 = async function () {
  try {
    const {
      code
    } = await fetch();
    return code;
  } catch (err) {
    console.error(err);
  }
};