Zero Code New Ideas: Orchestrating Page Logic with LogicFlow and @logicflow/engine
【免费下载链接】LogicFlowA flow chart editing framework focus on business customization. 专注于业务自定义的流程图编辑框架,支持实现脑图、ER图、UML、工作流等各种图编辑场景。项目地址: https://gitcode.com/GitHub_Trending/lo/LogicFlow
在滴滴客服业务中,零代码平台已经能够通过拖拽配置出大量页面,但传统的"组件属性面板"式配置在面对复杂页面交互时暴露出难以扩展的瓶颈。本文将基于 LogicFlow 开源仓库中收录的技术方案(lowcode-with-logicflow.en.md),系统讲解"用流程图编排页面逻辑"这一零代码新思路:如何用 LogicFlow 的自定义节点/边能力构建编排器(Organizer),用可在 JS 环境运行的LogicFlow Engine构建执行器(Executor),并用执行记录与页面数据构建调试器(Debugger)。读完本文,你将掌握页面逻辑编排的三阶段研发闭环,并能直接基于仓库内的@logicflow/engine子包与示例代码落地自己的页面逻辑编排器与执行器。
背景:零代码的便利与困境
客服业务是连接用户与公司各业务部门的桥梁。为了快速响应业务迭代并持续提升用户体验,技术团队需要搭建多个系统,让运营人员通过配置的方式完成页面搭建。零代码平台的拖拽交互与"所见即所得"(WYSIWYG)设计天然契合"非研发人员也能配置页面"这一诉求。
据该文章介绍,过去 4 年中,滴滴客服内部的多个系统已通过配置产出了12,900+ PC 页面与7,900+ H5 页面,覆盖整个服务链路,以多种形式解决用户问题,例如让运营人员编排智能客服流程、让人工客服按照运营设计的流程处理并记录用户问题。
然而,随着业务增长,零代码的短板逐渐显现。以一个典型的"表格修改操作"需求为例:点击操作列的"编辑"按钮时,需要先请求服务端接口获取当前用户的权限信息,再根据返回的权限展示不同的编辑弹窗。如果采用主流零代码方案,就得在表格组件的属性面板中不断叠加"请求接口""弹窗展示"等逻辑。这种在组件属性面板里做功能迭代的方式,只会导致:
- 配置内容越来越臃肿;
- 配置难度不断上升;
- 研发与维护成本变高;
- 最终体验"还不如直接写代码"。
思路:把"写代码"抽象成"画流程图"
回顾用 JS 代码实现上述表格编辑需求的过程,本质上是固定的处理链路:
监听事件 -> 收集数据 -> 发起请求 -> 解析返回结果 -> 修改页面组件内容既然代码流程可以被抽象成一条固定链路,那是否可以把这段逻辑"画"成流程图,用节点编排来实现同样的效果?答案是肯定的。文章将上述需求拆分成了 4 个最简单的逻辑单元:
- 表格触发编辑事件(事件节点)
- 请求权限信息(数据节点)
- 展示普通管理员编辑弹窗(行为节点)
- 展示高级管理员编辑弹窗(行为节点)
将它们按流程图画出来后,就能通过自由组合节点实现页面逻辑的灵活配置。例如:
- 当服务端接口响应较慢时,只需在请求节点前插入一个"加载中"节点;
- 当"单选框 A 选中 a1"时展示组件 B、隐藏组件 C;"单选框 A 选中 a2"时展示组件 C、隐藏组件 B——这条交互可拆成 5 个节点,并在连线上分别配置"单选框 A 选中 a1"与"单选框 A 选中 a2"两个条件。
由此得出该方案的核心方法论:
将复杂交互中各组件的变化抽象为一个个单元逻辑,再用流程图中的节点编排将它们表达出来。
这一思路很好地解决了零代码难以扩展的痛点,同时保留了"非程序员也能配置"的零代码优势——整个"开发"过程依然是可视化、拖拽式的。
方案:页面逻辑编排的三大核心能力
为了让"流程编排"真正替代"写代码",团队围绕研发流程的开发、运行、调试三个阶段,分别实现了三个核心组件:
| 组件 | 对应研发阶段 | 职责 |
|---|---|---|
| 编排器(Organizer) | 开发 | 通过拖拽节点与连线,产出描述流程图的 JSON 数据 |
| 执行器(Executor) | 运行 | 在浏览器 JS 环境中按流程图逻辑驱动页面组件 |
| 调试器(Debugger) | 调试 | 在不改代码的前提下运行并检查页面逻辑 |
由于整个方案围绕流程图展开,对流程图编辑框架提出了很高的定制要求。LogicFlow 的强大自定义能力恰好满足这些需求。其整体架构中,核心包@logicflow/core提供流程图编辑器的基本能力,@logicflow/extension则是在核心扩展能力之上开发的插件集合(详见 packages/core 与 packages/extension)。
下面逐一深入三大组件,并结合仓库源码说明其实现原理。
编排器(Organizer):把"开发产物"变成流程图 JSON
在传统开发模式下,开发产物是 JS 代码;而在流程编排模式下,开发产物变成描述流程图的 JSON 数据。由于流程图主要由节点(node)与边(edge)构成,编排器的核心工作便是自定义节点与边。
自定义节点:一个节点 = 一个页面逻辑单元
编排器的流程图支持多种节点类型,每种节点代表一个最基本的页面逻辑单元:
- 事件节点(Event Node):可理解为 JS 的事件监听器;
- 数据节点(Data Node):可理解为 Ajax 请求;
- 行为节点(Behavior Node):可理解为修改页面组件属性;
- 页面跳转节点(Page Jump Node):可理解为浏览器跳转到新页面。
为了给不同功能节点实现理想的外观与交互,编排器使用了 LogicFlow 的自定义 HTML 节点能力,可以完美支持高亮同步、悬浮按钮、悬浮提示等控件,提供流畅的"开发"体验。
在仓库的 HtmlNode 源码 中可以看到,自定义 HTML 节点的核心就是重写setHtml方法——它接收一个可供挂载的 DOM 节点(rootEl),开发者可以在里面挂载任意 HTML 内容:
export class HtmlNode<P extends IHtmlNodeProps = IHtmlNodeProps> extends BaseNode<P> { /** * @overridable 支持重写 * 自定义HTML节点内容 * @param {HTMLElement} rootEl 自定义HTML节点内容可以挂载的dom节点 */ setHtml(rootEl: SVGForeignObjectElement) { rootEl.appendChild(document.createElement('div')) } }在编排器场景中,节点内容是用 Vue(或 React)编写的,写法与平时开发组件完全一致,也可以直接使用 UI 组件库。其实现思路与上述源码一致:在自定义 HTML 节点中重写setHtml,在其内部挂载一个 Vue/React 根组件即可,例如:
// 自定义事件节点视图:挂载 Vue 组件 class EventHtmlNode extends HtmlNode { setHtml(rootEl) { const app = createApp(EventNodeContent) // Vue 组件,内部可使用任意 UI 组件库 app.mount(rootEl) } }自定义边:用"条件图标 + Popover"代替文本
在编排器中,边控制节点的执行顺序:默认情况下,一条边表示"上一个节点执行完成,继续执行下一个节点"。你可以在边上配置条件来中断流程,这与 JS 代码中的逻辑判断(if)功能类似。
编排器的边有个特殊需求:边上不需要显示文本,而是显示一个带 Popover 的条件图标。这需要借助 LogicFlow 的自定义边机制,重写默认的文本逻辑,把文本替换成图标。
默认情况下,LogicFlow 的文本是 SVG 元素。当需要在文本位置承载更丰富的 HTML 内容时,可以基于继承重写的自定义机制来重新实现文本——重写getText函数,并在 SVG 中插入一个foreignObject,从而把 HTML 内容嵌套进 SVG 内部:
class ConditionEdge extends PolylineEdge { getText() { // 用 foreignObject 承载 HTML 内容(条件图标 + Popover) return ( <foreignObject ...> <ConditionIcon onClick={...} /> </foreignObject> ) } }执行器(Executor):基于 LogicFlow Engine 驱动流程图运行
页面逻辑编排以流程图配置为基础,要让流程图按编排逻辑运行,最常用的手段是流程引擎。然而市面上的流程引擎大多运行在服务端(如 Activity、Flowable、Turbo 等),不适合浏览器环境下的页面逻辑执行。因此团队选择实现一个能在 JS 环境中运行的流程引擎——LogicFlow Engine,并在此基础上实现执行器。该引擎已随 LogicFlow 一起开源,位于本仓库的 packages/engine。
引擎的最小使用方式
从 GetStarted 示例 可以看到引擎最核心的用法——加载流程图数据并执行:
import { Engine } from '@logicflow/engine' const engine = new Engine() const flowData = { graphData: { nodes: [ { id: 'node1', type: 'StartNode', properties: {} }, { id: 'node2', type: 'TaskNode', properties: {} }, { id: 'node3', type: 'TaskNode', properties: {} }, ], edges: [ { id: 'edge1', sourceNodeId: 'node1', targetNodeId: 'node2', properties: { conditionExpression: 'a === 1' }, }, { id: 'edge2', sourceNodeId: 'node1', targetNodeId: 'node3', properties: { conditionExpression: 'a === 2' }, }, ], }, globalData: { a: 2 }, } engine.load(flowData) const result = await engine.execute() // 获取执行结果 const execution = await engine.getExecutionRecord(result?.executionId) // 获取执行记录在 Engine 源码 中可以看到,new Engine()会默认注册StartNode与TaskNode两个内置节点,并通过register()方法开放节点注册能力:
constructor(options?: Engine.Options) { this.nodeModelMap = new Map() this.instanceId = createEngineId() if (options?.debug) { this.recorder = new Recorder({ instanceId: this.instanceId }) } // 默认注册节点 register default nodes this.register({ type: StartNode.nodeTypeName, model: StartNode }) this.register({ type: TaskNode.nodeTypeName, model: TaskNode }) this.context = options?.context || {} } register(nodeConfig: Engine.NodeConfig) { this.nodeModelMap.set(nodeConfig.type, nodeConfig.model) }load()支持传入graphData、startNodeType(默认'StartNode')与globalData三个参数;execute()可被多次调用以支持流程重复执行。
能力一:支持多种类型节点
执行器包含事件节点、行为节点、请求数据节点、数据转换节点、页面跳转节点等,每个节点都有独特功能。而 LogicFlow Engine 目前只内置了开始节点(StartNode)和任务节点(TaskNode),你可以继承这些节点来实现自定义业务逻辑。
以"执行器的请求数据节点"为例:重写任务节点的action方法,在方法体内实现请求数据的逻辑即可。TaskNode 源码 非常简单,它继承自 BaseNode,而BaseNode.action()就是预留的扩展点:
// packages/engine/src/nodes/task.ts export default class TaskNode extends BaseNode { readonly baseType = 'task' static nodeTypeName = 'TaskNode' }// 自定义请求数据节点:重写 action 方法 class RequestDataNode extends TaskNode { async action({ executionId, nodeId, actionId }) { const res = await this.context.request.get(this.properties.url) this.globalData[this.properties.key] = res.data } }注意BaseNode中context与globalData的设计(base.ts):globalData是调用流程时传入的全局数据,在计算表达式时基于它进行;context是外部传入的上下文,例如可以把request、router等能力注入进去,让节点内部通过this.context.request.get(url)调用(参见 FlowModel 注释)。
能力二:支持并发执行
编排逻辑时经常遇到"一个事件发生后要同时做多件事"的场景。例如点击按钮后,既要发起请求更新数据,又要更新页面上的某些文本。对大多数人来说,用分支配置更"直观",这就要求执行器支持这种"并发"执行。
在 Scheduler 源码 中可以清楚地看到 LogicFlow Engine 默认的"并行网关"实现:run()会把同一executionId下待执行的节点一次性全部取出,逐个并发执行,避免某个节点执行时间过长导致其他节点长时间等待:
public run(runParam: Scheduler.ActionParam) { const nodeQueue = this.nodeQueueMap.get(runParam.executionId) // 将同一个 executionId 当前待执行的节点一起执行 // 避免出现某一个节点执行时间过长,导致其他节点等待时间过长 while (nodeQueue?.length) { const currentNode = nodeQueue.pop() const actionId = createActionId() const actionParam = { ...(currentNode as Engine.NodeParam), actionId } this.pushActionToRunningMap(actionParam) this.exec(actionParam) // 不 await,非阻塞并发执行 } // ... }配合 Scheduler.next():当一个节点执行完成,它会遍历所有出边,把条件结果为真的后续节点全部加入队列:
private next(data: Engine.NextActionParam) { if (data.outgoing && data.outgoing.length > 0) { data.outgoing.forEach((item) => { if (item.result) { this.addAction({ executionId: data.executionId, nodeId: item.target }) } }) } this.saveActionResult(data) this.removeActionFromRunningMap(data) this.run(data) }因此上文示例的执行顺序为:点击搜索按钮 -> 同时"请求数据"与"更新文本" -> 更新数据。边上的条件表达式由 BaseNode.isPass() 基于globalData求值,表达式写在边的properties.conditionExpression中。如果你想实现互斥网关(exclusive gateway),按照文章的方案,可以重写节点的getOutgoing相关逻辑,只放行满足条件的那一条出边。
能力三:支持多个开始节点
大多数页面会有多个可绑定事件的组件,因此一张流程图上也常常存在多个事件节点。在执行器中,事件节点是流程开始执行的起点,这就要求 LogicFlow Engine支持一张流程图上存在多个开始节点。
从 FlowModel.load() 可以看到:load解析graphData的nodes与edges,把边转换成节点的incoming/outgoing属性,并把所有type === startNodeType的节点收集到startNodes数组中。执行时,FlowModel.createExecution() 会把所有的开始节点都加入调度器:
this.startNodes.forEach((startNode) => { this.scheduler.addAction({ executionId, nodeId: startNode.id }) }) this.scheduler.run({ executionId })更强大的是,LogicFlow Engine 不仅支持一张流程有多个开始节点,还支持从流程中的任意节点开始执行,以及从已执行过的节点重新执行。执行器正是利用这一点:当页面某个组件触发事件时,执行器找到该组件对应的节点,然后从该节点开始执行。在createExecution中可以看到,只要传入nodeId且该节点存在,就会直接以这个节点作为开始节点:
if (execParam?.nodeId) { const nodeConfig = this.nodeConfigMap.get(execParam.nodeId) if (!nodeConfig) { /* 报错:节点不存在 */ } // 当指定了开始节点,且该节点存在,则直接以这个节点开始执行 this.startNodes = [nodeConfig] }能力四:流程可重复执行
表单内部经常遇到"选择不同内容、展示不同组件"的需求(例如前面提到的单选框 A 切换 a1/a2 控制组件 B/C 显隐)。为了满足用户不停切换单选框选项,流程引擎需要能够迭代执行。
LogicFlow Engine 支持同一个流程实例被多次调用执行。这得益于 FlowModel 内部的待执行队列设计:当流程正在执行时,如果再次触发执行,执行参数会被放入executeList队列,等上一次执行完成后再继续执行,避免多次执行之间的数据冲突;而每次执行都会生成一个唯一的executionId用于区分。这一点在execute()的注释中有明确说明(FlowModel.ts#L232-L242):
同一次执行内部,节点执行顺序为并行(避免异步节点阻塞其他节点);多次执行之间为串行(避免多次执行之间的数据冲突)。
此外,引擎还支持流程中断与恢复:自定义节点可以让流程中断,再通过engine.resume()恢复执行,仓库中的 07_interruptedAndResume.test.ts 即为对应的测试用例。
调试器(Debugger):不修改代码即可排查页面逻辑
编排器与执行器让页面逻辑的"可视化编排"与"可视化执行"成为现实。但即便可视化"开发"降低了"写代码"的难度,错误依然不可避免。因此,团队还提供了"调试"功能,让配置者无需修改代码即可运行并检查页面逻辑。
调试器主要依赖引擎的debug模式与 Recorder 执行记录能力。在 Engine 源码 中,new Engine({ debug: true })时会自动创建 Recorder;Recorder 会记录每个节点的执行动作,默认在浏览器使用sessionStorage、在 Node.js 使用内存存储(参见 setCustomRecorder 注释),并内置MAX_RECORDER = 100、MAX_INSTANCE = 100的上限保护(recorder/index.ts),执行记录不会自动删除,可自行清理。
调试器提供了三大能力:
能力一:运行记录(Operating Record)
每次触发事件节点的操作都会生成一条运行时记录,调试器以流程图路径的形式展示运行记录,可视化呈现每次操作触发了哪些流程节点。对应到引擎层,就是 Engine.getExecutionList() 与 getExecutionRecord(executionId)——前者获取该引擎实例的所有执行,后者按executionId取回某次执行中按顺序记录的节点动作列表,正好可以还原为一条流程图路径。
能力二:元素详情(Element Details)
在运行记录中点击已执行的节点,可以查看该节点的运行时内容,包括元素数据、错误原因等。引擎的 Recorder 在记录动作时保存了完整信息(addActionRecord):timestamp、actionId、nodeId、nodeType、properties、outgoing、status、detail等。其中status区分success / error / interrupted(见 index.ts 的 ActionStatus 定义),detail可承载错误详情,从而支撑"查看错误原因"这一调试诉求。
能力三:页面数据(Page Data)
页面数据指当前运行环境中的全局数据,包含两类:
- 动态数据:当前页面运行时产生的数据(如接口返回的数据、组件值等);
- 初始化数据:页面初始化时传入的数据(宿主系统传入的数据、URL 数据等)。
页面数据可以帮助配置者排查页面错误。在引擎层,对应的是globalData机制:load()时可传入globalData(index.ts#L63-L80),引擎还提供 getGlobalData / setGlobalData / updateGlobalData 三个方法读写它。全局数据会传递给每个节点,并作为边条件表达式求值的依据。
总结与展望
通过"页面逻辑编排 + 执行 + 调试"这套体系,团队在很大程度上解决了传统零代码技术难以扩展的问题,并且可以在这种架构设计之上,进一步封装出更简单的编排器,保证可扩展性、降低系统自身的维护成本。
当然,零代码领域还有很多难题待解,例如数据源、复杂算法、数据转换等。文章中提到,滴滴客服内部零代码平台在这些方面已有大量实践,并仍在实际使用中持续打磨。
值得一提的是,LogicFlow Engine 已作为 LogicFlow 的子包开源(本仓库 packages/engine),它不仅可用于页面逻辑执行,还能应用于许多其他场景。你可以参考 Engine 示例 与 条件表达式测试用例(该用例验证了"条件表达式为 false 时流程不再继续执行",并检查了执行记录长度与出边条件结果),尝试用它实现你自己的页面逻辑编排器与执行器。如果你有更多疑问,欢迎通过 issue 提交反馈,也欢迎为 LogicFlow 仓库提交 issue 与 PR。
【免费下载链接】LogicFlowA flow chart editing framework focus on business customization. 专注于业务自定义的流程图编辑框架,支持实现脑图、ER图、UML、工作流等各种图编辑场景。项目地址: https://gitcode.com/GitHub_Trending/lo/LogicFlow
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考