有一段时间,我一直在讨论如何使用服务器交互(通过ajax)在Redux中实现撤消/重做。
我使用command pattern提出了一个解决方案,其中操作使用execute
和undo
方法作为命令进行注册,而不是分派操作来调度命令。然后将命令存储在堆栈中并在需要时引发新操作。
我当前的实现使用中间件拦截调度,测试命令和调用Command的方法,看起来像这样:
let commands = [];
function undoMiddleware({ dispatch, getState }) {
return function (next) {
return function (action) {
if (action instanceof Command) {
// Execute the command
const promise = action.execute(action.value);
commands.push(action);
return promise(dispatch, getState);
} else {
if (action.type === UNDO) {
// Call the previous commands undo method
const command = commands.pop();
const promise = command.undo(command.value);
return promise(dispatch, getState);
} else {
return next(action);
}
}
};
};
}
const UNDO = 'UNDO';
function undo() {
return {
type: UNDO
}
}
function add(value) {
return (dispatch, getState) => {
const { counter } = getState();
const newValue = counter + value;
return new Promise((resolve, reject) => {
resolve(newValue); // Ajax call goes here
}).then((data) => {
dispatch(receiveUpdate(data));
});
}
}
function sub(value) {
return (dispatch, getState) => {
const { counter } = getState();
const newValue = counter - value;
return new Promise((resolve, reject) => {
resolve(newValue); // Ajax call goes here
}).then((data) => {
dispatch(receiveUpdate(data));
});
}
}
class Command {
execute() {
throw new Error('Not Implemented');
}
undo() {
throw new Error('Not Implemented');
}
}
class AddCommand extends Command {
constructor(value) {
super();
this.value = value;
}
execute() {
return add(this.value);
}
undo() {
return sub(this.value);
}
}
const store = createStoreWithMiddleware(appReducer);
store.dispatch(new AddCommand(10)); // counter = 10
store.dispatch(new AddCommand(5)); // counter = 15
// Some time later
store.dispatch(undo()); // counter = 10
(更完整的示例here)
我目前采用的方法有几个问题:
UNDO
命令类型。我的问题是,是否有人可以建议在Redux中实现此功能的更好方法?
我现在看到的最大缺陷是在行动完成之前添加的命令,以及如何在混合中添加乐观更新。
感谢任何见解。
答案 0 :(得分:3)
进一步讨论@ vladimir-rovensky提出的基于不可变的实现......
Immutable适用于客户端undo-redo管理。你可以简单地存储最后的" N"不可变状态的实例要么是你自己,要么使用immstruct之类的库来为你做这件事。由于内置于不可变的实例共享,它不会导致内存开销。
但是,如果您希望保持简单,则每次将模型与服务器同步可能会很昂贵,因为每次在客户端上修改时,您都需要将整个状态发送到服务器。根据州的规模,这将无法很好地扩展。
更好的方法是仅将修改发送到服务器。你需要一个"版本"您最初将其发送到客户端时的状态标头。对客户端完成的状态的每次其他修改都应该只记录差异并将它们发送到带有修订版的服务器。服务器可以执行diff操作并发送新版本和差异后的状态校验和。客户端可以根据当前状态校验和对此进行验证并存储新版本。差异也可以由标记有修订和校验和的服务器存储在其自己的撤消历史记录中。如果在服务器上需要撤消,则可以反转差异以获得状态,并且可以执行校验和检查。 我遇到的不可变的差异库是https://github.com/intelie/immutable-js-diff。它创建了RFC-6902样式的补丁,您可以在服务器状态下使用http://hackersome.com/p/zaim/immpatch执行它。
<强>优点 - 强>
答案 1 :(得分:0)
我不确定我是否完全理解您的用例,但在我看来,在ReactJS中实现undo / redo的最佳方法是通过immutable模型。一旦模型不可变,您可以在更改时轻松维护状态列表。具体来说,您需要一个撤消列表和一个重做列表。在您的示例中,它将类似于:
第一个列表中的最后一个值是当前状态(进入组件状态)。
这是一个比命令更简单的方法,因为您不需要为要执行的每个操作单独定义撤消/重做逻辑。
如果您需要与服务器同步状态,您也可以这样做,只需在撤消/重做操作中发送您的AJAX请求。
也应该可以进行乐观更新,您可以立即更新您的状态,然后发送您的请求并在其错误处理程序中恢复到更改之前的状态。类似的东西:
var newState = ...;
var previousState = undoList[undoList.length - 1]
undoList.push(newState);
post('server.com', buildServerRequestFrom(newState), onSuccess, err => { while(undoList[undoList.length-1] !== previousState) undoList.pop() };
事实上,我相信你应该能够实现你用这种方法列出的所有目标。如果您不这么认为,您能否更具体地了解您需要做什么?