likes
comments
collection
share

AST 在 AntV 中的应用

作者站长头像
站长
· 阅读数 15

背景

G2 升级到了 5.0 版本,官网示例都是 API 方式的调用,对于下游的 G2Plot、Ant Design Charts 等库,由于实现上的差异,示例语法也不一致,如何将 G2 官网所有示例一键转换为下游库的示例成了问题的关键,也决定了 ROI。

G2G2Plot
AST 在 AntV 中的应用AST 在 AntV 中的应用

解决方案

这有啥难的,CV 不就搞定了?CV 可以搞定,而且还不会出错,但对于数百+的示例,都去 CV 么?底层有更新的时候咋办?

作为开发者,我们要用技术的手段解决问题!

虽然 G2 官网有提供 Spec 的选项,但由于是运行时生成的,仓库里面并没有相关代码片段;其次是除了示例外,API 里面也会夹杂着很多示例代码,加之 G2 提供的 Spec 和下游实现会有部分出入,没法复用!

AST 在 AntV 中的应用

因此,我们可以借助 AST(Abstract Syntax Tree)的能力,将示例抽象为由各种节点组成的语法树,每个节点代表代码的不同部分,如表达式、语句、函数、变量等,对整棵树进行解析,抽取chartConfig config staticCode imported fetchURL等元数据,供下游组装。

AST 在 AntV 中的应用

G2 原始示例代码

import { Chart } from '@antv/g2';
import { regressionLog } from 'd3-regression';

const chart = new Chart({
  container: 'container',
  theme: 'classic',
  autoFit: true,
});

chart.data({
  type: 'fetch',
  value: 'https://assets.antv.antgroup.com/g2/logarithmic-regression.json',
});

chart
  .point()
  .encode('x', 'x')
  .encode('y', 'y')
  ...;

const logRegression = regressionLog()
  .x((d) => d.x)
  .y((d) => d.y)
  .domain([0.81, 35]);

chart
  .line()
  .data({
    transform: [
      {
        type: 'custom',
        callback: logRegression,
      },
    ],
  })
  ...;

chart.render();

AST parser:

  1. Imported AST 在 AntV 中的应用

  2. Static code AST 在 AntV 中的应用

  3. Chart config AST 在 AntV 中的应用

  4. Config AST 在 AntV 中的应用

代码实现

详见 github.com/ant-design/…

上层定制

实现非常简单,递归扫描目录,调用 parser 方法解析代码,调用 codeTemplate 方法生成代码模板,格式化之后生成对应文件即可。

const fs = require('fs');
const path = require('path');
const prettier = require('prettier');
const { parser } = require('./core');
const { codeTemplate } = require('./code-template');

/**
 * @param {string} dir 扫描目录
 * @description 递归扫描目录,调用 parser 方法解析代码,调用 codeTemplate 方法生成代码模板
 */
const scanDir = (dir) => {
  const files = fs.readdirSync(dir);
  files.forEach(file => {
    const filePath = path.resolve(dir, file);
    const stats = fs.statSync(filePath);
    if (stats.isFile()) {
      // 获取文件元信息
      const meta = parser(filePath);
      // 格式化代码
      const formattedCode = prettier.format(codeTemplate(meta), {
        semi: true,
        singleQuote: true,
        printWidth: 120,
        parser: 'babel',
      });
      // 示例代码生成
      fs.writeFileSync(path.resolve(__dirname, `./examples-parser/${file}`), formattedCode);
    } else {
      scanDir(filePath);
    }
  });
};

scanDir(path.resolve(__dirname, './examples'));

底层实现

暴露 2 个方法, 一个是核心的 parser ,一个是用于辅助解析 meta 的 transformSign 。

AST 在 AntV 中的应用

Global: 提供一个全局的 GLOBALSTATE ,用于提取 Static Code 。 由于 AST 解析代码时,存在嵌套结构,如果没有相关标识,会存在重复生成的情况,解决办法是记住 AST 的 start 和 end , 当新的片段进来时判断新片段的 start 和 end 是否在已有 range。

AST 在 AntV 中的应用

Utils: 提供一些常用方法,例如判断 CallExpression 是否为 fetch、处理 meta string 等

AST 在 AntV 中的应用

Parser: 核心函数,借助 babel 能力,将代码编译为 AST 语法树,并从中抽取对应的代码片段,并保存到 meta 上,最终通过 getResult 返回给下游处理

const fs = require('fs');
const babel = require('@babel/core');
const chalk = require('chalk');
const { get, pick } = require('lodash');

const meta = {
  fetchURL: '', // fetch url
  shape: '', // 图表类型
  imported: {}, // import 信息
  staticCode: [], // 静态代码
  config: [], // 配置项
  chartConfig: {}, // 图表配置项
  position: { min: 0, max: Infinity }, // 位置信息
  nextStatement: {}, // 暂存表达式
  currentEnd: Infinity, // 当前结束
  code: '', // 原始代码
};

// 状态重置
const reset = () => {
  RESETGLOBAL();
};

const getResult = () => {
  return pick(meta, 'chartConfig', 'config', 'staticCode', 'imported', 'fetchURL');
};

/**
 * @param {string} codePath
 * @description 解析代码,当 type 为 code 时,说明 params 是已经读取的代码,否则是文件路径
 */
const parser = (codePath) => {
  try {
    reset();
    meta.config.push({});
    meta.code = fs.readFileSync(codePath, 'utf-8');
      
    const visitorExpressions = {
        // import 信息
        ImportDeclaration(path) {
          const { node } = path;
          meta.imported = {
            ...meta.imported,
            [get(node, 'source.value')]: get(node, 'specifiers', []).map((item) => {
              return item.imported.name;
            }),
          };
        },
        // new Chart
        NewExpression(path) {
          const { node } = path;
          // .chart
          if (isNewExpression(node)) {
            meta.chartConfig = Object.assign(meta.chartConfig, getObjectValue(get(node, 'arguments.0'), meta.code));
          }
        },
        // 抽取静态代码 start end
        'FunctionDeclaration|VariableDeclaration|ExpressionStatement'(path) {
          setStaticCode(path);
        },
        CallExpression(path) {
          const { node } = path;
          const { start, end } = node;

          // .shape
          if (isShape(node)) {
            const shape = get(node, 'callee.property.name');
            getConfig()['type'] = shape;
          }
          
          if (isFetch(node)) {
            meta.fetchURL = get(node, 'arguments.0.value');
          }

          if (end > meta.currentEnd) {
            const lastConfig = getConfig();
            if (lastConfig.type) {
              Object.assign(lastConfig, meta.nextStatement);
            } else {
              meta.chartConfig = Object.assign(meta.chartConfig, meta.nextStatement);
            }
            meta.nextStatement = {};
          }

          meta.currentEnd = end;

          // 通用处理逻辑
          PIPELINE.forEach((item) => {
            const { key } = item;
            if (isMatchType(node, key)) {
              if (availabeCode(start, end)) {
                setConfigObject(key, node, getConfig(), item);
              } else {
                setConfigObject(key, node, meta.nextStatement, item);
              }
            }
          });
        },
      };
      const vistorPlugins = {
        visitor: visitorExpressions,
      };
      babel.transform(fs.readFileSync(params, 'utf-8'), {
        plugins: [vistorPlugins],
      });

    log(chalk.green(`解析成功:${params}`));
    return getResult();
  } catch (err) {
    log(chalk.red(`解析出错:params: ${params}; type: ${type}`));
    log(chalk.red(`出错信息:${err}`));
    return {
      hasError: true,
      errMessage: err,
    };
  }
};

module.exports = { parser, transformSign };

总结

AST 真的很强大,但我们日常开发中很少有交集,大家可以在 astexplorer.net/ 上面尝试解析一些代码,还是比较有趣的。