在Redux中实现undo/redo

Ash*_*son 15 javascript undo-redo reactjs redux

背景

有一段时间,我一直在讨论如何在Redux中使用服务器交互(通过ajax)实现undo/redo .

我已经提出了一个使用命令模式的解决方案,其中操作使用executeundo方法作为命令注册,而不是调度操作您调度命令.然后将命令存储在堆栈中并在需要时引发新操作.

我当前的实现使用中间件拦截调度,测试命令和调用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);
        }
      }
    };
  };
}
Run Code Online (Sandbox Code Playgroud)

操作

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));
        });
    }
}
Run Code Online (Sandbox Code Playgroud)

命令

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

应用

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
Run Code Online (Sandbox Code Playgroud)

(这里有一个更完整的例子)

我目前采用的方法有几个问题:

  • 由于通过中间件实现,整个应用程序可能只存在一个堆栈.
  • 无法自定义UNDO命令类型.
  • 创建一个命令来调用操作,而这些操作又会返回promises,这看起来非常复杂.
  • 在操作完成之前,命令将添加到堆栈中.错误会发生什么?
  • 由于命令未处于状态,因此无法添加is_undoable功能.
  • 您将如何实施乐观更新?

救命

那么我的问题是,是否有人可以建议在Redux中实现此功能的更好方法?

我现在看到的最大缺陷是在动作完成之前添加的命令,以及如何在混合中添加乐观更新.

任何见解都表示赞赏.

Vla*_*sky 0

不确定我完全理解你的用例,但在我看来,在 ReactJS 中实现撤消/重做的最佳方法是通过不可变模型。一旦你的模型是不可变的,你就可以在状态发生变化时轻松维护状态列表。具体来说,您需要一个撤消列表和一个重做列表。在你的例子中,它会是这样的:

  1. 起始计数器值 = 0 -> [0], []
  2. 加 5 -> [0, 5], []
  3. 加 10 -> [0, 5, 15], []
  4. 撤消 -> [0, 5], [15]
  5. 重做 -> [0, 5, 15], []

第一个列表中的最后一个值是当前状态(进入组件状态)。

这是比命令更简单的方法,因为您不需要为要执行的每个操作单独定义撤消/重做逻辑。

如果您需要与服务器同步状态,您也可以这样做,只需将 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() };
Run Code Online (Sandbox Code Playgroud)

事实上,我相信您应该能够通过这种方法实现您列出的所有目标。如果您有不同的感觉,您能否更具体地说明您需要做什么?