一、项目架构设计
1.1 整体架构概览
本工作流编辑器基于 Vue 3 + TypeScript + Pinia 技术栈,核心图形渲染依赖 AntV X6 图引擎。整体采用 分层架构,从下到上依次为:
┌─────────────────────────────────────────────────────────────┐
│ 视图层 (View Layer) │
│ WorkflowEditor | Toolbar | InspectorPanel | DryRunPanel │
├─────────────────────────────────────────────────────────────┤
│ 状态管理层 (Store Layer) │
│ graphStore | workflowStore | dryRunStore | uiStore │
├─────────────────────────────────────────────────────────────┤
│ 业务逻辑层 (Service Layer) │
│ graphOperations | eventService | uiService │
├─────────────────────────────────────────────────────────────┤
│ 核心引擎层 (Core Layer) │
│ dag-validator | topology-sort | workflow-executor │
├─────────────────────────────────────────────────────────────┤
│ 基础类型层 (Type Layer) │
│ graph.ts | workflow.ts | inspector.ts │
└─────────────────────────────────────────────────────────────┘
1.2 核心组件职责
| 组件/模块 | 职责描述 | 关键文件 |
|---|---|---|
| WorkflowEditor | 主编辑器容器,管理画布和面板状态 | WorkflowEditor.vue |
| graphStore | 图形状态管理,节点增删改查 | stores/graph/index.ts |
| dag-validator | DAG 循环检测与工作流验证 | utils/dag-validator.ts |
| topology-sort | 拓扑排序与执行顺序计算 | utils/topology-sort.ts |
| workflow-executor | 工作流执行引擎 | executor/workflow-executor.ts |
| dryRunStore | 试运行状态管理 | stores/dryRunStore.ts |
1.3 状态管理设计
项目采用 Pinia 进行状态管理,核心 Store 包括:
- graphStore:管理 X6 图形实例、节点选择状态、历史记录
- workflowStore:管理工作流执行状态
- dryRunStore:管理试运行功能的状态和历史记录
- uiStore:管理 UI 面板的显示/隐藏状态
二、DAG(有向无环图)校验机制
2.1 循环检测算法
项目采用 深度优先搜索 (DFS) 算法检测图中的循环,核心实现位于 dag-validator.ts:
export function detectCycle(graph: Graph): { hasCycle: boolean; cycle?: string[] } {
const adjacencyList: Record<string, string[]> = {};
nodes.forEach((node: GraphNode) => {
adjacencyList[node.id] = [];
});
edges.forEach((edge: GraphEdge) => {
const source = edge.getSourceCellId();
const target = edge.getTargetCellId();
if (source && target) {
adjacencyList[source].push(target);
}
});
const visited = new Set<string>();
const recStack = new Set<string>();
const path: string[] = [];
const dfs = (nodeId: string): string[] | null => {
visited.add(nodeId);
recStack.add(nodeId);
path.push(nodeId);
for (const neighbor of adjacencyList[nodeId]) {
if (!visited.has(neighbor)) {
const result = dfs(neighbor);
if (result !== null) return result;
} else if (recStack.has(neighbor)) {
const cycleStartIndex = path.indexOf(neighbor);
const cyclePath = path.slice(cycleStartIndex);
return [...cyclePath, neighbor];
}
}
path.pop();
recStack.delete(nodeId);
return null;
};
// ...
}
算法要点:
- 使用
visited集合记录已访问节点 - 使用
recStack集合记录当前递归路径 - 当发现回边(指向递归栈中节点的边)时,提取循环路径
2.2 工作流完整性验证
validateWorkflow 函数执行多项验证规则:
| 验证项 | 错误类型 | 触发条件 |
|---|---|---|
| 循环检测 | cycle | 图中存在环 |
| 空工作流 | empty | 无节点 |
| 缺少开始节点 | missing_start | 无 INPUT 类型节点 |
| 多个开始节点 | multiple_starts | 多个 INPUT 类型节点 |
| 缺少结束节点 | missing_end | 无 OUTPUT 类型节点 |
| 孤立节点 | isolated_node | 节点无任何连接 |
2.3 连接验证规则
connection.ts 中定义了连接的约束规则:
export function validateConnection(
sourceCell, targetCell, sourceMagnet, targetMagnet, graph
) {
// 1. 基础验证
if (!sourceMagnet) return { valid: false, reason: '源连接点不存在' };
if (!targetMagnet) return { valid: false, reason: '目标连接点不存在' };
if (sourceCell === targetCell) return { valid: false, reason: '不能连接到自身' };
// 2. 节点类型限制
if (sourceType === 'OUTPUT') return { valid: false, reason: '输出节点不能作为源节点' };
if (targetType === 'INPUT') return { valid: false, reason: '开始节点不能作为目标节点' };
// 3. 端口方向限制
if (!isOutputPort(sourcePortGroup)) return { valid: false, reason: '输入桩不能发起连线' };
if (!isInputPort(targetPortGroup)) return { valid: false, reason: '输出桩不能接收连线' };
// 4. 循环节点内部连接限制
const loopValidation = validateLoopNodeConnection(sourceCell, targetCell);
if (!loopValidation.valid) return { valid: false, reason: loopValidation.reason };
// 5. 重复连接检测
const existingConnection = edges.find(edge =>
edge.getSourceCellId() === sourceCellData.id &&
edge.getTargetCellId() === targetCellData.id
);
if (existingConnection) return { valid: false, reason: '这两节点之间已存在连线' };
return { valid: true, reason: '' };
}
三、执行器实现
3.1 执行器架构设计
WorkflowExecutor 采用 策略模式,支持不同节点类型的定制化执行:
export interface NodeExecutor<T = unknown> {
execute(node: GraphNode, context: ExecutionContext): Promise<T>;
canRetry?(error: unknown): boolean;
}
export class WorkflowExecutor {
private graph: Graph;
private nodeExecutors: Record<string, NodeExecutor>;
private workflowState: WorkflowState;
private nodeStates: Record<string, ExecutionState>;
// 核心执行逻辑
public async execute(): Promise<ExecutionResult> {
const executionOrder = getExecutionOrder(this.graph);
for (const nodeId of executionOrder) {
const node = this.graph.getNodes().find(n => n.id === nodeId);
if (!node) continue;
await this.waitIfPaused();
if (this.abortController.signal.aborted) { /* 处理取消 */ }
this.workflowState.currentNodeId = nodeId;
const result = await this.executeNodeWithRetry(node);
if (!result.success) {
return { success: false, error: result.error, workflowState: this.workflowState };
}
this.updateProgress();
}
// ...
}
}
3.2 重试机制
执行器内置 指数退避重试 策略:
private async executeNodeWithRetry(node: GraphNode): Promise<{ success: boolean; error?: unknown }> {
for (let attempt = 0; attempt <= nodeState.maxRetries; attempt++) {
// 检查取消信号
if (this.abortController && this.abortController.signal.aborted) {
return { success: false, error: new Error('Execution aborted') };
}
try {
this.updateNodeState(node.id, { status: 'running', startTime: Date.now(), retryCount: attempt });
const result = await executor.execute(node, context);
this.outputs[node.id] = result;
this.updateNodeState(node.id, { status: 'completed', endTime: Date.now(), output: result });
return { success: true };
} catch (error) {
const canRetry = executor.canRetry ? executor.canRetry(error) : true;
if (attempt >= nodeState.maxRetries || !canRetry) {
this.updateNodeState(node.id, { status: 'error', error: error.message, endTime: Date.now() });
return { success: false, error };
}
// 指数退避延迟
await this.delay(this.options.retryDelay * Math.pow(2, attempt));
}
}
}
3.3 执行控制能力
执行器支持完整的生命周期控制:
| 操作 | 方法 | 功能说明 |
|---|---|---|
| 启动 | execute() | 开始工作流执行 |
| 取消 | abort() | 立即终止执行 |
| 暂停 | pause() | 暂停执行流程 |
| 恢复 | resume() | 恢复暂停的执行 |
四、性能优化策略
4.1 拓扑排序缓存
topology-sort.ts 实现了智能缓存机制:
const TOPOLOGY_CACHE_MAX_AGE = 5000; // 5秒过期
const TOPOLOGY_CACHE_SIZE = 100; // 最多缓存100个条目
const topologyCache = new Map<string, CacheEntry>();
export function topologySort(graph: TopoGraph): string[] {
const cacheKey = generateCacheKey(nodes, edges);
const cachedResult = getCachedResult(cacheKey);
if (cachedResult) {
return cachedResult;
}
// 执行拓扑排序...
const result = /* 排序结果 */;
setCacheResult(cacheKey, result);
return result;
}
缓存策略:
- 时间过期:缓存条目 5 秒后自动失效
- LRU 淘汰:超过最大容量时删除最旧条目
- 键生成:基于节点 ID 和边信息的哈希值
4.2 事件节流与防抖
在 events.ts 中应用了性能优化:
const THROTTLE_DELAY = 50;
const DEBOUNCE_DELAY = 100;
// 节流:节点悬停事件
graphRef.value.on(
'node:mouseover',
throttle(({ node }: { node: Node }) => {
handleNodeMouseOver(node);
}, THROTTLE_DELAY)
);
// 防抖:节点位置变化事件
const positionHandler = debounce(
({ node, options }) => {
// 处理节点位置变化
},
DEBOUNCE_DELAY
);
4.3 渲染优化
通过 Z-index 管理 优化图层渲染:
graphRef.value.on('node:change:zIndex', ({ node, current }) => {
const nodeData = node.getData();
if (nodeData?.type === 'LOOP') {
const children = node.getChildren?.();
if (children) {
children.forEach((child) => {
child.setZIndex(current + 1);
});
}
}
});
五、关键技术难点与解决方案
坑点一:循环节点内部连接限制
问题描述: 循环节点(LOOP)内部的子节点需要与外部节点隔离,不能从外部直接连接到循环内部,也不能从循环内部连接到外部。
解决方案:
在 connection.ts 中实现 validateLoopNodeConnection 函数:
export function validateLoopNodeConnection(sourceCell, targetCell): LoopValidationResult {
const sourceLoopResult = isLoopChildNode(sourceNode);
const targetLoopResult = isLoopChildNode(targetNode);
// 循环内部节点不能连接到外部
if (sourceLoopResult.isLoopChild && !targetLoopResult.isLoopChild) {
return { valid: false, reason: '循环节点内部的子节点只能连接到同一循环节点内的其他子节点' };
}
// 外部节点不能连接到循环内部
if (!sourceLoopResult.isLoopChild && targetLoopResult.isLoopChild) {
return { valid: false, reason: '不能从循环节点外部连接到循环节点内部' };
}
// 不同循环节点的内部不能互相连接
if (sourceLoopResult.isLoopChild && targetLoopResult.isLoopChild) {
if (sourceLoopResult.loopNode?.id !== targetLoopResult.loopNode?.id) {
return { valid: false, reason: '循环节点内部的子节点只能连接到同一循环节点内' };
}
}
return { valid: true, reason: '' };
}
坑点二:节点嵌入与父子关系管理
问题描述: 当用户拖拽节点进入/离开循环节点时,需要正确维护父子关系,并处理节点移动时的边界检测。
解决方案:
在 init.ts 中配置 embedding 选项:
embedding: {
enabled: true,
findParent(this: Graph, { node }): Cell[] {
const bbox = node.getBBox();
return this.getNodes().filter((n) => {
const data = n.getData();
if (data && data.type === 'LOOP') {
if (data.collapsed === true) return false;
const targetBBox = n.getBBox();
return bbox.isIntersectWithRect(targetBBox);
}
return false;
});
},
validate(this: Graph, { child, parent }): boolean {
const forbiddenComponents = ['LOOP', 'INPUT'];
if (forbiddenComponents.includes(child.getData()?.type || '')) {
return false;
}
// 已有连接的节点不能拖入循环(需按Ctrl)
if (!child.parent) {
const hasNoConnectedEdges = this.getConnectedEdges(child).length === 0;
return hasNoConnectedEdges && ctrlPressed.value;
}
return false;
},
}
坑点三:实时状态更新的性能问题
问题描述: 工作流执行时,每个节点状态变化都会触发 UI 更新,大量节点执行会导致性能问题。
解决方案:
在 dryRunStore.ts 中使用 批量更新 策略:
const executeWithRealtimeUpdate = async () => {
for (let i = 0; i < executionOrder.length; i++) {
const nodeId = executionOrder[i];
// 暂停检测(让出主线程)
await new Promise((resolve) => setTimeout(resolve, 0));
// 批量更新状态
executionStates[nodeId] = { ...executionStates[nodeId], status: 'running', startTime: Date.now() };
logs.push({ timestamp: Date.now(), level: 'info', message: `开始执行节点: ${nodeId}` });
// 单次状态更新
dryRunState.value.currentRecord = {
...dryRunState.value.currentRecord!,
executionStates,
logs,
progress: Math.round((i / executionOrder.length) * 100),
};
// 执行节点...
}
};
坑点四:拓扑排序的正确性与缓存一致性
问题描述: 拓扑排序需要正确处理多种边界情况(自环、孤立节点、空图),同时缓存机制需要保证一致性。
解决方案:
- 完善的循环检测:
// 处理自环
if (source === target) {
return { hasCycle: true, cycle: [source, target] };
}
// 处理空图
if (nodes.length === 0) {
return { hasCycle: false };
}
- 智能缓存管理:
function cleanupCache(): void {
const now = Date.now();
const keysToDelete: string[] = [];
// 清理过期条目
for (const [key, entry] of topologyCache.entries()) {
if (now - entry.timestamp > TOPOLOGY_CACHE_MAX_AGE) {
keysToDelete.push(key);
}
}
// LRU 淘汰
if (topologyCache.size > TOPOLOGY_CACHE_SIZE) {
const entries = Array.from(topologyCache.entries());
entries.sort((a, b) => a[1].timestamp - b[1].timestamp);
const excess = topologyCache.size - TOPOLOGY_CACHE_SIZE;
for (let i = 0; i < excess; i++) {
topologyCache.delete(entries[i][0]);
}
}
}
坑点五:连接端口的交互体验
问题描述: 用户在创建连线时,需要清晰的视觉反馈来区分输入端口和输出端口。
解决方案:
- 端口样式区分:
// 输出端口(右侧/底部)- 绿色
// 输入端口(左侧/顶部)- 蓝色
export const OUTPUT_PORT_GROUPS = ['right', 'bottom'];
export const INPUT_PORT_GROUPS = ['left', 'top'];
- 悬停交互增强:
export function handleNodeMouseOver(node: Node): void {
const ports = node.getPorts();
ports.forEach((port) => {
if (port.group === 'right' || port.group === 'bottom') {
// 放大输出端口
node.setPortProp(port.id, 'attrs/circle/r', portInteractionStyles.nodeHover.r);
}
});
}
六、总结与展望
6.1 技术亮点
- 完整的 DAG 验证体系:循环检测、完整性验证、连接约束
- 灵活的执行器架构:策略模式 + 重试机制 + 生命周期控制
- 智能性能优化:缓存、节流、防抖、批量更新
- 完善的测试覆盖:单元测试覆盖核心算法
6.2 未来优化方向
- 并行执行支持:支持无依赖节点的并行执行
- 分布式执行:支持跨机器的分布式工作流执行
- 可视化调试器:实时查看执行流程和状态变化
- 协作编辑:支持多人实时协作编辑工作流
- 工作流模板市场:提供可复用的工作流模板
项目地址:https://github.com/wangjincai05/antvX6
预览地址:https://workflow.wantasy.asia/
核心文件清单:
src/utils/dag-validator.ts- DAG 校验src/utils/topology-sort.ts- 拓扑排序src/executor/workflow-executor.ts- 执行器src/stores/graph/index.ts- 图形状态管理src/components/workflow/WorkflowEditor.vue- 主编辑器
本文基于真实项目代码分析,涵盖了工作流编辑器从架构设计到性能优化的完整实现路径,希望能为开发者提供有价值的参考。