drawDB撤销重做机制:状态管理与历史记录
引言:数据库设计中的操作安全保障
在数据库设计工具中,用户经常会进行各种复杂操作:添加表、修改字段、创建关系、调整布局等。一旦操作失误,如果没有可靠的撤销重做机制,用户可能需要花费大量时间重新构建整个数据库结构。drawDB作为一款专业的在线数据库设计工具,实现了完善的撤销重做功能,让用户可以放心地进行各种设计操作。
读完本文,你将深入了解:
- drawDB撤销重做机制的核心架构设计
- 状态管理的实现原理与最佳实践
- 历史记录的数据结构与管理策略
- 多类型操作的支持与处理逻辑
- 性能优化与用户体验考量
核心架构:基于React Context的状态管理
drawDB采用React Context API构建了一套完整的撤销重做状态管理系统,通过分层架构实现高效的状态追踪和历史记录管理。
状态管理架构图
核心上下文定义
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)的实现:
状态不可变性保证
确保状态操作的不可变性,避免直接状态修改:
// 正确的状态更新方式
setUndoStack((prev) => [
...prev, // 创建新数组
newAction // 添加新操作
]);
// 错误的状态更新方式
undoStack.push(newAction); // 直接修改原数组
setUndoStack(undoStack); // 可能导致状态更新不触发
总结与展望
drawDB的撤销重做机制通过精心设计的架构和实现,为用户提供了可靠的操作安全保障。其核心特点包括:
- 分层架构设计:基于React Context的状态管理,实现关注点分离
- 完整操作支持:支持表、字段、关系、区域、注释等多种操作类型
- 性能优化:历史记录限制、批量操作处理等优化策略
- 用户体验:键盘快捷键、操作反馈等细节设计
- 代码质量:遵循命令模式、保证状态不可变性等最佳实践
这种设计不仅提供了强大的功能支持,也为后续的功能扩展奠定了良好的基础。随着drawDB的不断发展,撤销重做机制将继续演进,为用户提供更加流畅和可靠的数据设计体验。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



