Files
aida_front/src/component/Canvas/CanvasEditor/managers/examples/CommandManager-Guide.md
X1627315083 584f6a7db0 合并画布
2025-06-22 13:52:28 +08:00

20 KiB
Raw Blame History

CommandManager 命令管理器使用指南

📖 概述

CommandManager 是一个基于经典命令模式的撤销/重做系统,提供队列机制防止快速执行时命令丢失,支持复合命令进行批量操作,确保了高性能和稳定性。

核心特性

  • 撤销/重做: 基于经典命令模式的撤销重做功能
  • 队列机制: 防止快速执行多个命令时的命令丢失
  • 串行化执行: 确保命令按顺序执行,避免并发问题
  • Promise支持: 完全支持异步命令
  • 性能监控: 可选的性能统计功能
  • 复合命令: 支持批量操作和命令组合
  • 状态管理: 实时状态监控和回调

🎯 复合命令详解

什么是复合命令?

复合命令是一组相关操作的集合这些操作作为一个整体执行支持命令间的值传递和条件执行。在Canvas编辑器中复合命令特别适用于

  • 创建复杂组件(同时创建多个对象和图层)
  • 批量修改属性
  • 导入文件(可能涉及多个图层和对象的创建)
  • 复制粘贴操作

复合命令的执行流程

graph TD
    A[创建复合命令] --> B[添加子命令]
    B --> C[执行复合命令]
    C --> D[串行执行子命令]
    D --> E{执行成功?}
    E -->|是| F[记录已执行命令]
    F --> G[继续下一个命令]
    G --> D
    E -->|否| H[回滚已执行命令]
    H --> I[抛出错误]
    
    C --> J[撤销复合命令]
    J --> K[逆序撤销子命令]

🚀 快速开始

1. 基础批量操作

import { CommandManager } from './CommandManager.js';
import { Command } from '../../commands/Command.js';

// 创建命令管理器
const commandManager = new CommandManager({
  maxHistorySize: 50,
  performanceManager: null
});

// 创建简单命令
class SetPropertyCommand extends Command {
  constructor(target, property, newValue) {
    super({ name: '设置属性', description: `设置${property}${newValue}` });
    this.target = target;
    this.property = property;
    this.newValue = newValue;
    this.oldValue = null;
  }

  execute() {
    this.oldValue = this.target[this.property];
    this.target[this.property] = this.newValue;
    return { property: this.property, oldValue: this.oldValue, newValue: this.newValue };
  }

  undo() {
    this.target[this.property] = this.oldValue;
    return { property: this.property, restoredValue: this.oldValue };
  }
}

// 使用批量操作
const obj = { x: 0, y: 0, color: 'black' };

// 方法1: 使用 executeBatch
await commandManager.executeBatch([
  new SetPropertyCommand(obj, 'x', 100),
  new SetPropertyCommand(obj, 'y', 200),
  new SetPropertyCommand(obj, 'color', 'red')
], '批量设置对象属性');

console.log(obj); // { x: 100, y: 200, color: 'red' }

// 撤销时,所有属性会一次性恢复
await commandManager.undo();
console.log(obj); // { x: 0, y: 0, color: 'black' }

2. 创建自定义复合命令

import { CompositeCommand } from '../../commands/Command.js';

// 支持值传递的复合命令
class CreateLayerWithObjectsCommand extends CompositeCommand {
  constructor(layerName, objects) {
    super([], { name: '创建图层并添加对象' });
    this.layerName = layerName;
    this.objects = objects;
    this.layerId = null;
  }
  
  async execute() {
    // 先创建图层
    const createLayerCmd = new CreateLayerCommand(this.layerName);
    this.layerId = await createLayerCmd.execute();
    this.addCommand(createLayerCmd);
    
    // 使用返回的 layerId 添加对象
    for (const obj of this.objects) {
      const addObjectCmd = new AddObjectCommand(obj, this.layerId);
      await addObjectCmd.execute();
      this.addCommand(addObjectCmd);
    }
    
    this.executedCommands = [...this.commands];
    return { layerId: this.layerId, objectCount: this.objects.length };
  }
}

// 使用自定义复合命令
const objects = [object1, object2, object3];
const command = new CreateLayerWithObjectsCommand('新图层', objects);
const result = await commandManager.execute(command);
console.log(`创建了图层 ${result.layerId},包含 ${result.objectCount} 个对象`);

🔧 高级功能

1. 条件执行的复合命令

class ConditionalSetupCommand extends CompositeCommand {
  constructor(config) {
    super([], { name: '条件设置' });
    this.config = config;
  }
  
  async execute() {
    // 总是创建基础对象
    const createCmd = new CreateObjectCommand(this.config.object);
    await createCmd.execute();
    this.addCommand(createCmd);
    
    // 根据条件决定是否创建图层
    if (this.config.shouldCreateLayer) {
      const layerCmd = new CreateLayerCommand(this.config.layerName);
      await layerCmd.execute();
      this.addCommand(layerCmd);
    }
    
    // 根据条件应用样式
    if (this.config.applyStyle) {
      const styleCmd = new ApplyStyleCommand(this.config.style);
      await styleCmd.execute();
      this.addCommand(styleCmd);
    }
    
    this.executedCommands = [...this.commands];
    return { commandCount: this.commands.length };
  }
}

2. 错误处理和回滚

class RobustBatchCommand extends CompositeCommand {
  constructor(operations) {
    super([], { name: '健壮的批量操作' });
    this.operations = operations;
  }
  
  async execute() {
    try {
      for (const operation of this.operations) {
        const command = new operation.CommandClass(...operation.args);
        
        // 执行命令
        await command.execute();
        this.addCommand(command);
        
        console.log(`✅ 操作成功: ${command.constructor.name}`);
      }
      
      this.executedCommands = [...this.commands];
      return { success: true, executedCount: this.commands.length };
    } catch (error) {
      console.error('❌ 批量操作失败:', error);
      
      // 自动回滚已执行的命令
      await this._rollbackExecutedCommands();
      throw error;
    }
  }
}

3. 状态监控

// 设置状态变化回调
commandManager.setChangeCallback((state) => {
  console.log('命令管理器状态:', {
    canUndo: state.canUndo,
    canRedo: state.canRedo,
    undoCount: state.undoCount,
    redoCount: state.redoCount,
    isExecuting: state.isExecuting,
    lastCommand: state.lastCommand
  });
});

// 获取命令历史
const history = commandManager.getHistory();
console.log('撤销历史:', history.undoHistory);
console.log('重做历史:', history.redoHistory);

💡 实际应用场景

1. 复杂UI组件创建

class CreateComplexCardCommand extends CompositeCommand {
  constructor(canvas, config) {
    super([], { name: '创建复杂卡片' });
    this.canvas = canvas;
    this.config = config;
    this.createdObjects = [];
  }

  async execute() {
    try {
      // 创建背景
      const background = await this._createBackground();
      const bgCmd = new AddObjectCommand(background);
      await bgCmd.execute();
      this.addCommand(bgCmd);

      // 创建头部
      const header = await this._createHeader();
      const headerCmd = new AddObjectCommand(header);
      await headerCmd.execute();
      this.addCommand(headerCmd);

      // 创建内容区域
      const content = await this._createContent();
      const contentCmd = new AddObjectCommand(content);
      await contentCmd.execute();
      this.addCommand(contentCmd);

      // 创建按钮
      const buttons = await this._createButtons();
      for (const button of buttons) {
        const buttonCmd = new AddObjectCommand(button);
        await buttonCmd.execute();
        this.addCommand(buttonCmd);
      }

      this.executedCommands = [...this.commands];
      return { success: true, objectCount: this.commands.length };
    } catch (error) {
      await this._rollbackExecutedCommands();
      throw error;
    }
  }

  async _createBackground() {
    // 创建背景逻辑
    return new fabric.Rect({
      width: this.config.width,
      height: this.config.height,
      fill: this.config.backgroundColor
    });
  }

  // ...existing code...
}

2. 批量导入处理

class ImportMultipleImagesCommand extends CompositeCommand {
  constructor(files, layerManager) {
    super([], { name: `导入${files.length}个图片文件` });
    this.files = files;
    this.layerManager = layerManager;
    this.importedLayers = [];
  }

  async execute() {
    try {
      for (const file of this.files) {
        // 加载图片
        const image = await this._loadImageFromFile(file);
        
        // 创建图层
        const createLayerCmd = new CreateLayerCommand(file.name);
        const layerId = await createLayerCmd.execute();
        this.addCommand(createLayerCmd);
        
        // 添加对象到图层
        const addObjectCmd = new AddObjectToLayerCommand(image, layerId);
        await addObjectCmd.execute();
        this.addCommand(addObjectCmd);
        
        this.importedLayers.push(layerId);
      }
      
      this.executedCommands = [...this.commands];
      return { importedLayers: this.importedLayers };
    } catch (error) {
      console.error('批量导入失败:', error);
      await this._rollbackExecutedCommands();
      throw error;
    }
  }

  async _loadImageFromFile(file) {
    return new Promise((resolve, reject) => {
      const reader = new FileReader();
      reader.onload = (e) => {
        fabric.Image.fromURL(e.target.result, resolve);
      };
      reader.onerror = reject;
      reader.readAsDataURL(file);
    });
  }
}

// 使用
const files = [file1, file2, file3];
const importCmd = new ImportMultipleImagesCommand(files, layerManager);
const result = await commandManager.execute(importCmd);
console.log('导入的图层:', result.importedLayers);

3. 复制粘贴操作

class CopyPasteObjectsCommand extends CompositeCommand {
  constructor(sourceObjects, targetPosition) {
    super([], { name: '复制粘贴对象' });
    this.sourceObjects = sourceObjects;
    this.targetPosition = targetPosition;
    this.copiedObjects = [];
  }

  async execute() {
    try {
      for (const obj of this.sourceObjects) {
        // 克隆对象
        const clonedObj = await this._cloneObject(obj);
        
        // 设置新位置
        clonedObj.set({
          left: this.targetPosition.x + (clonedObj.left - this.sourceObjects[0].left),
          top: this.targetPosition.y + (clonedObj.top - this.sourceObjects[0].top)
        });

        // 添加到画布
        const addCmd = new AddObjectCommand(clonedObj);
        await addCmd.execute();
        this.addCommand(addCmd);
        
        this.copiedObjects.push(clonedObj);
      }
      
      this.executedCommands = [...this.commands];
      return { copiedObjects: this.copiedObjects };
    } catch (error) {
      await this._rollbackExecutedCommands();
      throw error;
    }
  }

  async _cloneObject(obj) {
    return new Promise((resolve) => {
      obj.clone(resolve);
    });
  }
}

🛠️ 错误处理策略

1. 复合命令中的错误处理

// 推荐的错误处理模式
class SafeBatchCommand extends CompositeCommand {
  constructor(operations, options = {}) {
    super([], { name: options.name || '安全批量操作' });
    this.operations = operations;
    this.stopOnError = options.stopOnError !== false; // 默认遇到错误就停止
  }

  async execute() {
    const results = [];
    const errors = [];

    for (let i = 0; i < this.operations.length; i++) {
      const operation = this.operations[i];
      
      try {
        const command = new operation.CommandClass(...operation.args);
        const result = await command.execute();
        
        this.addCommand(command);
        results.push({ index: i, success: true, result });
        
        console.log(`✅ 操作 ${i + 1}/${this.operations.length} 成功`);
      } catch (error) {
        errors.push({ index: i, error: error.message });
        console.error(`❌ 操作 ${i + 1}/${this.operations.length} 失败:`, error);
        
        if (this.stopOnError) {
          // 回滚已执行的命令
          await this._rollbackExecutedCommands();
          throw new Error(`批量操作在第 ${i + 1} 个操作时失败: ${error.message}`);
        }
      }
    }

    this.executedCommands = [...this.commands];
    return { 
      results, 
      errors, 
      successCount: results.length,
      errorCount: errors.length 
    };
  }
}

2. 超时处理

class TimeLimitedCommand extends CompositeCommand {
  constructor(operations, timeout = 5000) {
    super([], { name: '限时批量操作' });
    this.operations = operations;
    this.timeout = timeout;
  }

  async execute() {
    const timeoutPromise = new Promise((_, reject) => {
      setTimeout(() => reject(new Error('操作超时')), this.timeout);
    });

    try {
      const executePromise = this._executeOperations();
      const result = await Promise.race([executePromise, timeoutPromise]);
      
      this.executedCommands = [...this.commands];
      return result;
    } catch (error) {
      await this._rollbackExecutedCommands();
      throw error;
    }
  }

  async _executeOperations() {
    for (const operation of this.operations) {
      const command = new operation.CommandClass(...operation.args);
      await command.execute();
      this.addCommand(command);
    }
    return { success: true, operationCount: this.operations.length };
  }
}

📊 最佳实践

1. 命令粒度控制

// ✅ 好的命令粒度 - 逻辑相关的操作组合
class CreateUserProfileCommand extends CompositeCommand {
  constructor(userData) {
    super([], { name: '创建用户档案' });
    this.userData = userData;
  }
  
  async execute() {
    const avatarCmd = new CreateAvatarCommand(this.userData.avatar);
    await avatarCmd.execute();
    this.addCommand(avatarCmd);
    
    const nameCmd = new CreateNameLabelCommand(this.userData.name);
    await nameCmd.execute();
    this.addCommand(nameCmd);
    
    const infoCmd = new CreateInfoPanelCommand(this.userData.info);
    await infoCmd.execute();
    this.addCommand(infoCmd);
    
    this.executedCommands = [...this.commands];
    return { success: true };
  }
}

// ❌ 过大的命令粒度 - 不相关的操作混合
// 应该拆分成多个独立的命令

2. 命名规范

// ✅ 清晰的命令命名
await commandManager.executeBatch([...], '上传并创建图片图层');
new CreateLayerWithObjectsCommand('新图层', objects);
new ImportMultipleImagesCommand(files, layerManager);

// ❌ 模糊的命令命名
await commandManager.executeBatch([...], '操作');
new BatchCommand();
new ProcessCommand();

3. 性能优化

// 对于大量操作,考虑分批处理
class BatchProcessor {
  constructor(commandManager, batchSize = 10) {
    this.commandManager = commandManager;
    this.batchSize = batchSize;
  }

  async processBatch(items, createCommand, progressCallback) {
    const batches = this._createBatches(items, this.batchSize);

    for (let i = 0; i < batches.length; i++) {
      const batch = batches[i];
      const commands = batch.map(item => createCommand(item));
      
      await this.commandManager.executeBatch(
        commands, 
        `批处理 ${i + 1}/${batches.length}`
      );
      
      // 更新进度
      if (progressCallback) {
        const progress = ((i + 1) / batches.length) * 100;
        progressCallback(progress);
      }
    }
  }

  _createBatches(items, batchSize) {
    const batches = [];
    for (let i = 0; i < items.length; i += batchSize) {
      batches.push(items.slice(i, i + batchSize));
    }
    return batches;
  }
}

// 使用示例
const processor = new BatchProcessor(commandManager, 10);
await processor.processBatch(
  largeItemList,
  (item) => new ProcessItemCommand(item),
  (progress) => console.log(`处理进度: ${progress.toFixed(1)}%`)
);

🔧 API 参考

CommandManager 核心方法

// 执行单个命令
await commandManager.execute(command: Command): Promise<any>

// 批量执行命令
await commandManager.executeBatch(commands: Command[], batchName?: string): Promise<any>

// 撤销/重做
await commandManager.undo(): Promise<any>
await commandManager.redo(): Promise<any>

// 状态管理
commandManager.getState(): ManagerState
commandManager.getHistory(): HistoryInfo
commandManager.clear(): void

// 回调设置
commandManager.setChangeCallback(callback: (state: ManagerState) => void): void

CompositeCommand 核心方法

// 添加子命令
compositeCommand.addCommand(command: Command): CompositeCommand
compositeCommand.addCommands(commands: Command[]): CompositeCommand

// 执行和撤销
await compositeCommand.execute(): Promise<any>
await compositeCommand.undo(): Promise<any>

// 获取信息
compositeCommand.getInfo(): CommandInfo

🚨 常见问题

Q1: 什么时候使用 executeBatch什么时候创建自定义复合命令

A:

  • 使用 executeBatch: 当命令之间没有数据依赖,只是简单的批量执行时
  • 创建自定义复合命令: 当需要命令间值传递、条件执行或复杂逻辑时
// 简单批量 - 使用 executeBatch
await commandManager.executeBatch([
  new SetPropertyCommand(obj, 'x', 100),
  new SetPropertyCommand(obj, 'y', 200)
], '批量设置属性');

// 复杂逻辑 - 创建自定义复合命令
class CreateLayerWithObjectsCommand extends CompositeCommand {
  // 需要使用前一个命令的返回值
}

Q2: 复合命令执行失败时会自动回滚吗?

A: 是的,CompositeCommand 在执行失败时会自动回滚已执行的子命令。

// 如果第3个命令失败前2个命令会被自动撤销
const commands = [command1, command2, failingCommand, command4];
try {
  await commandManager.executeBatch(commands);
} catch (error) {
  // 此时 command1 和 command2 已被自动撤销
}

Q3: 如何处理部分命令失败的情况?

A: 创建自定义复合命令,设置错误处理策略:

class TolerantBatchCommand extends CompositeCommand {
  constructor(commands, options = {}) {
    super([], { name: '容错批量命令' });
    this.originalCommands = commands;
    this.continueOnError = options.continueOnError || false;
  }
  
  async execute() {
    const results = [];
    
    for (const cmd of this.originalCommands) {
      try {
        await cmd.execute();
        this.addCommand(cmd);
        results.push({ success: true, command: cmd.constructor.name });
      } catch (error) {
        results.push({ success: false, error: error.message });
        
        if (!this.continueOnError) {
          await this._rollbackExecutedCommands();
          throw error;
        }
      }
    }
    
    this.executedCommands = [...this.commands];
    return results;
  }
}

Q4: 如何优化大量命令的性能?

A: 使用分批处理和进度反馈:

// 分批处理大量命令
const BATCH_SIZE = 20;
const batches = [];

for (let i = 0; i < largeCommandList.length; i += BATCH_SIZE) {
  batches.push(largeCommandList.slice(i, i + BATCH_SIZE));
}

for (const [index, batch] of batches.entries()) {
  await commandManager.executeBatch(batch, `批次 ${index + 1}/${batches.length}`);
  
  // 给UI更新的机会
  await new Promise(resolve => setTimeout(resolve, 0));
}

🎯 总结

移除事务机制后的 CommandManager 更加简洁和灵活:

  1. 简化架构: 去除了不必要的事务状态管理
  2. 更好的数据流: 复合命令支持命令间值传递
  3. 灵活的错误处理: 自定义复合命令可以实现各种错误处理策略
  4. 更清晰的API: executeBatch 用于简单批量,自定义复合命令用于复杂逻辑
  5. 更好的性能: 减少了状态管理开销

推荐的使用模式:

  • 简单批量操作: 使用 commandManager.executeBatch()
  • 复杂业务逻辑: 创建继承自 CompositeCommand 的自定义命令
  • 需要值传递: 在自定义复合命令中处理命令间的数据依赖
  • 错误处理: 根据业务需求在复合命令中实现相应的错误处理策略