我知道这是一个观点问题,而且很长,但是我在Redux中提出一个好的解决方案时遇到了麻烦。

我正在构建一个关卡编辑器,我想向用户显示数据是否已被保存到服务器以来是否已被修改。首先,请考虑以下数据:

chapters: [{
    id: 1,
    levelIds: [ 2 ]
}],
levels: [{
     id: 2,
     entityIds: [ 4, 5 ]
}],
entities: [{
     id: 4, position:...
}, {
     id: 5, position:...
}]

一个章节具有多个级别,一个级别具有多个实体。在关卡编辑器中,将完整的章节作为一项进行编辑,因此,如果任何实体或关卡发生更改,则整个章节都将视为未保存。

我想跟踪用户自上次将数据持久保存到服务器以来是否对数据进行了任何更改。 如果某些更改,我想在章节名称旁边显示一个*例如。条件:
  • 跟踪未保存(未持久保存到服务器)状态
  • 状态必须与撤消/重做系统一起工作
  • 如果更改了某些“嵌套”数据(例如实体位置),则顶层章节必须知道其未保存,而不是实体本身

  • 我已经探索了一些选项,我将尝试说明为什么我不确定是否有任何一种解决方案比其他解决方案更好。

    选项1:在每章上存储一个“未保存”标志

    此解决方案涉及存储“未保存”标志(可能在单独的化简器中),该标志在进行任何修改时设置为true,在将章节保存到服务器时设置为false

    问题
  • 我需要跟踪许多 Action ,所以这有点冗长。我还需要手动跟踪哪些操作实际修改了本章。它可能看起来像:

  • function isUnsavedReducer( state = {}, action ):Object {
        switch( action.type ) {
            case CHANGE_ENTITY_POSITION:
            case CHANGE_ENTITY_NAME
            ...etc
            case CHANGE_LEVEL_TITLE: {
                return {
                    ...state,
                    [ action.chapterId ]: true
                };
            }
        }
    }
    
  • 大多数操作都不知道chapterId。例如,如果我移动一个实体,则 Action 看起来像{ entityId: 2, position: newPosition }。如果我走了这条路线,我想我必须将chapterId添加到所有操作中,即使它们没有修改本章?

  • 选项2:追踪最后保存的章节对象

    从表面上看,这看起来更简单。每当数据持久化时,只需存储当前的内存章对象即可:
    function lastSavedReducer( state = {}, action ):Object {
        switch( action.type ) {
            case SAVE_CHAPTER: {
                return {
                    ...state,
                    [ action.chapterId ]: action.chapter
                };
            }
        }
    }
    

    然后在 View 中检查当前数据是否未保存,这是严格的相等性检查:
    { lastSaved[ currentChapterId ] === this.props.chater ? 'Saved' : 'Unsaved' }
    

    问题:
  • 与上面的问题2相同。当我使用redux Action 修改实体位置时,我不会修改顶层章节本身。我必须修改所有的 reducer (如ChapterReducer)以返回新对象(即使实际上没有任何更改)。我还可以存储“最后一个持久实体”对象,但是由于所有实体都保存在一个商店中,因此我无法跟踪哪些章未保存,只是无法保存某些章节。


  • 有什么明显的解决方案我找不到吗?我是否应该修改数据的存储方式或 reducer 设置?我的化简器中的标准化数据以及可以设置“未保存”的许多可能操作,使我不确定前进的最佳方法。您是否实现了类似的方法,并且已经知道陷阱或最佳前进方向?

    最佳答案

    问题在于您已经标准化了数据,并且该数据充当了应用程序真相的来源。

    为什么要让尚未保存的版本事件直接修改此标准化数据?

  • 如果不从后端获取保存的状态(这不是必需的),则很难恢复到保存的状态
  • 令人困惑,因为我们不知道是否保存了标准化数据(您当前的问题)

  • 最佳解决方案:将未保存的操作保留在列表中,但是在保存之前不要运行它们

    您可以直接在商店列表中累积版本操作,而不是直接修改标准化数据。

    在渲染时,您可以使用此列表来投影Redux商店状态,因此您的商店状态不会改变,但是您仍可以将组件连接到当前版本状态。此技术与事件源和快照非常相似。

    想象一下您的存储状态如下:
    const storeState = {
        unsavedEditionActions: [a1,a2,a3],
        normalizedData: {
          chapters: [{
            id: 1,
            levelIds: [ 2 ]
          }],
          levels: [{
             id: 2,
             entityIds: [ 4, 5 ]
          }],
          entities: [{
             id: 4, position:...
          }, {
             id: 5, position:...
          }]
        }
    }
    

    如果组件需要获得版本状态和原始状态,则可以直接将其简化为mapStateToProps:
    let Component = ...
    
    Component = connect(state => {
    
      return {
        normalizedData: state.normalizedData,
        normalizedDataEdited: state.unsavedEditionActions.reduce(myNormalizedDataReducer,state.normalizedData)
      }
    
    })(Component)
    

    您的组件现在将同时接收当前保存的规范化数据和当前草稿版本数据。从那时起,您可以对2个DB的所有内容进行魔术和浅化比较,以了解已编辑的内容。

    保存时,清空列表并将其应用于规范化数据。
    取消时,您只需清空此列表(无需重新引用,因为您可以保持版本完整之前的状态)。

    我建议使用Reselect和ImmutableJS以获得更好的性能,但是如果没有它也应该可以正常工作。

    请注意,这种维护事件列表的技术也是许多人用于乐观更新的方法(即:在用户采取行动后立即提供反馈,而无需等待服务器批准,但是如果服务器执行了此操作,则可以恢复到以前的状态不批准更改)。一些有用的链接:
  • Lee Byron's Render 2016 talk (24")
  • Redux-optimistic-ui

  • 请注意,您没有被迫减少connect中的操作列表,还可以直接在商店中保存2个条目:normalizedData / normalizedDataEdited。

    其他解决方案:构建图形以从规范化数据进行编辑,然后比较initialGraph!= editedGraph

    您可以构建一个临时的非规范化图形,而不是直接在版本上修改规范化数据。因此,您可以构建该图的副本,编辑该副本,然后将该副本与原始图进行浅比较,以查看其是否已被修改。
    function levelEditionReducer( state = {}, action ) {
        switch( action.type ) {
            case LEVEL_DATA_LOADED:
              const levelData = action.payload.levelData;
              return {
                initialGraph: levelData,
                editedGraph: levelData,
              }
            case CHANGE_ENTITY_NAME
              const newEditedGraph = someFunctionToEditEntityName(state.editedGraph,action);
              return (newEditedGraph == state.editedGraph) ? state : { ...state, editedGraph: newEditionState }
            ......
        }
    }
    

    然后,在编辑后,您可以看到state.initialGraph != state.editedGraph并显示保存按钮和草稿状态。

    请注意,仅当您非常仔细地处理不可变的数据结构时,并且您确保不更新不必要的任何内容时,此浅表比较才有效。如果someFunctionToEditEntityName返回与输入相同的状态,则reducer返回完全相同的状态非常重要,因为该操作实际上没有任何改变!

    使用ES6编写此类代码并不简单,但是如果您使用其他库(例如Immutable或updeep或类似的东西),则如果尝试将对象的属性设置为已具有的值,则可能会缩短操作并导致请务必返回原始对象。

    另外请注意,您会及早发现自己缺乏护理,因为将显示“保存”按钮,而它不会显示:),因此您可能不会错过它。

    数据最终保存后,您可以将保存的图形合并到规范化数据中

    09-25 18:26
    查看更多