@potmot/command-history
v0.0.1
Published
Lightweight history by command.
Readme
@potmot/command-history
轻量级的命令历史记录库,支持撤销/重做、批量操作和事件监听功能。
特性
- 📦 批量操作 - 支持批量执行命令与批量 undo/redo
- 🎯 类型安全 - 完整的 TypeScript 类型支持
- 🔔 事件系统 - 丰富的事件监听能力
安装
npm install @potmot/command-historyyarn add @potmot/command-historypnpm add @potmot/command-history快速开始
前置目标
存在一个待办事项列表,需要实现以下功能:
- 添加待办事项
- 删除待办事项
- 修改待办事项
// 定义待办事项数据类型
type TodoItem = {
id: number;
text: string;
};
type TodoItemInput = {
id?: number;
text: string;
index?: number;
};
type TodoItemWithIndex = {
item: TodoItem;
index: number;
};
// 定义代办事项数据
const todoList: TodoItem[] = [];
let nextId = 1;
// 定义待办事项操作函数
const addTodoItem = (
text: string,
id: number = nextId++,
index: number = todoList.length,
): TodoItem => {
const newTodo: TodoItem = {id, text};
todoList.splice(index, 0, newTodo);
return newTodo;
};
const removeTodoItem = (id: number): TodoItemWithIndex => {
for (let i = 0; i < todoList.length; i++) {
if (todoList[i].id === id) {
const todoItem = todoList[i];
todoList.splice(i, 1);
return {item: todoItem, index: i};
}
}
throw new Error('Todo item not found');
};
const updateTodoItem = (todoItem: TodoItem): TodoItem => {
const index = todoList.findIndex((item) => item.id === todoItem.id);
if (index === -1) {
throw new Error('Todo item not found');
}
const oldTodoItem = todoList[index];
if (!oldTodoItem) {
throw new Error('Todo item not found');
}
todoList[index] = todoItem;
return oldTodoItem;
};1. 定义命令类型
import { useCommandHistory, type CommandDefinition } from '@potmot/command-history';
// 定义你的命令映射类型
// CommandDefinition<执行操作参数,撤销操作参数>
type TodoListCommandMap = {
add: CommandDefinition<TodoItemInput, {id: number}>;
remove: CommandDefinition<{id: number}, TodoItemWithIndex>;
update: CommandDefinition<TodoItem, TodoItem>;
};2. 创建历史管理器
const history = useCommandHistory<TodoListCommandMap>();3. 注册命令
注意:
applyAction的返回值会作为revertAction的参数revertAction的返回值会作为下次执行时applyAction的参数
history.registerCommand('add', {
applyAction: ({text, id, index}) => {
const item = addTodoItem(text, id, index);
return {id: item.id};
},
revertAction: ({id}) => {
const {item, index} = removeTodoItem(id);
return {text: item.text, id: item.id, index};
},
});
history.registerCommand('remove', {
applyAction: ({id}) => {
return removeTodoItem(id);
},
revertAction: ({item, index}) => {
const newItem = addTodoItem(item.text, item.id, index);
return {id: newItem.id};
},
});
history.registerCommand('update', {
applyAction: (newItem) => {
const oldItem = updateTodoItem(newItem);
return {id: oldItem.id, text: oldItem.text};
},
revertAction: (oldItem) => {
const newItem = updateTodoItem(oldItem);
return {id: newItem.id, text: newItem.text};
},
});4. 执行命令
// 执行单个命令
history.executeCommand('add', { text: '学习 TypeScript' });
// 撤销
history.undo();
// 重做
history.redo();错误处理
库提供了完善的错误处理机制,可以通过 onError 回调来自定义错误处理逻辑。
配置错误处理器
import { useCommandHistory, type ErrorHandler, type ErrorContext } from '@potmot/command-history';
// 自定义错误处理函数
const errorHandler: ErrorHandler<TodoListCommandMap> = (error, context) => {
console.error('发生错误:', error);
console.error('错误上下文:', context);
// 根据错误类型进行不同处理
switch (context.type) {
case 'execute':
console.error(`执行命令 ${String(context.key)} 时出错,操作类型:${context.operation}`);
break;
case 'register':
console.error(`注册命令 ${String(context.key)} 时出错`);
break;
case 'unregister':
console.error(`注销命令 ${String(context.key)} 时出错`);
break;
case 'batch':
console.error(`批量操作出错,操作类型:${context.operation}`);
break;
case 'undo':
console.error('撤销操作出错');
break;
case 'redo':
console.error('重做操作出错');
break;
case 'clean':
console.error('清空历史记录时出错');
break;
}
// 返回 stillThrow: false 可以阻止错误继续抛出
// 返回 undefined 或 stillThrow: true 会让错误继续抛出
return { stillThrow: false };
};
const history = useCommandHistory<TodoListCommandMap>({
onError: errorHandler
});错误上下文类型
ErrorContext 提供了详细的错误发生位置信息:
type ErrorContext<CommandMap> =
| {type: 'execute', key: Key, operation: 'apply' | 'revert'} // 执行命令时
| {type: 'register', key: Key} // 注册命令时
| {type: 'unregister', key: Key} // 注销命令时
| {type: 'batch', operation: 'start' | 'stop' | 'execute'} // 批量操作时
| {type: 'undo' | 'redo'} // 撤销/重做时
| {type: 'clean'}; // 清空时错误处理示例
// 示例 1: 记录错误但不中断程序
history.registerCommand('add', {
applyAction: ({text, id, index}) => {
// 假设这里发生了错误
throw new Error('添加失败');
},
revertAction: ({id}) => {
// ...
},
});
// 如果配置了 onError 并返回 { stillThrow: false }
// 错误会被捕获但不会中断程序执行
// 示例 2: 根据错误类型决定是否抛出
const selectiveErrorHandler: ErrorHandler<TodoListCommandMap> = (error, context) => {
// 对于 undo/redo 错误,不抛出(用户无感知)
if (context.type === 'undo' || context.type === 'redo') {
console.warn('撤销/重做失败:', error);
return { stillThrow: false };
}
// 其他错误继续抛出
return { stillThrow: true };
};错误常量
所有可能的错误消息定义在 ERROR_MESSAGES 对象中,可直接导入使用:
import { ERROR_MESSAGES } from '@potmot/command-history';| 常量 | 值 | 触发场景 |
|------|-----|---------|
| COMMAND_ALREADY_REGISTERED(key) | command ${key} is already registered | 重复注册同名命令 |
| COMMAND_NOT_REGISTERED(key) | command ${key} is not registered | 执行或注销未注册的命令 |
| EXECUTION_NESTING | Execution does not allow nesting | 在 applyAction/revertAction 中嵌套调用 executeCommand、undo 或 redo |
| CLEAN_DURING_BATCH | Cannot clean during batch | 在 startBatch 和 stopBatch 之间调用 clean |
| UNDO_DURING_BATCH | Cannot undo during batch | 在 startBatch 和 stopBatch 之间调用 undo |
| REDO_DURING_BATCH | Cannot redo during batch | 在 startBatch 和 stopBatch 之间调用 redo |
| STOP_BATCH_KEY_MISMATCH | stopBatch key is not match | stopBatch 的键与当前批次的键不匹配 |
使用示例:
import { ERROR_MESSAGES } from '@potmot/command-history';
try {
history.undo();
} catch (error) {
if (error instanceof Error && error.message === ERROR_MESSAGES.UNDO_DURING_BATCH) {
console.log('当前在批量操作中,无法撤销');
}
}API 文档
CommandMap
命令映射类型,约束命令名和对应的执行和撤销操作参数类型。
type CommandDefinition<ApplyOptions, RevertOptions = ApplyOptions> = {
applyAction: (options: ApplyOptions) => RevertOptions;
revertAction: (options: RevertOptions) => ApplyOptions | undefined | void;
};
type CustomCommandMap = Record<string, CommandDefinition<any>>;注意事项:
CommandMap必须继承自CustomCommandMap- 每个命令都需要定义
applyAction和revertAction的类型 applyAction的返回值类型应该与revertAction的参数类型匹配revertAction可以返回undefined或ApplyOptions类型,用于重做时再次执行的参数
核心方法
useCommandHistory(options?)
创建命令历史管理器的工厂函数。
import { useCommandHistory, type CommandHistoryOptions } from '@potmot/command-history';
const history = useCommandHistory<MyCommandMap>({
onError: (error, context) => {
console.error('错误:', error, context);
return { stillThrow: false }; // 阻止错误抛出
}
});参数说明:
options: 可选配置对象onError: 错误处理函数,类型为ErrorHandler<CommandMap>
返回值:
CommandHistory<CommandMap>: 命令历史管理器实例
registerCommand(key, options)
注册一个命令。
history.registerCommand('commandName', {
applyAction: (options) => {
// 执行操作
// options 类型为 CommandDefinition 的第一个泛型参数
return revertOptions; // 返回撤销时需要的参数
},
revertAction: (revertOptions) => {
// 撤销操作
// revertOptions 类型为 CommandDefinition 的第二个泛型参数
return options; // 可选,返回执行时需要的参数
}
});示例:
// 完整类型标注示例
history.registerCommand('add', {
applyAction: ({text, id, index}): {id: number} => {
const item = addTodoItem(text, id, index);
return {id: item.id};
},
revertAction: ({id}): {text: string; id: number; index?: number} | void => {
const {item, index} = removeTodoItem(id);
return {text: item.text, id: item.id, index};
},
});unregisterCommand(key)
注销一个已注册的命令。
相关事件:
unregisterCommand: 命令注销时触发
示例:
// 注销命令
const removedCommand = history.unregisterCommand('add');
console.log('已移除的命令:', removedCommand);
// 监听命令注销
history.eventBus.on('unregisterCommand', ({key}) => {
console.log(`命令 ${String(key)} 已注销`);
});executeCommand(key, options)
执行一个已注册的命令,并自动记录到历史栈。
const revertOptions = history.executeCommand('commandName', { /* 参数 */ });
// revertOptions 是 applyAction 的返回值,用于撤销操作参数说明:
key: 要执行的命令的标识符options: 执行命令时传递给applyAction的参数
返回值:
ReturnType<CommandMap[Key]['applyAction']> | undefined:applyAction的返回值,用于撤销操作
类型定义:
// executeCommand 的返回值类型是 applyAction 的返回值
const revertOptions: {id: number} = history.executeCommand('add', {
text: '学习 TypeScript',
id: 1,
index: 0
});示例:
// 执行命令并获取撤销参数
const revertOptions = history.executeCommand('add', {
text: '学习 TypeScript'
});
console.log(revertOptions); // { id: 1 }pushCommand(key, options, revertOptions)
手动推送一个命令到历史栈(不执行 applyAction),适用于已经执行过的操作。
// 假设你已经手动执行了某个操作
const revertOptions = { /* 撤销需要的参数 */ };
history.pushCommand('commandName', options, revertOptions);参数说明:
key: 命令的标识符options: 执行命令时的参数(用于记录历史)revertOptions: 撤销操作需要的参数
使用场景: 当你已经手动执行了某个操作,只想将其记录到历史栈时使用。
示例:
// 假设你已经手动执行了添加操作
const newItem = addTodoItem('手动添加的任务');
// 将操作记录到历史栈
history.pushCommand('add',
{ text: '手动添加的任务', id: newItem.id },
{ id: newItem.id } // 撤销时需要的参数
);undo()
撤销上一个操作。
history.undo();相关事件:
beforeUndo: 撤销前触发undo: 撤销后触发
示例:
// 检查是否可以撤销
if (history.canUndo()) {
history.undo();
}
// 监听撤销事件
history.eventBus.on('undo', (commandData) => {
console.log('已撤销:', commandData);
});redo()
重做刚才撤销的操作。
history.redo();相关事件:
beforeRedo: 重做前触发redo: 重做后触发
示例:
// 检查是否可以重做
if (history.canRedo()) {
history.redo();
}
// 监听重做事件
history.eventBus.on('redo', (commandData) => {
console.log('已重做:', commandData);
});canUndo(): boolean
检查是否可以撤销(当 undoStack 不为空且当前不在执行操作中时返回 true)。
if (history.canUndo()) { /* ... */ }示例:
if (history.canUndo()) {
console.log('可以撤销');
history.undo();
}canRedo(): boolean
检查是否可以重做(当 redoStack 不为空且当前不在执行操作中时返回 true)。
if (history.canRedo()) { /* ... */ }示例:
if (history.canRedo()) {
console.log('可以重做');
history.redo();
}clear()
清空所有历史记录栈(包括 undoStack、redoStack 和 batchStack)。
相关事件:
beforeClean: 清空前触发clean: 清空后触发
示例:
// 清空前检查
if (history.getUndoStack().length > 0) {
history.eventBus.on('beforeClean', () => {
console.log('即将清空历史记录');
});
history.clean();
}
// 监听清空事件
history.eventBus.on('clean', () => {
console.log('历史记录已清空');
});批量操作
startBatch(key)
开始一个批量操作。
const batchKey = Symbol('batch');
history.startBatch(batchKey);stopBatch(key)
停止批量操作。
history.stopBatch(batchKey);executeBatch(key, action)
执行批量操作(同步)。
const batchKey = Symbol('batch');
history.executeBatch(batchKey, () => {
history.executeCommand('command1', options1);
history.executeCommand('command2', options2);
// 这些命令会被作为一个整体记录
});executeAsyncBatch(key, action)
执行异步批量操作。
const batchKey = Symbol('batch');
await history.executeAsyncBatch(batchKey, async () => {
await fetchData();
history.executeCommand('command1', options1);
history.executeCommand('command2', options2);
});查询方法
getCommandMap()
获取已注册的命令映射。
getUndoStack()
获取撤销栈。
getRedoStack()
获取重做栈。
getBatchStack()
获取批量操作键栈。
事件系统
通过 eventBus 可以监听各种历史变化事件:
// 监听命令变更(apply: 执行,revert: 撤销,push: 手动推送)
history.eventBus.on('change', (data) => {
console.log('命令变更:', data.type, data.key, data);
});
// 监听撤销
history.eventBus.on('undo', (data) => {
console.log('撤销操作:', data);
});
// 监听重做
history.eventBus.on('redo', (data) => {
console.log('重做操作:', data);
});
// 监听批量操作开始/结束
history.eventBus.on('batchStart', () => { /* ... */ });
history.eventBus.on('batchStop', () => { /* ... */ });
// 其他可用事件:
// - beforeChange: 命令执行前(包含 type: 'apply' | 'revert' | 'push')
// - beforeUndo: 撤销前
// - beforeRedo: 重做前
// - beforeClean: 清空前
// - registerCommand: 命令注册时
// - unregisterCommand: 命令注销时嵌套批量操作
const outerBatch = Symbol('outer');
const innerBatch = Symbol('inner');
history.executeBatch(outerBatch, () => {
history.executeCommand('command1', options1);
// 嵌套批量操作
history.executeBatch(innerBatch, () => {
history.executeCommand('command2', options2);
});
history.executeCommand('command3', options3);
});
// 撤销时会作为一个包含 3 个命令的整体批次处理
history.undo();注意事项
禁止嵌套:在命令的
applyAction或revertAction中不能再次调用executeCommand、undo或redo,否则会抛出错误。批次键匹配:
startBatch和stopBatch必须使用相同的键,且支持嵌套但必须正确配对。
License
MIT
