在Redux中实现undo / redo

时间:2015-11-16 23:59:00

标签: javascript reactjs redux undo-redo

背景

有一段时间,我一直在讨论如何使用服务器交互(通过ajax)在Redux中实现撤消/重做。

我使用command pattern提出了一个解决方案,其中操作使用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);
        }
      }
    };
  };
}

操作

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命令类型。
  • 创建一个命令来调用操作,而这些操作又会返回promises。
  • 在操作完成之前将命令添加到堆栈中。错误会发生什么?
  • 由于命令未处于状态,因此无法添加is_undoable功能。
  • 您如何实施乐观更新?

帮助

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

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

感谢任何见解。

2 个答案:

答案 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模型。一旦模型不可变,您可以在更改时轻松维护状态列表。具体来说,您需要一个撤消列表和一个重做列表。在您的示例中,它将类似于:

  1. 起始计数器值= 0 - &gt; [0],[]
  2. 添加5 - &gt; [0,5],[]
  3. 添加10 - &gt; [0,5,15],[]
  4. 撤消 - &gt; [0,5],[15]
  5. 重做 - &gt; [0,5,15],[]
  6. 第一个列表中的最后一个值是当前状态(进入组件状态)。

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

    如果您需要与服务器同步状态,您也可以这样做,只需在撤消/重做操作中发送您的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() };
    

    事实上,我相信你应该能够实现你用这种方法列出的所有目标。如果您不这么认为,您能否更具体地了解您需要做什么?