
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 HtmlNodeP extends IHtmlNodeProps IHtmlNodeProps extends BaseNodeP { /** * 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.tsglobalData是调用流程时传入的全局数据在计算表达式时基于它进行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 })时会自动创建 RecorderRecorder 会记录每个节点的执行动作默认在浏览器使用sessionStorage、在 Node.js 使用内存存储参见 setCustomRecorder 注释并内置MAX_RECORDER 100、MAX_INSTANCE 100的上限保护recorder/index.ts执行记录不会自动删除可自行清理。调试器提供了三大能力能力一运行记录Operating Record每次触发事件节点的操作都会生成一条运行时记录调试器以流程图路径的形式展示运行记录可视化呈现每次操作触发了哪些流程节点。对应到引擎层就是 Engine.getExecutionList() 与 getExecutionRecord(executionId)——前者获取该引擎实例的所有执行后者按executionId取回某次执行中按顺序记录的节点动作列表正好可以还原为一条流程图路径。能力二元素详情Element Details在运行记录中点击已执行的节点可以查看该节点的运行时内容包括元素数据、错误原因等。引擎的 Recorder 在记录动作时保存了完整信息addActionRecordtimestamp、actionId、nodeId、nodeType、properties、outgoing、status、detail等。其中status区分success / error / interrupted见 index.ts 的 ActionStatus 定义detail可承载错误详情从而支撑查看错误原因这一调试诉求。能力三页面数据Page Data页面数据指当前运行环境中的全局数据包含两类动态数据当前页面运行时产生的数据如接口返回的数据、组件值等初始化数据页面初始化时传入的数据宿主系统传入的数据、URL 数据等。页面数据可以帮助配置者排查页面错误。在引擎层对应的是globalData机制load()时可传入globalDataindex.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),仅供参考