超实用的AST的基本操作,你学会了吗?

3,873 阅读5分钟

超实用的AST的基本操作

WechatIMG3941.jpeg

很多小伙伴在学习前端过程中,会遇到AST的解析。比如,vue源码,react源码,甚至很多底层框架里面随着大量业务的迁移架构的过程中,大大小小都会遇到需要批量转换代码的问题

本文适合在不了解AST原理知识的情况下,仍然对AST充满好奇心的开发者们。

什么是AST?

我们可以把AST看成一棵千变万化的树,它能够变成任何我们开发中想要的东西。

抽象语法🌲(Abstract Syntax Tree) 简称AST,是以树状形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。JavaScript引擎工作工作的第一步就是将代码解析为AST,Babel、eslint、prettier等工具都基于AST。

调试工具推荐

在正式学习AST之前,我们需要准备几个趁手的工具来帮助我们学习。

AST exporter

这是一个AST的在线调试工具,有了它,我们可以很直观的看到AST生成前后以及代码公户,它分为5个部分,我们接下来都依赖这个工具进行代码操作

👁图:

image-20210714143711160.png

toolTopBar区域,Transform下有个jscodeshift,接下来我们重点讲下它

jscodeshift

它是一个AST的转换器,我们通过它来讲原始代码转成ast语法树,并借助其开放的api操作ast,最终转换成我们想要的代码

jscodeshiftapi基于recast封装。recast是对babel/travers&babel/types的封装,他提供简易的ast操作,而travers是babel中用于操作ast的工具,types我们可以先把它理解成为一个字典,用于描述树的类型。

同时,jscodeshift还提供额外的功能,使得开发者们可以在项目开发调试阶段投入使用,同时不需要去感知babel转译前后的过程,只需要专注于如何操作或者改变树,并得知结果。

AST的权威API

babel-types

ast语法字典,方便我们快速查阅结构树的类型,它是我们想要通过ast生成某行代码的重要工具之一

假如我们有这样一段代码

var a= 1

转换成AST之后,以JSON格式展示如下;(注意:此时我选择了jscodeshift作为转义器了,并选取了JSON展示的核心代码)

{
  "type": "Program",
  "sourceType": "script",
  "body": [
    {
      "type": "VariableDeclaration",
      "kind": "var",
      "declarations": [
        {
          "type": "VariableDeclarator",
          "id": {
            "type": "Identifier",
            "name": "a"
          },
          "init": {
            "type": "Literal",
            "value": 1
          }
        }
      ]
    }
  ]
}

当你操作对象init中的value值1改为2时,对应的js也会跟着改变var a = 2,当把name对应的值改成b时,对应的var a = 1也会改变var b = 1

不知道大家是否能想到什么呢?我总结的一个结论就是: AST并不难,不就是AST操作一组有规则的 JSON

AST的节点

7aa4ba83eea976c7.png

探索AST节点类型

常用节点含义对照表

a8443a28aa5f419b.png

大家不要觉得多,你真想懂AST是什么,就把这个类型详细的看一遍表达的意思,看完规则后瞬间明白AST的json中,那些看不懂的type它到底是个什么玩意了(详细信息可参考@babel/types),真的就是描述语法的词汇罢了!

jscodeshift简易操作

查找

api类型参数描述
findfntype:ast类型找到所有符合筛选条件的ast类型的ast字节,并返回一个array
forEachfnCallback:接受一个回调,默认传递被调用的ast节点遍历ast节点,仝js的forEach函数

更多可通过ast explore 在操作区console查看、或直接查看 jscodeshift/collections

我们先来看一个例子,假设有如下代码

import * as React from 'react';
import styles from './index.module.scss';
import { Button } from "@alifd/next";


const Button = () => {
  return (
    <div>
      <h2>转译前</h2>
      <div>
        <Button type="normal">Normal</Button>
        <Button type="primary">Prirmary</Button>
        <Button type="secondary">Secondary</Button>


        <Button type="normal" text>Normal</Button>
        <Button type="primary" text>Primary</Button>
        <Button type="secondary" text>Secondary</Button>


        <Button type="normal" warning>Normal</Button>
      </div>
    </div>
  );
};

export default Button;

执行文件(通过jscodeshift进行操作)

module.exports = (file, api) => {
  const j = api.jscodeshift;
  const root = j(file.source);
  root
    .find(j.ImportDeclaration, { source: { value: "@alifd/next" } })
    .forEach((path) => {
    path.node.source.value = "antd";
  })
  root
    .find(j.JSXElement, {openingElement: { name: { name: 'h2' } }})
    .forEach((path) => {
    path.node.children = [j.jsxText('转译后')]
  })
  root
    .find(j.JSXOpeningElement, { name: { name: 'Button' } })
    .find(j.JSXAttribute)
    .forEach((path) => {
    const attr = path.node.name
    const attrVal = ((path.node.value || {}).expression || {}).value ? path.node.value.expression : path.node.value

    if (attr.name === "type") {
      if (attrVal.value === 'normal') {
        attrVal.value = 'default'
      }
    }

    if (attr.name === "size") {
      if (attrVal.value === 'medium') {
        attrVal.value = 'middle'
      }
    }

    if (attr.name === "warning") {
      attr.name = 'danger'
    }

    if (attr.name === "text") {
      const attrType = path.parentPath.value.filter(item => item.name.name === 'type')
      attr.name = 'type'
      if (attrType.length) {
        attrType[0].value.value = 'link'
        j(path).replaceWith('')
      } else {
        path.node.value = j.stringLiteral('link')
      }

    }
  });

  return root.toSource();
}

该例代码大致解读如下

  1. 将js转换为ast
  2. 遍历代码中所有包含@alifd/next的引用模块,并做如下操作
  3. 改变该模块名为antd。
  4. 找到代码中标签名为h2的代码块,并修改该标签内的文案。
  5. 遍历代码中所有Button标签,并做如下操作
  6. 改变标签中type和size属性的值
  7. 改变标签中text属性变为 type = "link"
  8. 改变标签中warning属性为danger
  9. 返回由ast转换后的js。

最终输出结果

import * as React from 'react';
import styles from './index.module.scss';
import { Button } from "antd";


const Button = () => {
  return (
    <div>
      <h2>转译后</h2>
      <div>
        <Button type="default">Normal</Button>
        <Button type="primary">Prirmary</Button>
        <Button type="secondary">Secondary</Button>


        <Button type="link" >Normal</Button>
        <Button type="link" >Primary</Button>
        <Button type="link" >Secondary</Button>


        <Button type="default" danger>Normal</Button>
      </div>
    </div>
  );
};

export default Button;

代码说明

  1. 获取必要的数据

    // 获取操作ast用的api,获取待编译的文件主体内容,并转换为AST结构。
    // api.jscodeshift : 对jscodeshift库的引用
    // api.stats  : --dry运行期间收集统计信息的功能
    // api.report :  将传递的字符串打印到stdout
    const j = api.jscodeshift;
    const root = j(file.source); // file.source: 待操作的文件主体 file.path : 文件路径
    

    执行jscodeshift命令后,执行文件接收 3 个参数

代码转换

// root: 被转换后的ast跟节点  
root
// ImportDeclaration 对应 import 句式
  .find(j.ImportDeclaration, { source: { value: "@alifd/next" } })
  .forEach((path) => {
  // path.node 为import句式对应的ast节点
  path.node.source.value = "antd";
})

遍历代码中所有包含@alifd/next的引用模块,并做如下操作

  1. 改变模块名

    root
    // JSXElement 对应 element 完整句式,如 <h2 ...> ... </h2>
    // openingElement 对应 element 的 开放标签句式, 如 <h2 ...>
      .find(j.JSXElement, {openingElement: { name: { name: 'h2' } }})
      .forEach((path) => {
      // jsxText 对应 text
      path.node.children = [j.jsxText('转译后')]
    })
    
  2. 筛选标签为h2的html,更改该标签的内容的text为“转译后”

    root
    // 筛选Button的 element开放句式
      .find(j.JSXOpeningElement, { name: { name: 'Button' } })
    // JSXAttribute 对应 element 的 attribute 句式, 如 type="normal" ...
      .find(j.JSXAttribute)
      .forEach((path) => {
      const attr = path.node.name
      const attrVal = ((path.node.value || {}).expression || {}).value ? path.node.value.expression : path.node.value
    
      if (attr.name === "type") {
        if (attrVal.value === 'normal') {
          attrVal.value = 'default'
        }
      }
    
      if (attr.name === "size") {
        if (attrVal.value === 'medium') {
          attrVal.value = 'middle'
        }
      }
    
      if (attr.name === "warning") {
        attr.name = 'danger'
      }
    
      if (attr.name === "text") {
        // 判断该ast节点的兄弟节点是否存在 type,
        // 如果有,则修改type的值为link,如果没有则改变当前节点为type=“link”
        const attrType = path.parentPath.value.filter(item => item.name.name === 'type')
        attr.name = 'type'
        if (attrType.length) {
          attrType[0].value.value = 'link'
          j(path).replaceWith('')
        } else {
          // stringLiteral 对应 string类型字段值
          path.node.value = j.stringLiteral('link')
        }
    
      }
    });