前言
介绍了如何使用抽象语法树(AST)将低代码平台的配置转换为源代码,并分享了具体的实现思路和示例。今日前端早读课文章由 @前端小付授权分享。
正文从这开始~~
一、抽象语法树(AST)
生成源码方案我这边使用的是抽象语法树,所以先带着大家了解一下抽象语法树。
1、什么是抽象语法树(AST)?
抽象语法树(Abstract Syntax Tree,简称 AST)是一种树状数据结构,用来表示源代码的语法结构。它将源代码中的每个元素映射成一个树形节点,节点之间的关系表示代码中的语法和结构。与传统的语法树不同,AST 省略了与语法相关的无关细节,比如空格和括号,而只关心代码的逻辑和语法结构。
2、作用
AST 主要用于编程语言的编译、解释和分析,尤其在 JavaScript 这样的解释型语言中非常重要。它的作用包括:
代码分析:
代码转换与优化:
代码生成:
编译器和工具通常会将 AST 转换回可执行代码或目标代码。例如,Babel 会将修改后的 AST 重新生成 JavaScript 代码。
【早阅】aiCoder:利用AST实现AI生成代码的合并工具
代码重构:
通过操作 AST,开发工具可以安全地进行代码重构(例如,重命名变量、函数提取等)。这种操作能够保持语法结构的正确性。
静态分析:
在代码检查、类型检查、错误检测等过程中,AST 使得分析工作更加高效。例如,ESLint 使用 AST 来检测代码是否符合某些风格或潜在的错误。
3、举个例子
我们可以在 AST 网站中输入代码,在右边可以实时看到代码对应的语法树。
把没用的属性去除掉,留下有用的部分。
{
"type": "VariableDeclaration",
"declarations": [
{
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "num"
},
"init": {
"type": "BinaryExpression",
"left": {
"type": "NumericLiteral",
"value": 1
},
"operator": "+",
"right": {
"type": "NumericLiteral",
"value": 1
}
}
}
],
"kind": "const"
}
下面给大家演示一下在 node 项目中把代码转换为语法树,需要先安装 @babel/parser 依赖。
const ast = require('@babel/parser').parse('const num = 1 + 1');
console.log(JSON.stringify(ast, null, 2));
运行上面代码后输出
{
"type": "File",
"start": 0,
"end": 17,
"loc": {
"start": {
"line": 1,
"column": 0,
"index": 0
},
"end": {
"line": 1,
"column": 17,
"index": 17
}
}
,
"errors": [],
"program": {
"type": "Program",
"start": 0,
"end": 17,
"loc": {
"start": {
"line": 1,
"column": 0,
"index": 0
},
"end": {
"line": 1,
"column": 17,
"index": 17
}
},
"sourceType": "script",
"interpreter": null,
"body": [
{
"type": "VariableDeclaration",
"start": 0,
"end": 17,
"loc": {
"start": {
"line": 1,
"column": 0,
"index": 0
},
"end": {
"line": 1,
"column": 17,
"index": 17
}
},
"declarations": [
{
"type": "VariableDeclarator",
"start": 6,
"end": 17,
"loc": {
"start": {
"line": 1,
"column": 6,
"index": 6
},
"end": {
"line": 1,
"column": 17,
"index": 17
}
},
"id": {
"type": "Identifier",
"start": 6,
"end": 9,
"loc": {
"start": {
"line": 1,
"column": 6,
"index": 6
},
"end": {
"line": 1,
"column": 9,
"index": 9
},
"identifierName": "num"
},
"name": "num"
},
"init": {
"type": "BinaryExpression",
"start": 12,
"end": 17,
"loc": {
"start": {
"line": 1,
"column": 12,
"index": 12
},
"end": {
"line": 1,
"column": 17,
"index": 17
}
},
"left": {
"type": "NumericLiteral",
"start": 12,
"end": 13,
"loc": {
"start": {
"line": 1,
"column": 12,
"index": 12
},
"end": {
"line": 1,
"column": 13,
"index": 13
}
},
"extra": {
"rawValue": 1,
"raw": "1"
},
"value": 1
},
"operator": "+",
"right": {
"type": "NumericLiteral",
"start": 16,
"end": 17,
"loc": {
"start": {
"line": 1,
"column": 16,
"index": 16
},
"end": {
"line": 1,
"column": 17,
"index": 17
}
},
"extra": {
"rawValue": 1,
"raw": "1"
},
"value": 1
}
}
}
],
"kind": "const"
}
],
"directives": []
},
"comments": []
}
上面我们实现了把代码转换为抽象语法树,下面再给大家演示一下通过抽象语法树生成代码。
安装
@babel/types
和
@babel/generator
依赖
const t = require('@babel/types');
const g = require('@babel/generator')
const ast = t.program(
[
t.variableDeclaration(
'const',
[
t.variableDeclarator(
t.identifier('num'),
t.binaryExpression(
'+',
t.numericLiteral(1),
t.numericLiteral(1)
)
)
]
)
]
)
const code = g.default(ast).code;
console.log(code);
运行上面代码后输出
二、低代码生成代码实战
1、实现思路
使用 node 起一个 express 服务,对外暴露生成代码接口,前端调用这个接口,并且把当前页面 json 数据传到后端,后端解析 json 数据生成抽象语法树,然后通过抽象语法树生成代码。
前面我实现过一个低代码 demo 项目,就拿这个项目来说吧。建议大家可以先看一下我前面做的低代码平台。
2、实战
在前端低代码页面拖一个按钮到画布,然后点击生成代码按钮,调用生成代码接口。
页面 json 数据
{
"components": [
{
"id": 1,
"name": "Page",
"props": {},
"desc": "页面",
"fileName": "page",
"children": [
{
"id": 1740045570763,
"fileName": "button",
"name": "Button",
"props": {
"text": {
"type": "static",
"value":
"按钮"
}
},
"desc": "按钮",
"parentId": 1
}
]
}
]
}
把 json 数据转换为 jsx 元素
const t = require('@babel/types');
const g = require('@babel/generator')
const prettier = require('prettier');
function createJsxStatement(component) {
// 创建 jsx 元素
return t.jsxElement(
t.jsxOpeningElement(
t.jsxIdentifier(component.name),
[]
),
t.jsxClosingElement(
t.jsxIdentifier(component.name),
[]
),
// 递归创建子元素
(component.children || []).map(createJsxStatement)
);
}
function generateCode(components) {
// 创建一个 App 方法
const ast = t.functionDeclaration(
t.identifier("App"),
[],
// 创建方法内部的语句
t.blockStatement([
// 创建 return 语句
t.returnStatement(
// 创建 <>
t.jsxFragment(
t.jsxOpeningFragment(),
t.jsxClosingFragment(),
components.map(createJsxStatement)
)
)
])
)
// 格式化代码
return prettier.format(
g.default(ast).code,
{ parser: 'babel' }
);
}
module.exports = {
generateCode
}
生成的代码
给组件加属性,遍历组件配置里的 props
上面代码并不能在项目里直接运行,因为没有导入组件,那我们再用抽象语法树动态生成导入语句。
完整代码
const t = require('@babel/types');
const g = require('@babel/generator')
const prettier = require('prettier');
let importStatements = new Map();
function createJsxStatement(component) {
const attrs = [];
Object.keys(component.props).forEach(key => {
const propValue = component.props[key];
if (typeof propValue === 'object') {
console.log(propValue.value)
attrs.push(
t.jsxAttribute(
t.jsxIdentifier(key),
t.stringLiteral(propValue.value)
)
)
}
})
;
// 生成导入语句,如果已经导入了则跳过
if (!importStatements.has(component.name)) {
importStatements.set(component.name,
t.importDeclaration(
[t.importDefaultSpecifier(t.identifier(component.name))],
t.stringLiteral(`@/editor/components/${component.fileName}/prod`)
)
)
}
// 创建 jsx 元素
return t.jsxElement(
t.jsxOpeningElement(
t.jsxIdentifier(component.name),
attrs
),
t.jsxClosingElement(
t.jsxIdentifier(component.name),
),
// 递归创建子元素
(component.children || []).map(createJsxStatement)
);
}
function generateCode(components) {
importStatements = new Map();
// 默认导入 react和 useRef、useState
importStatements.set("react",
t.importDeclaration(
[
t.importDefaultSpecifier(t.identifier('React')),
t.importSpecifier(
t.identifier('useRef'),
t.identifier('useRef')
),
t.importSpecifier(
t.identifier('useState'),
t.identifier('useState')
)
],
t.stringLiteral('react')
)
);
// 创建一个 App 方法
const funcStatement = t.functionDeclaration(
t.identifier("App"),
[],
// 创建方法内部的语句
t.blockStatement([
// 创建 return 语句
t.returnStatement(
// 创建 <>
t.jsxFragment(
t.jsxOpeningFragment(),
t.jsxClosingFragment(),
components.map(createJsxStatement)
)
)
])
)
const ast = t.program(
[
...importStatements.values(),
funcStatement,
// 生成默认导出 App 方法
t.exportDefaultDeclaration(
t.identifier("App")
)
]
)
// 格式化代码
return prettier.format(
g.default(ast, {
jsescOption: { minimal: true },
}).code,
{ parser: 'babel' }
);
}
module.exports = {
generateCode
}
接下来我们来支持动态生成事件,在低代码页面拖一个按钮和一个弹框,给按钮添加点击事件调用弹框显示方法。
生成代码传给后端的数据
判断 key 是不是以 on 开头,如果是表示事件
目前方法内部还没实现,接下来我们实现一下方法内部。通过配置可以知道方法内部其实就是调用 modal 组件的 open 方法,调用一个组件内部方法,需要用到 ref,所以我们需要为所有组件都创建对应的 ref。
支持组件属性绑定变量。先在低代码页面上定义一个变量。
再拖一个按钮,给当前按钮文本绑定变量
再给按钮点击事件添加方法,改变变量的值。
点击生成代码,把前端定义的变量传给后端
后端根据传过来变量动态生成 useState 语句。
在对应的实现方法中调用 set 方法设置值
组件属性绑定变量,而不是直接写死字符串
生成的代码
把生成的代码复制项目里测试一下
点击一下按钮
三、完整代码
const t = require('@babel/types');
const g = require('@babel/generator')
const prettier = require('prettier');
let importStatements = new Map();
let eventHandleStatements = [];
let refStatements = [];
let stateStatements = [];
// 首字母大写
const capitalize = str => str.charAt(0).toUpperCase() + str.slice(1);
function generateEventHandleStatement(config) {
if (config.type === 'ComponentMethod') {
return t.expressionStatement(
t.callExpression(
t.memberExpression(
t.memberExpression(
t.identifier(`component_${config.config.componentId}_ref`),
t.identifier("current")
),
t.identifier(config.config.method)
),
[]
),
)
} else if (config.type === 'SetVariable') {
return t.expressionStatement(
t.callExpression(
t.identifier(`set${capitalize(config.config.variable)}`),
[t.stringLiteral(config.config.value)]
)
)
}
}
function createJsxStatement