Dify 前端可视化编排深度解析:技术架构 × 源码剖析 × 嵌入实战
作者 :前端架构师 | 发布时间 :2026-04-24 | 阅读时长 :约 25 分钟
标签 :#Dify#ReactFlow#可视化编排#前端架构#二次开发#Zustand#TypeScript
目录
- 一、可视化编排模块定位与技术选型
- 二、前端技术栈全景
- 三、源码目录结构详解
- [四、核心架构:ReactFlow 画布层](#四、核心架构:ReactFlow 画布层)
- 五、节点系统深度剖析
- [六、状态管理:Zustand Store 分层设计](#六、状态管理:Zustand Store 分层设计)
- 七、变量引用系统:节点间数据流
- [八、Panel 面板 + useConfig Hook 四件套](#八、Panel 面板 + useConfig Hook 四件套)
- 九、工作流执行与实时反馈
- [十、嵌入方案一:官方 SDK / iframe(快速集成)](#十、嵌入方案一:官方 SDK / iframe(快速集成))
- 十一、嵌入方案二:源码级二次开发(深度定制)
- 十二、自定义节点开发完整实战
- [十三、将 Workflow 画布剥离嵌入自有项目](#十三、将 Workflow 画布剥离嵌入自有项目)
- 十四、常见避坑指南
- 十五、总结
一、可视化编排模块定位与技术选型

Dify 的 Workflow Builder 是整个平台最复杂的前端模块------它是一个生产级的 DAG 流程图编辑器,需要同时满足:
| 需求 | 具体要求 |
|---|---|
| 可视化编辑 | 拖拽创建节点、连线、选择、缩放、小地图 |
| 节点配置 | 每种节点类型有独立配置面板(右侧 Panel) |
| 变量系统 | 跨节点变量引用,类型安全校验 |
| 实时执行 | 运行时节点状态流式更新(SSE 推送) |
| 历史记录 | 撤销/重做(Undo/Redo) |
| DSL 导入导出 | YAML 格式的工作流序列化/反序列化 |
为什么选 ReactFlow(@xyflow/react)?
候选方案对比:
X6(AntV) → 重,中文生态好,但 React 集成不够原生
LogicFlow → 业务流程图,节点自定义能力弱
Mermaid → 纯展示,不支持交互编辑
D3.js → 底层灵活但需要大量封装
✅ ReactFlow → React 原生、节点完全自定义、性能优秀、社区活跃
二、前端技术栈全景

web/
├── 框架层 Next.js 14 (App Router + SSR)
├── UI 框架 React 18 + TypeScript 5
├── 画布引擎 @xyflow/react(ReactFlow v12)
├── 状态管理 Zustand(多 Store 分层)
├── 不可变更新 immer(produce)
├── 样式方案 TailwindCSS + CSS Modules
├── 数据获取 SWR(缓存 + 重验证)
├── 国际化 i18next
└── 图标 Remixicon / 自定义 SVG
三、源码目录结构详解
web/app/components/workflow/
│
├── index.tsx ← 主入口,ReactFlow 画布初始化
├── context.tsx ← React Context,提供全局配置
├── constants.ts ← 常量(节点尺寸、颜色等)
├── types.ts ← 全局 TypeScript 类型定义
├── features.tsx ← ReactFlow Features 配置
│
├── nodes/ ← 🔑 所有节点组件
│ ├── _base/ ← 节点基础组件(Handle、Header、状态指示器)
│ │ ├── components/ ← 共享子组件(Field、VarReferencePicker、OutputVars...)
│ │ ├── hooks/ ← 节点通用 Hooks
│ │ └── node.tsx ← 节点外壳(统一样式、选中态)
│ │
│ ├── llm/ ← LLM 节点
│ │ ├── index.tsx ← 画布节点组件
│ │ ├── panel.tsx ← 右侧配置面板
│ │ ├── use-config.ts ← 面板状态管理 Hook
│ │ ├── types.ts ← 类型定义
│ │ └── default.ts ← 默认值 & 校验逻辑
│ │
│ ├── knowledge-retrieval/ ← 知识库检索节点
│ ├── if-else/ ← 条件分支节点
│ ├── http-request/ ← HTTP 请求节点
│ ├── code/ ← 代码执行节点
│ ├── start/ ← 开始节点
│ ├── end/ ← 结束节点
│ ├── question-classifier/ ← 问题分类节点
│ ├── template-transform/ ← 模板转换节点
│ ├── tool/ ← 工具节点
│ ├── iteration/ ← 迭代节点
│ ├── variable-assigner/ ← 变量赋值节点
│ └── agent/ ← Agent 节点
│
├── store/ ← 🔑 Zustand 状态管理
│ ├── index.ts ← Store 聚合导出
│ ├── workflow-store.ts ← 主工作流状态(nodes/edges/运行态)
│ └── workflow-history-store.tsx ← Undo/Redo 历史记录
│
├── hooks/ ← 🔑 核心业务 Hooks
│ ├── use-workflow.ts ← 工作流全局管理(最重要!)
│ ├── use-nodes-interactions.ts ← 节点拖拽/选择交互
│ ├── use-edges-interactions.ts ← 连线交互
│ ├── use-workflow-run.ts ← 工作流执行
│ └── use-nodes-sync-draft.ts ← 草稿自动同步
│
├── panel/ ← 画布面板(工具栏、小地图、操作按钮)
├── run/ ← 执行结果展示面板
├── operator/ ← 画布操作器(缩放/全屏等)
├── block-selector/ ← 节点选择器(添加节点弹窗)
├── custom-edge.tsx ← 自定义连线组件
└── custom-connection-line.tsx ← 实时拖拽连线样式
四、核心架构:ReactFlow 画布层
4.1 画布初始化(index.tsx 核心逻辑)
tsx
// web/app/components/workflow/index.tsx(简化版)
import { ReactFlow, Background, Controls, MiniMap } from '@xyflow/react'
import { useWorkflowStore } from './store'
import { nodeTypes } from './nodes' // 节点类型注册表
import { edgeTypes } from './custom-edge' // 自定义边类型
import CustomConnectionLine from './custom-connection-line'
const WorkflowCanvas: FC = () => {
const {
nodes,
edges,
onNodesChange,
onEdgesChange,
onConnect,
} = useWorkflowStore()
const {
handleNodeClick,
handlePaneClick,
handleNodeDragStop,
} = useNodesInteractions()
return (
<ReactFlowProvider>
<ReactFlow
nodes={nodes}
edges={edges}
nodeTypes={nodeTypes} // 注册所有自定义节点
edgeTypes={edgeTypes} // 注册自定义边
connectionLineComponent={CustomConnectionLine}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
onNodeClick={handleNodeClick}
onPaneClick={handlePaneClick}
onNodeDragStop={handleNodeDragStop}
fitView
minZoom={0.25}
maxZoom={2}
snapToGrid
snapGrid={[8, 8]}
>
<Background />
<Controls />
<MiniMap />
</ReactFlow>
</ReactFlowProvider>
)
}
4.2 节点类型注册表
typescript
// web/app/components/workflow/nodes/index.ts
import { BlockEnum } from '../types'
import StartNode from './start'
import EndNode from './end'
import LLMNode from './llm'
import KnowledgeRetrievalNode from './knowledge-retrieval'
import IfElseNode from './if-else'
import HttpRequestNode from './http-request'
import CodeNode from './code'
import ToolNode from './tool'
import IterationNode from './iteration'
import AgentNode from './agent'
export const nodeTypes: Record<string, ComponentType<NodeProps>> = {
[BlockEnum.Start]: StartNode,
[BlockEnum.End]: EndNode,
[BlockEnum.LLM]: LLMNode,
[BlockEnum.KnowledgeRetrieval]: KnowledgeRetrievalNode,
[BlockEnum.IfElse]: IfElseNode,
[BlockEnum.HttpRequest]: HttpRequestNode,
[BlockEnum.Code]: CodeNode,
[BlockEnum.Tool]: ToolNode,
[BlockEnum.Iteration]: IterationNode,
[BlockEnum.Agent]: AgentNode,
}
4.3 自定义连线(custom-edge.tsx)
tsx
import { EdgeLabelRenderer, getBezierPath, EdgeProps } from '@xyflow/react'
import { CommonEdgeType } from './types'
const CustomEdge: FC<EdgeProps<CommonEdgeType>> = ({
id, sourceX, sourceY, targetX, targetY,
sourcePosition, targetPosition, data, selected,
}) => {
const [edgePath, labelX, labelY] = getBezierPath({
sourceX, sourceY, sourcePosition,
targetX, targetY, targetPosition,
})
const isRunning = data?._sourceRunningStatus === NodeRunningStatus.Running
return (
<>
<path
id={id}
className={cn(
'react-flow__edge-path',
selected && 'stroke-primary-600',
isRunning && 'stroke-blue-500 stroke-2 animate-pulse',
)}
d={edgePath}
/>
{/* 运行中的动态小圆点 */}
{isRunning && (
<circle r="4" fill="#3b82f6">
<animateMotion dur="1.5s" repeatCount="indefinite" path={edgePath} />
</circle>
)}
</>
)
}
五、节点系统深度剖析
5.1 节点枚举(BlockEnum)
typescript
// web/app/components/workflow/types.ts
export enum BlockEnum {
Start = 'start',
End = 'end',
Answer = 'answer',
LLM = 'llm',
KnowledgeRetrieval = 'knowledge-retrieval',
QuestionClassifier = 'question-classifier',
IfElse = 'if-else',
Code = 'code',
TemplateTransform = 'template-transform',
HttpRequest = 'http-request',
VariableAssigner = 'variable-assigner',
VariableAggregator = 'variable-aggregator',
Tool = 'tool',
ParameterExtractor = 'parameter-extractor',
Iteration = 'iteration',
IterationStart = 'iteration-start',
Agent = 'agent',
Loop = 'loop',
LoopStart = 'loop-start',
}
5.2 通用节点数据类型(CommonNodeType)
typescript
export type CommonNodeType<T = {}> = T & {
// ── 连接状态 ──
_connectedSourceHandleIds?: string[]
_connectedTargetHandleIds?: string[]
_targetBranches?: Branch[]
// ── 运行态(前端临时状态,不持久化)──
_runningStatus?: NodeRunningStatus // 'running' | 'succeeded' | 'failed'
_singleRunningStatus?: NodeRunningStatus
_waitingRun?: boolean
_retryIndex?: number
// ── 迭代/循环上下文 ──
isInIteration?: boolean
isInLoop?: boolean
iteration_id?: string
loop_id?: string
// ── UI 辅助状态 ──
selected?: boolean
_isCandidate?: boolean // 候选连接高亮
_inParallelHovering?: boolean
// ── 持久化配置 ──
title: string
desc: string
type: BlockEnum
width?: number
height?: number
error_strategy?: ErrorHandleTypeEnum // 错误处理策略
retry_config?: WorkflowRetryConfig // 重试配置
}
5.3 节点运行状态枚举
typescript
export enum NodeRunningStatus {
NotStart = 'not-start',
Waiting = 'waiting',
Running = 'running',
Succeeded = 'succeeded',
Failed = 'failed',
Exception = 'exception',
Retry = 'retry',
}
5.4 节点外壳组件(_base/node.tsx)
所有节点都共享同一个"外壳"组件,实现统一的选中态、状态指示器、标题栏:
tsx
// web/app/components/workflow/nodes/_base/node.tsx
const BaseNode: FC<NodeProps<CommonNodeType>> = ({
id, data, selected, children,
}) => {
const { runningStatus, selected: isSelected } = data
return (
<div
className={cn(
'workflow-node rounded-xl border-2 bg-white shadow-md',
'transition-all duration-200',
isSelected && 'border-primary-600 shadow-lg',
runningStatus === NodeRunningStatus.Running && 'border-blue-400',
runningStatus === NodeRunningStatus.Succeeded && 'border-green-500',
runningStatus === NodeRunningStatus.Failed && 'border-red-500',
)}
>
{/* 节点头部 */}
<NodeHeader id={id} data={data} />
{/* 节点内容(各节点自定义) */}
<div className="p-3">{children}</div>
{/* 运行状态角标 */}
{runningStatus && (
<StatusBadge status={runningStatus} retryIndex={data._retryIndex} />
)}
</div>
)
}
六、状态管理:Zustand Store 分层设计
6.1 主工作流 Store
typescript
// web/app/components/workflow/store/workflow-store.ts
import { create } from 'zustand'
import { subscribeWithSelector } from 'zustand/middleware'
import { produce } from 'immer'
import {
applyNodeChanges, applyEdgeChanges,
addEdge, type NodeChange, type EdgeChange, type Connection,
} from '@xyflow/react'
interface WorkflowState {
// ── 画布数据 ──
nodes: WorkflowNode[]
edges: WorkflowEdge[]
// ── 运行状态 ──
isRunning: boolean
runningNodeId: string | null
// ── 草稿状态 ──
isDraft: boolean
workflowId: string
// ── UI 状态 ──
selectedNodeId: string | null
hoveredNodeId: string | null
// ── Actions ──
setNodes: (nodes: WorkflowNode[]) => void
setEdges: (edges: WorkflowEdge[]) => void
onNodesChange: (changes: NodeChange[]) => void
onEdgesChange: (changes: EdgeChange[]) => void
onConnect: (connection: Connection) => void
updateNodeData: (nodeId: string, data: Partial<CommonNodeType>) => void
addNode: (node: WorkflowNode) => void
deleteNode: (nodeId: string) => void
}
export const useWorkflowStore = create<WorkflowState>()(
subscribeWithSelector((set, get) => ({
nodes: [],
edges: [],
isRunning: false,
runningNodeId: null,
isDraft: false,
workflowId: '',
selectedNodeId: null,
hoveredNodeId: null,
setNodes: (nodes) => set({ nodes }),
setEdges: (edges) => set({ edges }),
onNodesChange: (changes) => set((state) => ({
nodes: applyNodeChanges(changes, state.nodes),
})),
onEdgesChange: (changes) => set((state) => ({
edges: applyEdgeChanges(changes, state.edges),
})),
onConnect: (connection) => set((state) => ({
edges: addEdge(connection, state.edges),
})),
updateNodeData: (nodeId, data) => set((state) => ({
nodes: state.nodes.map(node =>
node.id === nodeId
? produce(node, (draft) => { Object.assign(draft.data, data) })
: node
),
})),
addNode: (node) => set((state) => ({
nodes: [...state.nodes, node],
})),
// 开始节点受保护,不可删除
deleteNode: (nodeId) => set((state) => {
const node = state.nodes.find(n => n.id === nodeId)
if (node?.data.type === BlockEnum.Start) return {}
return {
nodes: state.nodes.filter(n => n.id !== nodeId),
edges: state.edges.filter(
e => e.source !== nodeId && e.target !== nodeId
),
}
}),
}))
)
6.2 历史记录 Store(Undo/Redo)
typescript
// web/app/components/workflow/store/workflow-history-store.tsx
interface HistorySnapshot {
nodes: WorkflowNode[]
edges: WorkflowEdge[]
timestamp: number
}
interface HistoryState {
past: HistorySnapshot[]
future: HistorySnapshot[]
maxHistorySize: number
pushHistory: (snapshot: HistorySnapshot) => void
undo: () => HistorySnapshot | null
redo: () => HistorySnapshot | null
canUndo: () => boolean
canRedo: () => boolean
}
export const useHistoryStore = create<HistoryState>((set, get) => ({
past: [],
future: [],
maxHistorySize: 50,
pushHistory: (snapshot) => set((state) => {
const past = [...state.past, snapshot].slice(-state.maxHistorySize)
return { past, future: [] } // 新操作清空 future
}),
undo: () => {
const { past, future } = get()
if (past.length === 0) return null
const previous = past[past.length - 1]
set({ past: past.slice(0, -1), future: [previous, ...future] })
return previous
},
redo: () => {
const { past, future } = get()
if (future.length === 0) return null
const next = future[0]
set({ past: [...past, next], future: future.slice(1) })
return next
},
canUndo: () => get().past.length > 0,
canRedo: () => get().future.length > 0,
}))
七、变量引用系统:节点间数据流
这是 Dify 前端最核心的设计之一,理解它是二次开发的关键。
7.1 变量选择器格式
typescript
// ValueSelector = [nodeId, ...keyPath]
// 例如:
['start_node_id', 'user_input'] // 开始节点的 user_input 字段
['llm_node_id', 'text'] // LLM 节点的输出文本
['http_node_id', 'body', 'data', 'id'] // HTTP 节点嵌套字段
['sys', 'user_id'] // 系统变量
['env', 'API_KEY'] // 环境变量
7.2 变量类型系统
typescript
export enum VarType {
string = 'string',
number = 'number',
boolean = 'boolean',
object = 'object',
array = 'array',
arrayString = 'array[string]',
arrayNumber = 'array[number]',
arrayObject = 'array[object]',
arrayFile = 'array[file]',
file = 'file',
secret = 'secret',
any = 'any',
}
7.3 VarReferencePicker 核心组件使用
tsx
// 在自定义节点的 panel.tsx 中引用上游变量
import VarReferencePicker from '../_base/components/variable/var-reference-picker'
const MyNodePanel: FC<{ id: string; data: MyNodeType }> = ({ id, data }) => {
const { inputs, setInputs } = useNodeCrud(id)
return (
<Field title="输入变量" required>
<VarReferencePicker
nodeId={id}
value={inputs.input_variable} // ValueSelector
onChange={(selector) => {
setInputs(produce(inputs, (draft) => {
draft.input_variable = selector
}))
}}
filterVar={(varPayload) => // 只允许选择字符串类型变量
varPayload.type === VarType.string
}
/>
</Field>
)
}
7.4 获取前置节点输出变量(use-workflow 核心能力)
typescript
// 在节点内部获取所有可引用的上游变量
import { useAvailableVars } from '../hooks/use-workflow'
const { availableVars } = useAvailableVars(nodeId)
// availableVars: Array<{ nodeId, nodeType, vars: Variable[] }>
// 仅包含当前节点在 DAG 路径上的"祖先节点"输出变量
八、Panel 面板 + useConfig Hook 四件套
每个节点类型都遵循"四件套"约定,这是 Dify 前端最优雅的设计之一:
8.1 四件套结构
nodes/my-custom-node/
├── index.tsx ← 画布节点(显示在画布上的节点外观)
├── panel.tsx ← 配置面板(右侧配置区 UI)
├── use-config.ts ← 面板状态 Hook(状态 + 事件处理)
├── types.ts ← TypeScript 类型
└── default.ts ← 默认值 + 校验函数
8.2 types.ts
typescript
// nodes/my-custom-node/types.ts
import { CommonNodeType } from '../../types'
export type MyCustomNodeType = CommonNodeType & {
// 自定义配置字段
api_url: string
method: 'GET' | 'POST' | 'PUT' | 'DELETE'
input_variable: ValueSelector
output_schema: Record<string, VarType>
timeout: number
}
8.3 default.ts
typescript
// nodes/my-custom-node/default.ts
import { BlockEnum } from '../../types'
import type { MyCustomNodeType } from './types'
export const defaultConfig: Partial<MyCustomNodeType> = {
type: BlockEnum.HttpRequest,
title: '自定义节点',
desc: '',
api_url: '',
method: 'POST',
input_variable: [],
timeout: 30000,
}
export function validateConfig(
inputs: MyCustomNodeType,
t: (key: string) => string
): string[] {
const errors: string[] = []
if (!inputs.api_url) errors.push(t('workflow.nodes.myNode.apiUrlRequired'))
if (!inputs.input_variable?.length)
errors.push(t('workflow.nodes.myNode.inputVarRequired'))
return errors
}
8.4 use-config.ts
typescript
// nodes/my-custom-node/use-config.ts
import { useCallback } from 'react'
import { produce } from 'immer'
import useNodeCrud from '../_base/hooks/use-node-crud'
import type { MyCustomNodeType } from './types'
const useConfig = (id: string, payload: MyCustomNodeType) => {
const { inputs, setInputs } = useNodeCrud<MyCustomNodeType>(id, payload)
const handleApiUrlChange = useCallback((url: string) => {
const newInputs = produce(inputs, (draft) => {
draft.api_url = url
})
setInputs(newInputs)
}, [inputs, setInputs])
const handleMethodChange = useCallback((method: MyCustomNodeType['method']) => {
const newInputs = produce(inputs, (draft) => {
draft.method = method
})
setInputs(newInputs)
}, [inputs, setInputs])
const handleInputVarChange = useCallback((selector: ValueSelector) => {
const newInputs = produce(inputs, (draft) => {
draft.input_variable = selector
})
setInputs(newInputs)
}, [inputs, setInputs])
return {
inputs,
handleApiUrlChange,
handleMethodChange,
handleInputVarChange,
}
}
export default useConfig
8.5 panel.tsx
tsx
// nodes/my-custom-node/panel.tsx
import { memo } from 'react'
import Field from '../_base/components/field'
import VarReferencePicker from '../_base/components/variable/var-reference-picker'
import { VarType } from '../../types'
import useConfig from './use-config'
import type { MyCustomNodeType } from './types'
const Panel: FC<{ id: string; data: MyCustomNodeType }> = memo(({ id, data }) => {
const {
inputs,
handleApiUrlChange,
handleMethodChange,
handleInputVarChange,
} = useConfig(id, data)
return (
<div className="space-y-4">
{/* API URL 配置 */}
<Field title="API 地址" required>
<input
value={inputs.api_url}
onChange={e => handleApiUrlChange(e.target.value)}
placeholder="https://api.example.com/endpoint"
className="w-full px-3 py-2 rounded-lg border border-gray-200"
/>
</Field>
{/* 请求方法 */}
<Field title="请求方法">
<select
value={inputs.method}
onChange={e => handleMethodChange(e.target.value as any)}
className="w-full px-3 py-2 rounded-lg border border-gray-200"
>
{['GET', 'POST', 'PUT', 'DELETE'].map(m => (
<option key={m} value={m}>{m}</option>
))}
</select>
</Field>
{/* 输入变量选择(引用上游节点输出) */}
<Field title="输入变量" required>
<VarReferencePicker
nodeId={id}
value={inputs.input_variable}
onChange={handleInputVarChange}
filterVar={(v) => v.type === VarType.string}
/>
</Field>
</div>
)
})
export default Panel
8.6 index.tsx(画布节点外观)
tsx
// nodes/my-custom-node/index.tsx
import { memo } from 'react'
import { Handle, Position, type NodeProps } from '@xyflow/react'
import NodeShell from '../_base/node'
import type { MyCustomNodeType } from './types'
const MyCustomNode: FC<NodeProps<MyCustomNodeType>> = memo(({ id, data, selected }) => {
return (
<NodeShell id={id} data={data} selected={selected}>
{/* 输入 Handle */}
<Handle type="target" position={Position.Left} id="target" />
{/* 节点内容预览 */}
<div className="text-sm text-gray-600 truncate max-w-[200px]">
{data.api_url || '未配置 API 地址'}
</div>
{/* 输出 Handle */}
<Handle type="source" position={Position.Right} id="source" />
</NodeShell>
)
})
export default MyCustomNode
九、工作流执行与实时反馈
9.1 执行流程
用户点击「运行」
↓
useWorkflowRun.handleStartWorkflowRun()
↓
POST /api/workflows/run(带 inputs + mode)
↓
后端返回 task_id,建立 SSE 连接
↓
EventSource 监听 /api/workflows/run/streaming
↓
解析事件类型:
node_started → 更新节点状态为 Running(高亮蓝色)
node_finished → 更新节点状态为 Succeeded/Failed(绿/红)
workflow_finished → 全部完成,渲染输出
9.2 SSE 事件处理
typescript
// hooks/use-workflow-run.ts(简化)
const handleSSEMessage = useCallback((event: MessageEvent) => {
const data = JSON.parse(event.data)
switch (data.event) {
case 'node_started':
updateNodeData(data.data.node_id, {
_runningStatus: NodeRunningStatus.Running,
})
break
case 'node_finished':
updateNodeData(data.data.node_id, {
_runningStatus: data.data.status === 'succeeded'
? NodeRunningStatus.Succeeded
: NodeRunningStatus.Failed,
// 存储节点输出,供调试面板展示
_nodeRunResult: data.data.outputs,
})
break
case 'workflow_finished':
setIsRunning(false)
setWorkflowRunResult(data.data)
break
}
}, [updateNodeData])
十、嵌入方案一:官方 SDK / iframe(快速集成)
这是最快速、零维护成本的方案,适合需要在现有系统中嵌入 Dify 应用能力(聊天机器人、对话界面)。
10.1 方案对比
| 嵌入方式 | 开发成本 | 定制程度 | 维护成本 | 适用场景 |
|---|---|---|---|---|
| iframe | 极低(5分钟) | 低(仅样式) | 极低 | 快速上线、外部系统集成 |
| JS SDK | 低(1小时) | 中(交互可控) | 低 | 需要自定义触发时机和交互 |
| API 直调 | 中(1-2天) | 高(完全自定义UI) | 低 | 自研UI组件,调用Dify后端 |
| 源码二开 | 高(1-2周+) | 极高(一切可改) | 高 | 深度定制,嵌入编排画布本身 |
10.2 iframe 嵌入
html
<!-- 基础嵌入 -->
<iframe
src="https://your-dify.com/chatbot/YOUR_APP_TOKEN"
width="100%"
height="600"
style="border: none; border-radius: 12px;"
allow="microphone">
</iframe>
<!-- 响应式全屏 -->
<div style="position:relative; width:100%; padding-bottom:56.25%;">
<iframe
src="https://your-dify.com/chatbot/YOUR_APP_TOKEN"
style="position:absolute; top:0; left:0; width:100%; height:100%; border:none;"
></iframe>
</div>
10.3 JS SDK 嵌入(聊天气泡 + 预填参数)
html
<script>
window.difyChatbotConfig = {
token: 'YOUR_APP_TOKEN',
baseUrl: 'https://your-dify.com', // 自托管地址
// 位置与样式
containerProps: {
style: {
right: '24px',
bottom: '24px',
},
className: 'my-custom-chat-btn',
},
// 可拖拽
draggable: true,
dragAxis: 'both',
// 预填充用户上下文(对应应用中定义的变量)
inputs: {
user_name: window.__currentUser?.name || '访客',
user_role: window.__currentUser?.role || 'guest',
page_context: window.location.pathname,
},
// 系统变量追踪
systemVariables: {
user_id: window.__currentUser?.id,
},
}
</script>
<script src="https://your-dify.com/embed.js" async></script>
10.4 API 直调(完全自研 UI)
typescript
// 调用 Dify Chat API,自定义 UI
class DifyApiClient {
constructor(
private baseUrl: string,
private apiKey: string,
) {}
async *streamChat(params: {
query: string
conversationId?: string
inputs?: Record<string, string>
userId: string
}): AsyncGenerator<ChatChunk> {
const response = await fetch(`${this.baseUrl}/v1/chat-messages`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: params.query,
conversation_id: params.conversationId,
inputs: params.inputs ?? {},
user: params.userId,
response_mode: 'streaming',
}),
})
const reader = response.body!.getReader()
const decoder = new TextDecoder()
while (true) {
const { done, value } = await reader.read()
if (done) break
const chunk = decoder.decode(value)
const lines = chunk.split('\n').filter(l => l.startsWith('data: '))
for (const line of lines) {
const data = JSON.parse(line.slice(6))
if (data.event === 'message') {
yield { text: data.answer, conversationId: data.conversation_id }
}
}
}
}
}
// React 组件中使用
const ChatWidget: FC = () => {
const [messages, setMessages] = useState<Message[]>([])
const client = new DifyApiClient('/dify-api', 'app-xxxx')
const handleSend = async (query: string) => {
for await (const chunk of client.streamChat({ query, userId: 'user-1' })) {
// 流式追加消息
setMessages(prev => appendToLastMessage(prev, chunk.text))
}
}
return <YourCustomChatUI messages={messages} onSend={handleSend} />
}
十一、嵌入方案二:源码级二次开发(深度定制)
这是将 Dify Workflow Builder 画布嵌入自有项目的方案,难度较高但定制空间无限。
11.1 前置准备:本地开发环境
bash
# 1. Fork + Clone Dify 仓库
git clone https://github.com/langgenius/dify.git
cd dify/web
# 2. 安装依赖(推荐 pnpm)
pnpm install # 或 npm install
# 3. 配置环境变量
cp .env.example .env.local
# 编辑 .env.local:
# NEXT_PUBLIC_API_PREFIX=http://localhost:5001/console/api
# NEXT_PUBLIC_PUBLIC_API_PREFIX=http://localhost:5001/api
# 4. 启动开发服务器
pnpm dev # → http://localhost:3000
11.2 修改前端代码生效的关键
yaml
# docker-compose.yaml --- 必须改为本地构建,否则修改不生效!
services:
web:
# ❌ 删除或注释掉这行
# image: langgenius/dify-web:1.13.3
# ✅ 改为本地构建
build:
context: ../web
dockerfile: Dockerfile
# 或者直接挂载开发服务器:
# command: pnpm dev
# volumes:
# - ../web:/app
# - /app/node_modules
11.3 代码隔离约定(参考 dify-plus 实践)
# 自定义代码使用 extend_ 前缀隔离,便于跟踪和升级维护
web/app/components/workflow/nodes/
├── extend_my-api-node/ ← 自定义节点目录(extend_ 前缀)
├── extend_database-node/
└── ...
# 修改记录追踪
git log --all --oneline web/app/components/workflow/
十二、自定义节点开发完整实战
以下实现一个"数据库查询节点",从指定数据库表查询数据并输出给下游节点。
12.1 后端:注册新节点类型
python
# api/core/workflow/nodes/database_query/__init__.py
from .database_query_node import DatabaseQueryNode
NODE_TYPE_CLASSES_MAPPING = {
# ... 现有节点
'database-query': DatabaseQueryNode,
}
python
# api/core/workflow/nodes/database_query/database_query_node.py
from core.workflow.nodes.base_node import BaseNode
class DatabaseQueryNode(BaseNode):
_node_data_cls = DatabaseQueryNodeData
def _run(self, variable_pool: VariablePool) -> NodeRunResult:
node_data: DatabaseQueryNodeData = cast(self._node_data_cls, self.node_data)
# 从变量池解析输入变量
query_input = variable_pool.get(node_data.query_variable)
# 执行数据库查询(实际业务逻辑)
result = execute_db_query(
connection_str=node_data.connection_string,
query=node_data.sql_template.format(input=query_input)
)
return NodeRunResult(
status=WorkflowNodeExecutionStatus.SUCCEEDED,
inputs={"query_variable": str(query_input)},
outputs={"result": result, "row_count": len(result)},
process_data={}
)
12.2 前端 types.ts
typescript
// web/app/components/workflow/nodes/database-query/types.ts
import type { CommonNodeType, ValueSelector } from '../../types'
export type DatabaseQueryNodeType = CommonNodeType & {
connection_string: string // 数据库连接串(应使用环境变量)
sql_template: string // SQL 模板,支持 {input} 占位符
query_variable: ValueSelector // 引用上游节点的查询参数
max_rows: number // 最大返回行数
}
12.3 前端 default.ts
typescript
// web/app/components/workflow/nodes/database-query/default.ts
export const defaultConfig: Partial<DatabaseQueryNodeType> = {
title: '数据库查询',
desc: '',
connection_string: '',
sql_template: 'SELECT * FROM users WHERE name = \'{input}\' LIMIT 10',
query_variable: [],
max_rows: 100,
}
export const validate = (inputs: DatabaseQueryNodeType, t: TFunction): string[] => {
const errors: string[] = []
if (!inputs.connection_string) errors.push('连接字符串不能为空')
if (!inputs.sql_template) errors.push('SQL 模板不能为空')
if (!inputs.query_variable?.length) errors.push('查询变量不能为空')
return errors
}
12.4 前端 use-config.ts
typescript
// web/app/components/workflow/nodes/database-query/use-config.ts
import { useCallback } from 'react'
import { produce } from 'immer'
import useNodeCrud from '../_base/hooks/use-node-crud'
import type { DatabaseQueryNodeType } from './types'
const useConfig = (id: string, payload: DatabaseQueryNodeType) => {
const { inputs, setInputs } = useNodeCrud<DatabaseQueryNodeType>(id, payload)
const handleConnectionChange = useCallback((conn: string) => {
setInputs(produce(inputs, d => { d.connection_string = conn }))
}, [inputs, setInputs])
const handleSqlChange = useCallback((sql: string) => {
setInputs(produce(inputs, d => { d.sql_template = sql }))
}, [inputs, setInputs])
const handleQueryVarChange = useCallback((selector: ValueSelector) => {
setInputs(produce(inputs, d => { d.query_variable = selector }))
}, [inputs, setInputs])
const handleMaxRowsChange = useCallback((rows: number) => {
setInputs(produce(inputs, d => { d.max_rows = rows }))
}, [inputs, setInputs])
return { inputs, handleConnectionChange, handleSqlChange, handleQueryVarChange, handleMaxRowsChange }
}
export default useConfig
12.5 前端 panel.tsx
tsx
// web/app/components/workflow/nodes/database-query/panel.tsx
import { memo } from 'react'
import Field from '../_base/components/field'
import VarReferencePicker from '../_base/components/variable/var-reference-picker'
import { VarType } from '../../types'
import useConfig from './use-config'
const Panel: FC<{ id: string; data: DatabaseQueryNodeType }> = memo(({ id, data }) => {
const {
inputs,
handleConnectionChange, handleSqlChange,
handleQueryVarChange, handleMaxRowsChange,
} = useConfig(id, data)
return (
<div className="space-y-4 p-4">
<Field title="数据库连接" required tooltip="使用环境变量引用敏感信息">
<input
value={inputs.connection_string}
onChange={e => handleConnectionChange(e.target.value)}
placeholder="postgresql://user:pass@host/db 或 {{env.DB_URL}}"
className="input-field w-full"
/>
</Field>
<Field title="查询参数" required tooltip="从上游节点选择查询参数变量">
<VarReferencePicker
nodeId={id}
value={inputs.query_variable}
onChange={handleQueryVarChange}
filterVar={v => [VarType.string, VarType.number].includes(v.type)}
/>
</Field>
<Field title="SQL 模板" required>
<textarea
value={inputs.sql_template}
onChange={e => handleSqlChange(e.target.value)}
rows={4}
placeholder="SELECT * FROM table WHERE field = '{input}'"
className="input-field w-full font-mono text-sm"
/>
</Field>
<Field title="最大返回行数">
<input
type="number"
min={1}
max={1000}
value={inputs.max_rows}
onChange={e => handleMaxRowsChange(Number(e.target.value))}
className="input-field w-32"
/>
</Field>
</div>
)
})
export default Panel
12.6 注册到节点类型表
typescript
// web/app/components/workflow/nodes/index.ts
import DatabaseQueryNode from './database-query'
// ...
export const nodeTypes = {
// ...existing,
'database-query': DatabaseQueryNode,
}
// 在 block-selector 中加入节点面板选择项
export const allBlocksMap: Record<BlockEnum, BlockConfig> = {
// ...
'database-query': {
type: 'database-query',
title: '数据库查询',
icon: <DatabaseIcon />,
category: 'data',
description: '从数据库查询数据并输出给下游节点',
},
}
十三、将 Workflow 画布剥离嵌入自有项目
这是最复杂的方案,目标是将 ReactFlow 画布作为一个独立组件嵌入到你的 React 项目中。
13.1 依赖安装
bash
pnpm add @xyflow/react zustand immer
pnpm add -D @types/react @types/react-dom
13.2 最小化 Canvas 组件(从 Dify 提取)
tsx
// src/components/workflow-canvas/index.tsx(你的项目)
import { ReactFlow, ReactFlowProvider, Background, Controls } from '@xyflow/react'
import '@xyflow/react/dist/style.css'
import { useWorkflowStore } from './store'
import { nodeTypes } from './node-types' // 从 Dify 迁移的节点组件
import { CustomEdge } from './custom-edge'
interface WorkflowCanvasProps {
initialNodes?: WorkflowNode[]
initialEdges?: WorkflowEdge[]
readonly?: boolean
onSave?: (nodes: WorkflowNode[], edges: WorkflowEdge[]) => void
}
export const WorkflowCanvas: FC<WorkflowCanvasProps> = ({
initialNodes = [],
initialEdges = [],
readonly = false,
onSave,
}) => {
const {
nodes, edges,
onNodesChange, onEdgesChange, onConnect,
} = useWorkflowStore(initialNodes, initialEdges)
return (
<div style={{ width: '100%', height: '100%', minHeight: '600px' }}>
<ReactFlowProvider>
<ReactFlow
nodes={nodes}
edges={edges}
nodeTypes={nodeTypes}
edgeTypes={{ custom: CustomEdge }}
onNodesChange={readonly ? undefined : onNodesChange}
onEdgesChange={readonly ? undefined : onEdgesChange}
onConnect={readonly ? undefined : onConnect}
nodesDraggable={!readonly}
nodesConnectable={!readonly}
fitView
>
<Background />
<Controls />
{!readonly && (
<button
onClick={() => onSave?.(nodes, edges)}
className="absolute top-4 right-4 z-10 px-4 py-2 bg-blue-600 text-white rounded-lg"
>
保存工作流
</button>
)}
</ReactFlow>
</ReactFlowProvider>
</div>
)
}
13.3 DSL 序列化(保存/加载工作流)
typescript
// 将画布状态序列化为 Dify DSL 格式
export function serializeWorkflow(
nodes: WorkflowNode[],
edges: WorkflowEdge[]
): DifyWorkflowDSL {
return {
version: '0.6',
kind: 'Workflow',
graph: {
nodes: nodes.map(node => ({
id: node.id,
type: node.data.type,
position: node.position,
data: omit(node.data, ['_runningStatus', '_connectedSourceHandleIds', ...]),
})),
edges: edges.map(edge => ({
id: edge.id,
source: edge.source,
target: edge.target,
sourceHandle: edge.sourceHandle,
targetHandle: edge.targetHandle,
})),
},
}
}
// 从 DSL 反序列化恢复画布
export function deserializeWorkflow(dsl: DifyWorkflowDSL): {
nodes: WorkflowNode[]
edges: WorkflowEdge[]
} {
return {
nodes: dsl.graph.nodes.map(n => ({
id: n.id,
type: n.type,
position: n.position,
data: { ...defaultConfig[n.type], ...n.data },
})),
edges: dsl.graph.edges.map(e => ({
id: e.id,
source: e.source,
target: e.target,
sourceHandle: e.sourceHandle,
targetHandle: e.targetHandle,
type: 'custom',
})),
}
}
13.4 在 Next.js / React 项目中使用
tsx
// pages/workflow-editor.tsx
import { WorkflowCanvas } from '@/components/workflow-canvas'
import { serializeWorkflow } from '@/components/workflow-canvas/utils'
export default function WorkflowEditorPage() {
const [savedDSL, setSavedDSL] = useState<string>('')
const handleSave = (nodes: WorkflowNode[], edges: WorkflowEdge[]) => {
const dsl = serializeWorkflow(nodes, edges)
const yaml = dslToYaml(dsl)
setSavedDSL(yaml)
// 保存到后端 API
fetch('/api/workflows', {
method: 'POST',
body: JSON.stringify({ dsl: yaml }),
})
}
return (
<div className="h-screen flex flex-col">
<header className="h-14 border-b flex items-center px-6">
<h1>工作流编辑器</h1>
</header>
<div className="flex-1">
<WorkflowCanvas onSave={handleSave} />
</div>
</div>
)
}
十四、常见避坑指南
14.1 修改代码不生效
bash
# ❌ 问题:Docker 使用的是远程镜像
# ✅ 解决:切换为本地构建
docker compose down
# 修改 docker-compose.yaml,将 image 改为 build:
docker compose up -d --build
14.2 开始节点被删除
typescript
// ❌ 直接使用 ReactFlow 默认删除
// ✅ 在 deleteNode action 中保护开始节点
deleteNode: (nodeId) => set((state) => {
const node = state.nodes.find(n => n.id === nodeId)
if (node?.data.type === BlockEnum.Start) return {} // 受保护,直接返回
return { nodes: state.nodes.filter(n => n.id !== nodeId) }
})
14.3 Windows 开发环境 localhost 访问问题
yaml
# ❌ Docker 容器内访问 localhost 会报错
# ✅ 使用 host.docker.internal
NEXT_PUBLIC_API_PREFIX=http://host.docker.internal:5001/console/api
14.4 变量引用循环依赖
typescript
// ❌ 节点 A 引用 B,节点 B 又引用 A → 造成 DAG 循环
// ✅ 在 onConnect 时检测是否引入环
const onConnect = (connection: Connection) => {
if (wouldCreateCycle(connection, get().edges, get().nodes)) {
toast.error('连接会导致循环依赖,已取消')
return
}
set(state => ({ edges: addEdge(connection, state.edges) }))
}
14.5 大型工作流性能问题
tsx
// ✅ 节点组件必须用 memo 包裹
const MyNode = memo<NodeProps<MyNodeType>>(({ data }) => {
return <div>{data.title}</div>
})
// ✅ useSelector 精确订阅,避免全量重渲染
const nodeCount = useWorkflowStore(state => state.nodes.length)
// ❌ 不要这样(订阅整个 store)
const store = useWorkflowStore()
十五、总结
15.1 架构全景回顾

ReactFlow 画布层
↕(onNodesChange / onEdgesChange / onConnect)
Zustand Store 层(nodes / edges / 运行态 / history)
↕(useNodeCrud / useWorkflow hooks)
节点 Panel 层(panel.tsx + use-config.ts)
↕(VarReferencePicker / Field / OutputVars)
变量引用系统(ValueSelector + VarType + 可用变量计算)
↕(SSE 流式推送)
工作流执行层(useWorkflowRun + EventSource)
15.2 二次开发路径选择
| 你的需求 | 推荐方案 | 预计工期 |
|---|---|---|
| 在网站嵌入 AI 对话 | iframe / JS SDK | < 1 天 |
| 自定义对话 UI | API 直调 | 2-5 天 |
| 修改现有节点外观/配置 | 源码二开 + Docker 本地构建 | 1-3 天 |
| 新增自定义节点 | 四件套约定(前端+后端) | 3-7 天 |
| 将画布嵌入自有 React 项目 | 提取 workflow 模块 + @xyflow/react | 1-3 周 |
| 完全自研编排引擎 | 参考设计,基于 ReactFlow 从头实现 | 1-3 月 |
15.3 核心设计借鉴点
无论你选择哪种方案,以下设计值得在自己项目中复用:
- 四件套节点约定(panel / use-config / types / default)------ 新增节点成本极低
- ValueSelector 变量路径系统------ 类型安全的跨节点数据引用
- immer + useNodeCrud 模式 ------ 不可变状态更新简洁高效
- SSE 实时状态推送------ 节点运行态实时可视化
- 保护特殊节点的删除逻辑------ 防止用户误操作破坏流程结构
🔗 参考资源
- Dify 源码:https://github.com/langgenius/dify/tree/main/web/app/components/workflow
- ReactFlow 文档:https://reactflow.dev/docs
- DeepWiki 架构分析:https://deepwiki.com/langgenius/dify/10.3-workflow-node-ui-components
- dify-plus 二次开发参考:https://github.com/YFGaia/dify-plus
基于 Dify v1.13.3 · @xyflow/react v12 · 欢迎交流前端架构设计经验!