AST 在 AntV 中的应用
背景
G2 升级到了 5.0 版本,官网示例都是 API 方式的调用,对于下游的 G2Plot、Ant Design Charts 等库,由于实现上的差异,示例语法也不一致,如何将 G2 官网所有示例一键转换为下游库的示例成了问题的关键,也决定了 ROI。
G2 | G2Plot |
---|---|
![]() | ![]() |
解决方案
这有啥难的,CV 不就搞定了?CV 可以搞定,而且还不会出错,但对于数百+的示例,都去 CV 么?底层有更新的时候咋办?
作为开发者,我们要用技术的手段解决问题!
虽然 G2 官网有提供 Spec 的选项,但由于是运行时生成的,仓库里面并没有相关代码片段;其次是除了示例外,API 里面也会夹杂着很多示例代码,加之 G2 提供的 Spec 和下游实现会有部分出入,没法复用!
因此,我们可以借助 AST(Abstract Syntax Tree)的能力,将示例抽象为由各种节点组成的语法树,每个节点代表代码的不同部分,如表达式、语句、函数、变量等,对整棵树进行解析,抽取chartConfig
config
staticCode
imported
fetchURL
等元数据,供下游组装。
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:
-
Imported
-
Static code
-
Chart config
-
Config
代码实现
上层定制
实现非常简单,递归扫描目录,调用 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 。
Global: 提供一个全局的 GLOBALSTATE ,用于提取 Static Code 。 由于 AST 解析代码时,存在嵌套结构,如果没有相关标识,会存在重复生成的情况,解决办法是记住 AST 的 start 和 end , 当新的片段进来时判断新片段的 start 和 end 是否在已有 range。
Utils: 提供一些常用方法,例如判断 CallExpression 是否为 fetch、处理 meta string 等
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/ 上面尝试解析一些代码,还是比较有趣的。
转载自:https://juejin.cn/post/7257708221360701499