专栏名称: 前端早读课
我们关注前端,产品体验设计,更关注前端同行的成长。 每天清晨五点早读,四万+同行相伴成长。
目录
相关文章推荐
前端早读课  ·  【第3463期】使用抽象语法树把低代码配置转 ... ·  22 小时前  
前端早读课  ·  【图书】零基础开发AI ... ·  昨天  
前端早读课  ·  【第3462期】7 分钟深度理解柯里化 ·  昨天  
前端早读课  ·  【第3461期】多种序列帧格式的最佳实践 ·  2 天前  
前端大全  ·  vue实现预览编辑ppt、word、pdf、 ... ·  3 天前  
51好读  ›  专栏  ›  前端早读课

【第3463期】使用抽象语法树把低代码配置转换成源码

前端早读课  · 公众号  · 前端  · 2025-02-27 08:00

正文

前言

介绍了如何使用抽象语法树(AST)将低代码平台的配置转换为源代码,并分享了具体的实现思路和示例。今日前端早读课文章由 @前端小付授权分享。

正文从这开始~~

一、抽象语法树(AST)

生成源码方案我这边使用的是抽象语法树,所以先带着大家了解一下抽象语法树。

1、什么是抽象语法树(AST)?

抽象语法树(Abstract Syntax Tree,简称 AST)是一种树状数据结构,用来表示源代码的语法结构。它将源代码中的每个元素映射成一个树形节点,节点之间的关系表示代码中的语法和结构。与传统的语法树不同,AST 省略了与语法相关的无关细节,比如空格和括号,而只关心代码的逻辑和语法结构。

2、作用

AST 主要用于编程语言的编译、解释和分析,尤其在 JavaScript 这样的解释型语言中非常重要。它的作用包括:

代码分析:

  • 通过生成 AST ,可以深入理解和分析代码。开发工具和编辑器(如 VSCode、ESLint 等)都依赖 AST 来进行语法检查、代码提示、重构等操作。

  • 工具可以扫描 AST 以识别潜在的错误或不符合规范的代码风格。

代码转换与优化:

  • AST 是许多代码转换工具(如 Babel、TypeScript)和编译器的核心。它允许你在语言层次上操作和转换代码。例如,可以将 ES6+ 代码转换为 ES5 代码,或者将 TypeScript 转换为 JavaScript。

  • AST 也可以用于优化代码,删除冗余的代码、合并表达式等。

代码生成:

编译器和工具通常会将 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 依赖

  • @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






请到「今天看啥」查看全文