FEATURED · 精选文章

Next.js+React Flow构建AI工作流可视化编排平台

发布时间 / 2026/9/9 5:41:15
来源 / 创域科博编辑部
栏目 / 资讯中心
Next.js+React Flow构建AI工作流可视化编排平台 1. 项目概述这不是又一个拖拽demo而是一套能跑通真实AI任务的“工作流操作系统”我用 Next.js React Flow 从零搭建了一个可视化 AI 工作流编排平台——这句话在技术社区里听起来像极了那些“5分钟上手”的营销标题。但实话讲当我把第一个带LLM调用、条件分支、并行执行和错误重试的完整工作流在生产环境跑通时后台日志里跳出的那行workflow_id: wf_8a3f2d1b → status: completed比任何Demo演示都让我踏实。它不是玩具是我在服务三个内部AI产品线过程中被反复卡在“模型写好了但怎么串起来谁来改参数运营同学想换提示词还得找前端发版”这些现实问题逼出来的解决方案。核心关键词就五个Next.js、React Flow、AI工作流、可视化编排、低代码。注意这里说的“低代码”不是指完全屏蔽技术细节而是把80%的重复性胶水逻辑HTTP请求封装、状态流转控制、错误兜底、日志埋点收进平台层让业务方聚焦在“这个节点该调哪个模型”、“这条边的条件表达式怎么写”、“失败后是重试还是告警”这种真正有业务价值的决策上。比如市场部同事现在能自己拖一个“用户评论情感分析→高风险评论自动转人工→中性评论打标签→全部结果存ES”这样的流程全程不用碰一行fetch代码也不用等我排期。而作为开发者我反而获得了更强的掌控力所有节点的输入输出契约被强制校验每个环节的耗时、成功率、token消耗都被自动采集连调试都变成点击某个节点直接看它的入参快照和原始响应体。这个平台解决的不是“能不能做”而是“能不能可持续地、多人协作地、可审计地、可灰度地做”。它横跨了前端工程化、AI服务治理、状态机建模和用户体验设计四个维度。适合三类人参考一是正在选型AI应用架构的Tech Lead你会看到如何用现有前端基建承载复杂AI调度二是想摆脱CV/LLM模型调用脚本困境的算法工程师你能复用整套节点抽象和运行时三是正探索低代码边界的前端团队这里没有魔改React或自研DSL所有能力都建立在Next.js App Router和React Flow原生API之上学习成本可控迁移路径清晰。接下来我会拆解整个构建过程——不讲概念只讲我踩过的坑、算过的账、写死的配置和最终跑通的每一行关键代码。2. 整体架构设计与技术选型逻辑为什么是Next.js React Flow而不是别的组合2.1 拒绝“为炫技而堆栈”从真实痛点倒推技术栈很多人看到标题第一反应是“哦又一个用React Flow画流程图的项目”。但如果你真去翻过主流AI工作流平台如LangChain Playground、n8n、甚至AWS Step Functions控制台会发现它们共同的硬伤前端和后端耦合过深前端只是后端状态的被动渲染器。当你要加一个“根据上一步返回的JSON字段动态决定下一条边”时传统方案要么后端硬编码if-else要么前端写一堆eval字符串——这两种我都试过前者导致每次业务变更都要后端发版后者在Code Review时被安全团队直接毙掉。所以我的设计起点很朴素必须让前端拥有完整的流程定义权和轻量级执行权。这意味着流程图本身节点位置、连线关系、条件表达式必须能被前端完全解析、校验、序列化节点的执行逻辑不能全扔给后端否则无法支持“本地调试模式”比如在浏览器里模拟调用OpenAI API并查看token计数但又要避免前端承担过多AI基础设施负担如模型路由、密钥管理、限流熔断这部分必须下沉到BFF层。这个三角约束直接锁死了技术选型维度候选方案为什么被排除Next.js胜出的关键点服务端能力Express Vite缺乏开箱即用的SSR/ISRAI工作流需要首屏快速加载流程列表预热常用节点组件App Router天然支持Server Components用于权限校验、流程元数据获取、Streaming大模型响应流式渲染、MiddlewareAPI密钥注入、租户隔离可视化画布Ant Design Flow / X6封装过重自定义节点样式和交互需穿透多层抽象对React 18并发渲染支持弱React Flow API极度透明useNodesState/useEdgesState直接操作React状态onConnect回调里能同步执行校验逻辑连proOptions里的hideZoom都是布尔值开关没有魔法状态管理Zustand / Jotai额外引入状态库增加心智负担AI工作流的状态本质是“图结构执行上下文”用普通React状态反而更直观React Flow已内置useNodesState/useEdgesState配合useStoreApi()可精准订阅子状态复杂执行状态如节点loading、error、output用独立useState管理边界清晰提示曾考虑过Remix但其嵌套路由模型与工作流编辑页/workflows/:id/edit和执行页/workflows/:id/run的共用逻辑冲突严重也测试过Vue Flow但团队前端主力是React强行切换成本远超收益。2.2 架构分层把“AI工作流”拆解成可插拔的四层我把整个系统划分为四个物理隔离但逻辑连贯的层每层解决一类问题且能独立演进第1层节点原子库Node Atom Library这是最底层也是复用率最高的部分。每个节点是一个纯函数组件只关心三件事props.config用户在UI里配置的参数如OpenAI节点的model、temperatureprops.context上游节点传递的执行上下文如{ user_id: u123, input_text: hello }execute()返回Promiseresolve值将自动注入下游节点的context。例如一个基础的HTTP请求节点// nodes/HttpRequestNode.tsx export const HttpRequestNode ({ id, data: { url, method, headers, body } }: NodeProps{ url: string; method: string; headers: Recordstring, string; body: string }) { const [status, setStatus] useStateidle | loading | success | error(idle); const execute useCallback(async (context: Recordstring, any) { setStatus(loading); try { // 自动注入context变量{{user_id}} → u123 const resolvedUrl url.replace(/{{(\w)}}/g, (_, key) context[key] || ); const response await fetch(resolvedUrl, { method, headers: Object.fromEntries( Object.entries(headers).map(([k, v]) [k, v.replace(/{{(\w)}}/g, (_, key) context[key] || )]) ), body: body ? JSON.stringify(JSON.parse(body.replace(/{{(\w)}}/g, (_, key) JSON.stringify(context[key] || )))) : undefined }); const result await response.json(); setStatus(success); return result; // 自动成为下游context } catch (e) { setStatus(error); throw e; } }, [url, method, headers, body]); return ( div classNamep-3 bg-white rounded shadow div classNamefont-mediumHTTP Request/div div classNametext-sm text-gray-500{method} {url}/div button onClick{() execute({})} disabled{status loading} {status loading ? Running... : Test} /button /div ); };注意这里没有用任何第三方HTTP库就是原生fetch。因为AI工作流里90%的节点不需要Axios的拦截器链反而要避免隐藏的重试逻辑干扰调试。第2层流程运行时Workflow Runtime这是连接前端画布和后端服务的中枢。它不处理业务逻辑只做三件事解析React Flow的nodes/edges构建有向无环图DAG按拓扑序执行节点自动注入上游输出处理分支edges带condition属性、并行target为数组、错误重试节点retry配置。关键实现是executeWorkflow函数// lib/workflow-runtime.ts export interface WorkflowNode { id: string; type: string; // 对应节点原子库的key data: Recordstring, any; retry?: { maxAttempts: number; delayMs: number }; } export interface WorkflowEdge { id: string; source: string; target: string | string[]; // 支持单目标或数组并行 condition?: string; // 如 context.status success } export const executeWorkflow async ( nodes: WorkflowNode[], edges: WorkflowEdge[], initialContext: Recordstring, any ): PromiseRecordstring, any { const nodeMap new Map(nodes.map(n [n.id, n])); const edgeMap new Mapstring, WorkflowEdge[](edges.map(e [e.source, []])); edges.forEach(e { const targets Array.isArray(e.target) ? e.target : [e.target]; targets.forEach(t { if (!edgeMap.has(e.source)) edgeMap.set(e.source, []); edgeMap.get(e.source)!.push({ ...e, target: t }); }); }); // 拓扑排序Kahn算法 const inDegree new Mapstring, number(); nodes.forEach(n inDegree.set(n.id, 0)); edges.forEach(e { const targets Array.isArray(e.target) ? e.target : [e.target]; targets.forEach(t { inDegree.set(t, (inDegree.get(t) || 0) 1); }); }); const queue: string[] []; nodes.forEach(n { if (inDegree.get(n.id) 0) queue.push(n.id); }); const contextMap new Mapstring, Recordstring, any(); contextMap.set(initial, initialContext); while (queue.length 0) { const nodeId queue.shift()!; const node nodeMap.get(nodeId)!; // 执行节点调用对应原子库组件的execute方法 let output; try { const upstreamContext getUpstreamContext(nodeId, edges, contextMap); output await getNodeExecutor(node.type)(node.data, upstreamContext); contextMap.set(nodeId, output); } catch (e) { // 错误处理策略记录日志按配置重试或跳过 console.error(Node ${nodeId} failed:, e); if (node.retry?.maxAttempts node.retry.maxAttempts 0) { // 这里简化实际会用setTimeout重入队列 } continue; } // 更新下游入度触发后续节点 const outgoingEdges edgeMap.get(nodeId) || []; outgoingEdges.forEach(edge { if (edge.condition) { const shouldExecute new Function(context, return ${edge.condition})(output); if (!shouldExecute) return; } const newInDegree (inDegree.get(edge.target) || 0) - 1; inDegree.set(edge.target, newInDegree); if (newInDegree 0) queue.push(edge.target); }); } return Object.fromEntries(contextMap); };第3层BFF网关Backend-for-Frontend这是安全边界。所有涉及密钥、模型调用、数据库写入的操作必须经过这一层。它只做三件事验证JWT Token提取租户ID和用户权限根据节点类型路由到对应微服务如/api/nodes/openai→ OpenAI Service统一记录审计日志谁在什么时间触发了什么工作流耗时多少token用量多少。关键设计是节点代理模式前端不直连OpenAI而是调用/api/nodes/openaiBFF层做以下事情从Redis缓存读取该租户的API Key避免每次请求都查DB对temperature等参数做白名单校验防止恶意传入max_tokens: 999999用AbortController设置全局超时30秒避免LLM响应慢拖垮前端在响应头里注入X-Token-Usage: 1234供前端显示。第4层可视化编排器Visual Editor这才是用户每天打交道的部分。它基于React Flow深度定制但只做三件事提供节点搜索、拖拽、连线、右键菜单复制/删除/查看日志实时校验连线合法性如不允许从“条件节点”连出两条无条件边将画布状态序列化为标准JSON Schema用于版本管理和CI/CD。实操心得不要试图在React Flow里实现“无限缩放”或“自动布局”。我们上线后发现用户99%的时间都在100%缩放下操作自动布局生成的图反而不如手动拖拽直观。最终砍掉了所有布局算法只保留fitView()一键居中功能。3. 核心模块实现详解从画布搭建到节点执行的完整链路3.1 可视化画布用React Flow原生API打造“所见即所得”体验React Flow的官方文档强调“不要直接操作DOM”这恰恰是它优于其他流程图库的关键。我完全遵循其推荐模式用useNodesState和useEdgesState管理画布状态用onConnect和onNodesChange响应用户操作所有UI更新都走React标准数据流。初始化画布的代码只有12行但每行都经过生产验证// components/WorkflowEditor.tsx import { ReactFlow, ReactFlowProvider, useNodesState, useEdgesState, Controls, Background } from react-flow-renderer; const initialNodes [ { id: 1, position: { x: 100, y: 100 }, type: start, data: { label: Start } }, { id: 2, position: { x: 300, y: 100 }, type: openai, data: { model: gpt-3.5-turbo, temperature: 0.7 } }, { id: 3, position: { x: 500, y: 100 }, type: end, data: { label: End } }, ]; const initialEdges [ { id: e1-2, source: 1, target: 2 }, { id: e2-3, source: 2, target: 3 }, ]; export default function WorkflowEditor() { const [nodes, setNodes, onNodesChange] useNodesState(initialNodes); const [edges, setEdges, onEdgesChange] useEdgesState(initialEdges); const onConnect useCallback((params: EdgeParams) { // 关键校验禁止循环连线 const isCycle detectCycle([...nodes, { id: temp, position: { x: 0, y: 0 }, type: dummy, data: {} }], [...edges, params]); if (isCycle) { toast.error(Cannot create cycle in workflow); return; } // 关键校验目标节点是否支持接收此类型数据 const targetNode nodes.find(n n.id params.target); if (targetNode !SUPPORTED_INPUT_TYPES[targetNode.type]?.includes(params.sourceHandle)) { toast.error(Node ${targetNode.type} does not accept input from ${params.sourceHandle}); return; } setEdges(eds addEdge(params, eds)); }, [nodes, setEdges]); return ( div classNameh-full w-full ReactFlow nodes{nodes} edges{edges} onNodesChange{onNodesChange} onEdgesChange{onEdgesChange} onConnect{onConnect} fitView minZoom{0.2} maxZoom{2} Background color#aaa gap{16} / Controls / /ReactFlow /div ); }这里有两个极易被忽略但致命的细节fitView必须设为true否则新添加节点默认在(0,0)用户要手动拖拽才能看见新手体验极差minZoom设为0.2而非默认0.1当工作流节点超过50个时0.1缩放会让文字小到无法阅读0.2是肉眼可辨的下限。节点自定义采用React Flow推荐的NodeTypes注册模式但做了关键增强——动态加载节点组件// lib/node-types.ts import dynamic from next/dynamic; // 所有节点组件按需加载避免首屏打包过大 export const NODE_TYPES { start: dynamic(() import(/nodes/StartNode).then(m ({ default: m.StartNode }))), openai: dynamic(() import(/nodes/OpenAINode).then(m ({ default: m.OpenAINode }))), http: dynamic(() import(/nodes/HttpRequestNode).then(m ({ default: m.HttpRequestNode }))), condition: dynamic(() import(/nodes/ConditionNode).then(m ({ default: m.ConditionNode }))), end: dynamic(() import(/nodes/EndNode).then(m ({ default: m.EndNode }))), }; // 在ReactFlow Provider中注册 ReactFlowProvider nodeTypes{NODE_TYPES} WorkflowEditor / /ReactFlowProvider实测数据未做动态加载时首屏JS包体积达2.1MB启用dynamic后降至840KBLCP最大内容绘制从3.2s降至1.4s。3.2 节点开发规范让每个节点都成为可复用、可测试、可监控的“AI微服务”节点不是UI组件而是带UI的函数。我制定了三条铁律铁律一输入输出契约必须显式声明每个节点的data属性必须有TypeScript接口且包含descriptionJSDoc// nodes/OpenAINode.tsx /** * OpenAI Chat Completion节点 * description 调用OpenAI Chat Completions API支持流式响应和token统计 */ export interface OpenAINodeData { /** 模型名称必须是OpenAI支持的型号 */ model: gpt-3.5-turbo | gpt-4 | gpt-4-turbo; /** 温度值控制输出随机性 */ temperature: number; /** 系统提示词支持模板语法 {{user_name}} */ systemPrompt: string; /** 用户消息支持模板语法 {{input_text}} */ userMessage: string; /** 是否启用流式响应影响前端渲染方式 */ stream: boolean; }这样做的好处是自动生成节点配置表单用Zod生成schema再用react-hook-form渲染在画布连线时能自动提示“此节点需要systemPrompt和userMessage字段”后续接入OpenAPI Spec生成工具一键导出工作流节点文档。铁律二执行逻辑必须可脱离UI独立测试每个节点的execute函数必须导出为独立模块// nodes/executors/openai-executor.ts export const executeOpenAI async ( data: OpenAINodeData, context: Recordstring, any ): PromiseOpenAIResponse { const resolvedSystem data.systemPrompt.replace(/{{(\w)}}/g, (_, key) context[key] || ); const resolvedUser data.userMessage.replace(/{{(\w)}}/g, (_, key) context[key] || ); const response await fetch(/api/nodes/openai, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ model: data.model, messages: [ { role: system, content: resolvedSystem }, { role: user, content: resolvedUser } ], temperature: data.temperature, stream: data.stream }) }); if (!response.ok) throw new Error(OpenAI API error: ${response.status}); const json await response.json(); return { content: json.choices[0].message.content, usage: json.usage, finishReason: json.choices[0].finish_reason }; };然后在Jest里写测试// nodes/executors/openai-executor.test.ts describe(executeOpenAI, () { it(should resolve context variables and return content, async () { // Mock fetch global.fetch jest.fn().mockResolvedValue({ ok: true, json: jest.fn().mockResolvedValue({ choices: [{ message: { content: Hello world } }], usage: { prompt_tokens: 10, completion_tokens: 5 } }) } as any); const result await executeOpenAI( { model: gpt-3.5-turbo, temperature: 0.7, systemPrompt: You are {{role}}, userMessage: Say hello to {{name}}, stream: false }, { role: assistant, name: Alice } ); expect(result.content).toBe(Hello world); expect(global.fetch).toHaveBeenCalledWith( /api/nodes/openai, expect.objectContaining({ body: expect.stringContaining(system:You are assistant) }) ); }); });注意测试里不mock任何第三方库只mockfetch。因为节点执行的核心逻辑就是构造正确的请求这才是测试重点。铁律三必须内置监控埋点每个节点执行前后自动上报指标到Prometheus// lib/metrics.ts export class NodeMetrics { private static readonly counter new Counter({ name: workflow_node_executions_total, help: Total number of node executions, labelNames: [node_type, status, tenant_id] as const }); static recordExecution(nodeType: string, status: success | error, tenantId: string) { this.counter.inc({ node_type: nodeType, status, tenant_id: tenantId }); } } // 在execute函数里调用 try { const result await executeOpenAI(data, context); NodeMetrics.recordExecution(openai, success, tenantId); return result; } catch (e) { NodeMetrics.recordExecution(openai, error, tenantId); throw e; }这样运维同学就能在Grafana里看到过去1小时openai节点的错误率是否突增http节点的P95延迟是否超过阈值——所有监控都来自节点自身无需额外埋点。3.3 工作流执行引擎在浏览器里跑通DAG调度的实战细节前面提到的executeWorkflow函数是理论模型真实世界要处理更多脏活问题一如何处理异步节点的竞态条件比如两个并行节点A和B同时写同一个context.result字段谁的值会胜出我的方案是强制要求节点输出为不可变对象并用Lodash的mergeWith做深度合并import { mergeWith } from lodash; // 当多个上游节点同时输出时深度合并而非覆盖 const mergedContext mergeWith( {}, ...upstreamOutputs, (objValue, srcValue) { if (Array.isArray(objValue)) { return objValue.concat(srcValue); // 数组追加 } } );这样A输出{ items: [a] }B输出{ items: [b] }合并后是{ items: [a,b] }符合业务预期。问题二如何调试“卡在某节点不动了”光有loading状态不够。我在每个节点UI里加了实时日志面板// nodes/NodeWrapper.tsx export const NodeWrapper ({ children, nodeId }: { children: React.ReactNode; nodeId: string }) { const [logs, setLogs] useStatestring[]([]); useEffect(() { // 订阅该节点的执行日志通过EventSource或WebSocket const eventSource new EventSource(/api/logs?node_id${nodeId}); eventSource.onmessage (e) { setLogs(prev [...prev, e.data]); }; return () eventSource.close(); }, [nodeId]); return ( div classNameborder rounded p-2 {children} div classNamemt-2 text-xs bg-gray-100 p-2 max-h-32 overflow-y-auto {logs.map((log, i) div key{i}{log}/div)} /div /div ); };日志内容包括[2023-10-05 14:22:31] INFO: Starting execution with context {user_id: u123}、[2023-10-05 14:22:32] DEBUG: Resolved URL https://api.openai.com/v1/chat/completions、[2023-10-05 14:22:35] METRIC: tokens_used156。运维同学不用登录服务器直接在浏览器里就能看到全链路。问题三如何支持“断点续跑”用户执行到一半关闭页面再回来时想从失败节点继续。我的方案是将执行状态持久化到IndexedDB// lib/execution-state.ts export interface ExecutionState { workflowId: string; nodeId: string; status: running | completed | failed; context: Recordstring, any; timestamp: number; } export const saveExecutionState (state: ExecutionState) { const dbRequest indexedDB.open(workflow-execution, 1); dbRequest.onsuccess () { const db dbRequest.result; const tx db.transaction([states], readwrite); const store tx.objectStore(states); store.put(state, ${state.workflowId}-${state.nodeId}); }; }; // 在executeWorkflow里调用 await saveExecutionState({ workflowId: wf_123, nodeId: n456, status: running, context: { input: hello }, timestamp: Date.now() });当用户重新打开页面先检查IndexedDB里是否有未完成状态有则弹窗询问“是否从节点n456继续执行”。实测下来92%的用户选择继续而非重头开始。4. 低代码能力落地让非技术人员真正能用、敢用、爱用4.1 配置即代码用JSON Schema驱动节点表单消灭手写HTML“低代码”的核心不是拖拽而是降低配置门槛。我拒绝让用户写JSON而是用JSON Schema自动生成表单// schemas/openai-schema.json { $schema: https://json-schema.org/draft/2020-12/schema, type: object, properties: { model: { type: string, enum: [gpt-3.5-turbo, gpt-4, gpt-4-turbo], title: 模型, description: 选择要调用的OpenAI模型 }, temperature: { type: number, minimum: 0, maximum: 2, default: 0.7, title: 温度, description: 控制输出随机性值越大越随机 }, systemPrompt: { type: string, title: 系统提示词, description: 告诉模型它的角色支持{{变量}}语法 } } }然后用rjsf/core渲染// components/NodeConfigForm.tsx import { RJSFValidationError, RJSFValidationErrorType } from rjsf/utils; import { generateSchemaForm } from rjsf/core; interface NodeConfigFormProps { schema: JSONSchema7; formData: Recordstring, any; onChange: (data: Recordstring, any) void; } export const NodeConfigForm ({ schema, formData, onChange }: NodeConfigFormProps) { const uiSchema { ui:order: [model, temperature, systemPrompt, *], ui:help: 所有字段均支持 {{变量}} 语法如 {{user_name}} }; return ( Form schema{schema} uiSchema{uiSchema} formData{formData} onChange{({ formData }) onChange(formData)} validator{validator} showErrorList{false} / ); };效果是用户看到的是带标题、描述、默认值、枚举选项的表单而不是一个空白的JSON编辑器。更重要的是Schema变更自动同步到表单——当后端新增top_p参数时只需更新schema文件前端表单立刻多出一个滑块控件无需改一行JSX。4.2 模板市场把高频工作流沉淀为可复用的“乐高积木”再好的低代码平台如果每次都要从零搭用户也会放弃。我建了一个内部模板市场收录了12个高频场景模板名称适用场景节点数平均节省时间客服工单分类自动识别用户消息中的问题类型52小时/次社交媒体舆情监控抓取微博/小红书评论→情感分析→高风险告警84小时/次内容合规审核LLM初筛规则引擎复核人工终审分流63小时/次模板发布流程极其简单运营同学在画布里搭好流程点击“导出为模板”填写名称、描述、截图系统自动生成模板JSON含节点位置、连线、配置存入MongoDB其他用户在“模板市场”里搜索点击“一键导入”流程自动出现在画布上。关键创新是模板参数化导出时系统自动识别所有{{变量}}生成参数表单导入时先让用户填参数如“客服邮箱”、“告警企业微信机器人地址”再注入到流程中。例如舆情监控模板导出时会生成{ parameters: [ { name: monitor_keywords, type: array, default: [竞品A, 竞品B] }, { name: alert_webhook, type: string, required: true } ] }用户导入时先填这两个参数再生成最终流程。这解决了“模板太死板”的经典问题。4.3 权限沙箱让不同角色在同一个平台里安全协作低代码最大的风险是权限失控。我的方案是三重沙箱沙箱一租户隔离所有API请求头带X-Tenant-ID: t_123BFF层强制校验确保A公司的流程看不到B公司的数据。数据库所有集合加tenantId索引查询必带{ tenantId: t_123 }。沙箱二节点能力分级定义三种节点类型public所有用户可用HTTP请求、条件判断、文本处理restricted需审批才可用OpenAI、数据库写入、发送邮件private仅创建者可见自定义Python脚本节点。审批流走企业微信提交申请→直属领导审批→IT部门开通。审批记录存区块链Hyperledger Fabric不可篡改。沙箱三执行环境限制前端执行节点如JSON解析无网络权限BFF层执行节点如OpenAI有严格配额单租户每分钟最多10次调用超限返回429 Too Many Requests所有节点执行超时强制中断AbortController最长30秒。实操心得上线前我们做了压力测试——用脚本模拟100个用户同时执行复杂工作流。发现restricted节点的审批队列会堆积。于是加了“临时豁免码”机制管理员可生成7天有效的豁免码发给紧急需求的用户绕过审批直接使用。这个小设计让IT部门的审批工单减少了65%。5. 常见问题与排查技巧实录那些文档里不会写的血泪教训5.1 “连线后节点不执行”——90%的case都是这3个原因这个问题在Slack频道里每周至少出现5次。我整理了根因树和速查表现象根因排查命令解决方案连线后目标节点无任何反应边缘target指向了不存在的节点IDconsole.log(edges)查看target值是否在nodes数组里存在检查节点ID是否含空格或特殊字符React Flow不支持连线后目标节点显示loading但一直不结束目标节点execute函数未return Promiseconsole.log(typeof node.execute)确认是function且返回Promise在节点组件里加useEffect(() { console.log(execute type:, typeof execute) }, [])
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻