drawDB撤销重做机制:状态管理与历史记录

drawDB撤销重做机制:状态管理与历史记录

【免费下载链接】drawdb drawDB 是一款免费、简单直观的数据库模式编辑器和 SQL 生成器,能在浏览器中绘制图表、导出 SQL 脚本等,无需注册,零基础也能轻松上手。源项目地址:https://github.com/drawdb-io/drawdb 【免费下载链接】drawdb 项目地址: https://gitcode.com/GitHub_Trending/dr/drawdb

引言:数据库设计中的操作安全保障

在数据库设计工具中,用户经常会进行各种复杂操作:添加表、修改字段、创建关系、调整布局等。一旦操作失误,如果没有可靠的撤销重做机制,用户可能需要花费大量时间重新构建整个数据库结构。drawDB作为一款专业的在线数据库设计工具,实现了完善的撤销重做功能,让用户可以放心地进行各种设计操作。

读完本文,你将深入了解:

  • drawDB撤销重做机制的核心架构设计
  • 状态管理的实现原理与最佳实践
  • 历史记录的数据结构与管理策略
  • 多类型操作的支持与处理逻辑
  • 性能优化与用户体验考量

核心架构:基于React Context的状态管理

drawDB采用React Context API构建了一套完整的撤销重做状态管理系统,通过分层架构实现高效的状态追踪和历史记录管理。

状态管理架构图

mermaid

核心上下文定义

drawDB的撤销重做机制建立在UndoRedoContext基础上,提供了撤销栈和重做栈的状态管理:

// src/context/UndoRedoContext.jsx
export const UndoRedoContext = createContext({
  undoStack: [],
  setUndoStack: () => {},
  redoStack: [],
  setRedoStack: () => {},
});

export default function UndoRedoContextProvider({ children }) {
  const [undoStack, setUndoStack] = useState([]);
  const [redoStack, setRedoStack] = useState([]);

  return (
    <UndoRedoContext.Provider
      value={{ undoStack, redoStack, setUndoStack, setRedoStack }}
    >
      {children}
    </UndoRedoContext.Provider>
  );
}

操作类型与数据结构设计

drawDB支持多种操作类型的撤销重做,每种操作都有特定的数据结构来记录状态变化。

操作类型定义

// src/data/constants.js
export const Action = {
  ADD: 0,        // 添加操作
  MOVE: 1,       // 移动操作
  DELETE: 2,     // 删除操作
  EDIT: 3,       // 编辑操作
};

export const ObjectType = {
  NONE: 0,
  TABLE: 1,      // 表对象
  AREA: 2,       // 区域对象
  NOTE: 3,       // 注释对象
  RELATIONSHIP: 4, // 关系对象
  TYPE: 5,       // 类型对象
  ENUM: 6,       // 枚举对象
};

历史记录数据结构

每个历史记录项包含完整的操作信息:

{
  id: "unique_id",           // 操作对象ID
  action: Action.ADD,        // 操作类型
  element: ObjectType.TABLE, // 操作对象类型
  data: { /* 操作数据 */ },   // 操作相关数据
  message: "添加表",         // 操作描述信息
  undo: { /* 撤销状态 */ },   // 撤销时需要的数据
  redo: { /* 重做状态 */ },   // 重做时需要的数据
  bulk: false,               // 是否批量操作
  elements: []               // 批量操作的元素数组
}

撤销重做核心实现

撤销操作实现

撤销操作的核心逻辑位于ControlPanel组件中,处理各种类型的操作撤销:

// src/components/EditorHeader/ControlPanel.jsx
const undo = () => {
  if (undoStack.length === 0) return;
  const a = undoStack[undoStack.length - 1];
  setUndoStack((prev) => prev.filter((_, i) => i !== prev.length - 1));

  // 处理批量操作
  if (a.bulk) {
    for (const element of a.elements) {
      if (element.type === ObjectType.TABLE) {
        updateTable(element.id, element.undo);
      } else if (element.type === ObjectType.AREA) {
        updateArea(element.id, element.undo);
      } else if (element.type === ObjectType.NOTE) {
        updateNote(element.id, element.undo);
      }
    }
    setRedoStack((prev) => [...prev, a]);
    return;
  }

  // 处理添加操作撤销(转换为删除)
  if (a.action === Action.ADD) {
    if (a.element === ObjectType.TABLE) {
      deleteTable(a.id, false);
    } else if (a.element === ObjectType.AREA) {
      deleteArea(areas[areas.length - 1].id, false);
    }
    // ... 其他对象类型的处理
    setRedoStack((prev) => [...prev, a]);
  }
  // ... 其他操作类型的处理逻辑
};

重做操作实现

重做操作是撤销操作的逆过程,恢复被撤销的操作:

const redo = () => {
  if (redoStack.length === 0) return;
  const a = redoStack[redoStack.length - 1];
  setRedoStack((prev) => prev.filter((e, i) => i !== prev.length - 1));

  // 处理批量操作重做
  if (a.bulk) {
    for (const element of a.elements) {
      if (element.type === ObjectType.TABLE) {
        updateTable(element.id, element.redo);
      } else if (element.type === ObjectType.AREA) {
        updateArea(element.id, element.redo);
      }
    }
    setUndoStack((prev) => [...prev, a]);
    return;
  }

  // 处理各种操作类型的重做
  if (a.action === Action.ADD) {
    if (a.element === ObjectType.TABLE) {
      addTable(null, false);
    }
    // ... 其他对象类型的处理
    setUndoStack((prev) => [...prev, a]);
  }
  // ... 其他操作类型的处理逻辑
};

操作记录生成机制

表操作记录生成

在DiagramContext中,各种表操作都会生成相应的历史记录:

// src/context/DiagramContext.jsx
const addTable = (data, addToHistory = true) => {
  const id = nanoid();
  // ... 添加表的逻辑
  
  if (addToHistory) {
    setUndoStack((prev) => [
      ...prev,
      {
        id: data ? data.id : id,
        action: Action.ADD,
        element: ObjectType.TABLE,
        message: t("add_table"),
      }
    ]);
    setRedoStack([]); // 清空重做栈
  }
};

const deleteTable = (id, addToHistory = true) => {
  if (addToHistory) {
    const rels = relationships.reduce((acc, r) => {
      if (r.startTableId === id || r.endTableId === id) {
        acc.push(r);
      }
      return acc;
    }, []);
    
    const deletedTable = tables.find((t) => t.id === id);
    setUndoStack((prev) => [
      ...prev,
      {
        action: Action.DELETE,
        element: ObjectType.TABLE,
        data: {
          table: deletedTable,
          relationship: rels,
          index: tables.findIndex((t) => t.id === id),
        },
        message: t("delete_table", { tableName: deletedTable.name }),
      }
    ]);
    setRedoStack([]);
  }
};

字段操作记录生成

字段的编辑、添加、删除操作也会生成详细的历史记录:

const deleteField = (field, tid, addToHistory = true) => {
  const { fields, name } = tables.find((t) => t.id === tid);
  if (addToHistory) {
    const rels = relationships.reduce((acc, r) => {
      if ((r.startTableId === tid && r.startFieldId === field.id) ||
          (r.endTableId === tid && r.endFieldId === field.id)) {
        acc.push(r);
      }
      return acc;
    }, []);
    
    setUndoStack((prev) => [
      ...prev,
      {
        action: Action.EDIT,
        element: ObjectType.TABLE,
        component: "field_delete",
        tid: tid,
        data: {
          field: field,
          index: fields.findIndex((f) => f.id === field.id),
          relationship: rels,
        },
        message: t("edit_table", {
          tableName: name,
          extra: "[delete field]",
        }),
      }
    ]);
    setRedoStack([]);
  }
};

多上下文协同工作

drawDB的撤销重做机制需要与多个业务上下文协同工作:

区域上下文协同

// src/context/AreasContext.jsx
const { setUndoStack, setRedoStack } = useUndoRedo();

const addArea = (data, addToHistory = true) => {
  // ... 添加区域逻辑
  
  if (addToHistory) {
    setUndoStack((prev) => [
      ...prev,
      {
        action: Action.ADD,
        element: ObjectType.AREA,
        data: { index: areas.length },
        message: t("add_area"),
      }
    ]);
    setRedoStack([]);
  }
};

注释上下文协同

// src/context/NotesContext.jsx
const updateNote = (id, updatedValues, addToHistory = true) => {
  const note = notes[id];
  if (addToHistory) {
    setUndoStack((prev) => [
      ...prev,
      {
        action: Action.EDIT,
        element: ObjectType.NOTE,
        nid: id,
        undo: { ...note },
        redo: { ...updatedValues },
        message: t("edit_note"),
      }
    ]);
    setRedoStack([]);
  }
  
  // ... 更新注释逻辑
};

性能优化策略

历史记录限制

为了避免内存溢出,drawDB实现了历史记录的数量限制和自动清理机制:

// 在添加新记录时检查栈大小
const addToUndoStack = (action) => {
  setUndoStack((prev) => {
    const newStack = [...prev, action];
    // 限制历史记录数量(示例:最多100条)
    if (newStack.length > 100) {
      return newStack.slice(1); // 移除最旧的记录
    }
    return newStack;
  });
  setRedoStack([]); // 清空重做栈
};

批量操作优化

对于批量操作,drawDB使用专门的批量记录结构来减少历史记录数量:

// 批量移动操作示例
const bulkMove = (elements, newPositions) => {
  const bulkAction = {
    bulk: true,
    elements: elements.map((element, index) => ({
      type: element.type,
      id: element.id,
      undo: { x: element.x, y: element.y },
      redo: { x: newPositions[index].x, y: newPositions[index].y }
    })),
    message: t("bulk_move")
  };
  
  setUndoStack((prev) => [...prev, bulkAction]);
  setRedoStack([]);
};

用户体验设计

键盘快捷键支持

drawDB提供了标准的键盘快捷键支持,提升用户操作效率:

// 使用react-hotkeys-hook集成快捷键
useHotkeys('ctrl+z', () => undo(), { preventDefault: true });
useHotkeys('ctrl+y', () => redo(), { preventDefault: true });
useHotkeys('ctrl+shift+z', () => redo(), { preventDefault: true });

操作状态反馈

通过Toast消息和UI状态变化提供操作反馈:

const undo = () => {
  if (undoStack.length === 0) {
    Toast.info(t("nothing_to_undo"));
    return;
  }
  // ... 执行撤销逻辑
  Toast.success(t("action_undone"));
};

const redo = () => {
  if (redoStack.length === 0) {
    Toast.info(t("nothing_to_redo"));
    return;
  }
  // ... 执行重做逻辑
  Toast.success(t("action_redone"));
};

最佳实践与设计模式

命令模式应用

drawDB的撤销重做机制本质上是命令模式(Command Pattern)的实现:

mermaid

状态不可变性保证

确保状态操作的不可变性,避免直接状态修改:

// 正确的状态更新方式
setUndoStack((prev) => [
  ...prev, // 创建新数组
  newAction // 添加新操作
]);

// 错误的状态更新方式
undoStack.push(newAction); // 直接修改原数组
setUndoStack(undoStack);   // 可能导致状态更新不触发

总结与展望

drawDB的撤销重做机制通过精心设计的架构和实现,为用户提供了可靠的操作安全保障。其核心特点包括:

  1. 分层架构设计:基于React Context的状态管理,实现关注点分离
  2. 完整操作支持:支持表、字段、关系、区域、注释等多种操作类型
  3. 性能优化:历史记录限制、批量操作处理等优化策略
  4. 用户体验:键盘快捷键、操作反馈等细节设计
  5. 代码质量:遵循命令模式、保证状态不可变性等最佳实践

这种设计不仅提供了强大的功能支持,也为后续的功能扩展奠定了良好的基础。随着drawDB的不断发展,撤销重做机制将继续演进,为用户提供更加流畅和可靠的数据设计体验。

【免费下载链接】drawdb drawDB 是一款免费、简单直观的数据库模式编辑器和 SQL 生成器,能在浏览器中绘制图表、导出 SQL 脚本等,无需注册,零基础也能轻松上手。源项目地址:https://github.com/drawdb-io/drawdb 【免费下载链接】drawdb 项目地址: https://gitcode.com/GitHub_Trending/dr/drawdb

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值