Redux中的排队操作

问题描述 投票:31回答:5

我目前有一种情况需要Redux Actions连续运行。我已经看过各种各样的中间件,比如redux-promise,如果你知道连续的动作是在根源(缺乏一个更好的术语)触发的动作的话,这似乎很好。

从本质上讲,我想维护一个可以随时添加的动作队列。每个对象在其状态中具有该队列的实例,并且相关的动作可以相应地排队,处理和出列。我有一个实现,但是这样做我在我的动作创建者中访问状态,这感觉就像一个反模式。

我将尝试给出一些用例和实现的背景信息。

用例

假设您要创建一些列表并将其保留在服务器上。在列表创建时,服务器使用该列表的id进行响应,该ID用于与列表相关的后续API端点:

http://my.api.com/v1.0/lists/           // POST returns some id
http://my.api.com/v1.0/lists/<id>/items // API end points include id

想象一下,客户希望对这些API点进行乐观更新,以增强用户体验 - 没有人喜欢看微调器。因此,当您创建列表时,会立即显示新列表,并在添加项目时添加选项:

+-------------+----------+
|  List Name  | Actions  |
+-------------+----------+
| My New List | Add Item |
+-------------+----------+

假设有人试图在初始创建调用的响应之前添加一个项目。 items API依赖于id,因此我们知道在拥有该数据之前我们无法调用它。但是,我们可能希望乐观地显示新项目并将对项目API的调用排入队列,以便在创建调用完成后触发它。

潜在的解决方案

我目前用来解决这个问题的方法是给每个列表一个动作队列 - 也就是说,将连续触发的Redux动作列表。

列表创建的reducer功能可能如下所示:

case ADD_LIST:
  return {
    id: undefined, // To be filled on server response
    name: action.payload.name,
    actionQueue: []
  }

然后,在动作创建者中,我们将行动排队,而不是直接触发它:

export const createListItem = (name) => {
    return (dispatch) => {
        dispatch(addList(name));  // Optimistic action
        dispatch(enqueueListAction(name, backendCreateListAction(name));
    }
}

为简洁起见,假设backendCreateListAction函数调用一个fetch API,它会在成功/失败时将消息分派到列表中。

问题

这里让我担心的是enqueueListAction方法的实现。这是我访问状态以控制队列进度的地方。它看起来像这样(忽略名称上的匹配 - 实际上这实际上使用了clientId,但我试图保持示例简单):

const enqueueListAction = (name, asyncAction) => {
    return (dispatch, getState) => {
        const state = getState();

        dispatch(enqueue(name, asyncAction));{

        const thisList = state.lists.find((l) => {
            return l.name == name;
        });

        // If there's nothing in the queue then process immediately
        if (thisList.actionQueue.length === 0) {
            asyncAction(dispatch);
        } 
    }
}

这里,假设enqueue方法返回一个普通操作,该操作将异步操作插入到列表actionQueue中。

整个事情感觉有点不利于谷物,但我不确定是否还有另一种方式可以解决它。另外,由于我需要在我的asyncActions中调度,我需要将调度方法传递给它们。

该方法中有类似的代码从列表中出列,如果存在则触发下一个操作:

const dequeueListAction = (name) => {
    return (dispatch, getState) => {
        dispatch(dequeue(name));

        const state = getState();
        const thisList = state.lists.find((l) => {
            return l.name === name;
        });

        // Process next action if exists.
        if (thisList.actionQueue.length > 0) {
            thisList.actionQueue[0].asyncAction(dispatch);
    }
}

一般来说,我可以忍受这个,但我担心它是一个反模式,并且可能有一个更简洁,惯用的方式在Redux中这样做。

任何帮助表示赞赏。

javascript reactjs redux
5个回答
2
投票

我有完美的工具,可以满足您的需求。当你需要对redux进行大量控制时,(特别是异步的东西)并且你需要按顺序发生redux动作,没有比Redux Sagas更好的工具了。它建立在es6生成器之上,为您提供了很多控制,因为从某种意义上说,您可以在某些点暂停代码。

您描述的操作队列就是所谓的传奇。现在,因为它是为了与redux一起工作而创建的,所以可以通过在组件中调度来触发这些传奇。

由于Sagas使用发电机,您还可以确定您的发货按特定顺序发生,并且仅在某些条件下发生。以下是他们的文档中的示例,我将引导您完成它以说明我的意思:

function* loginFlow() {
  while (true) {
    const {user, password} = yield take('LOGIN_REQUEST')
    const token = yield call(authorize, user, password)
    if (token) {
      yield call(Api.storeItem, {token})
      yield take('LOGOUT')
      yield call(Api.clearItem, 'token')
    }
  }
}

好吧,它起初看起来有点令人困惑,但这个传奇定义了登录序列需要发生的确切顺序。由于生成器的性质,允许无限循环。当你的代码达到收益时,它将停在该行并等待。在你告诉它之前,它不会继续下一行。所以看看它说的yield take('LOGIN_REQUEST')。该传奇将在此时产生或等待,直到你发送'LOGIN_REQUEST',之后saga将调用authorize方法,直到下一个yield。下一个方法是异步yield call(Api.storeItem, {token}),因此在该代码解析之前它不会转到下一行。

现在,这就是魔术发生的地方。该传奇将再次停留在yield take('LOGOUT'),直到您在应用程序中发送LOGOUT。这是至关重要的,因为如果您在LOGOUT之前再次发送LOGIN_REQUEST,则不会调用登录过程。现在,如果您发送LOGOUT,它将循环回第一个yield并等待应用程序再次发送LOGIN_REQUEST。

到目前为止,Redux Sagas是我最喜欢使用Redux的工具之一。它为您提供了对应用程序的大量控制,任何阅读代码的人都会感谢您,因为现在所有内容都会一次读取一行。


1
投票

看看这个:https://github.com/gaearon/redux-thunk

单独的id不应该通过reducer。在您的动作创建者(thunk)中,首先获取列表ID,然后()执行第二次调用以将项目添加到列表中。在此之后,您可以根据添加是否成功来分派不同的操作。

您可以在执行此操作时调度多个操作,以报告服务器交互何时开始和结束。这将允许您显示消息或微调器,以防操作繁重并可能需要一段时间。

可以在这里找到更深入的分析:http://redux.js.org/docs/advanced/AsyncActions.html

所有归功于丹阿布拉莫夫


0
投票

您不必处理排队操作。它将隐藏数据流,这将使您的应用程序更加繁琐的调试。

我建议您在创建列表或项目时使用一些临时ID,然后在实际从商店收到实际ID时更新这些ID。

这样的事可能吗? (不测试,但你得到的ID):

编辑:我一开始并不理解保存列表时需要自动保存的项目。我编辑了createList动作创作者。

/* REDUCERS & ACTIONS */

// this "thunk" action creator is responsible for :
//   - creating the temporary list item in the store with some 
//     generated unique id
//   - dispatching the action to tell the store that a temporary list
//     has been created (optimistic update)
//   - triggering a POST request to save the list in the database
//   - dispatching an action to tell the store the list is correctly
//     saved
//   - triggering a POST request for saving items related to the old
//     list id and triggering the correspondant receiveCreatedItem
//     action
const createList = (name) => {

  const tempList = {
    id: uniqueId(),
    name
  }

  return (dispatch, getState) => {
    dispatch(tempListCreated(tempList))
    FakeListAPI
      .post(tempList)
      .then(list => {
        dispatch(receiveCreatedList(tempList.id, list))

        // when the list is saved we can now safely
        // save the related items since the API
        // certainly need a real list ID to correctly
        // save an item
        const itemsToSave = getState().items.filter(item => item.listId === tempList.id)
        for (let tempItem of itemsToSave) {
          FakeListItemAPI
            .post(tempItem)
            .then(item => dispatch(receiveCreatedItem(tempItem.id, item)))
        }
      )
  }

}

const tempListCreated = (list) => ({
  type: 'TEMP_LIST_CREATED',
  payload: {
    list
  }
})

const receiveCreatedList = (oldId, list) => ({
  type: 'RECEIVE_CREATED_LIST',
  payload: {
    list
  },
  meta: {
    oldId
  }
})


const createItem = (name, listId) => {

  const tempItem = {
    id: uniqueId(),
    name,
    listId
  }

  return (dispatch) => {
    dispatch(tempItemCreated(tempItem))
  }

}

const tempItemCreated = (item) => ({
  type: 'TEMP_ITEM_CREATED',
  payload: {
    item
  }
})

const receiveCreatedItem = (oldId, item) => ({
  type: 'RECEIVE_CREATED_ITEM',
  payload: {
    item
  },
  meta: {
    oldId
  }
})

/* given this state shape :
state = {
  lists: {
    ids: [ 'list1ID', 'list2ID' ],
    byId: {
      'list1ID': {
        id: 'list1ID',
        name: 'list1'
      },
      'list2ID': {
        id: 'list2ID',
        name: 'list2'
      },
    }
    ...
  },
  items: {
    ids: [ 'item1ID','item2ID' ],
    byId: {
      'item1ID': {
        id: 'item1ID',
        name: 'item1',
        listID: 'list1ID'
      },
      'item2ID': {
        id: 'item2ID',
        name: 'item2',
        listID: 'list2ID'
      }
    }
  }
}
*/

// Here i'm using a immediately invoked function just 
// to isolate ids and byId variable to avoid duplicate
// declaration issue since we need them for both
// lists and items reducers
const lists = (() => {
  const ids = (ids = [], action = {}) => ({
    switch (action.type) {
      // when receiving the temporary list
      // we need to add the temporary id 
      // in the ids list
      case 'TEMP_LIST_CREATED':
        return [...ids, action.payload.list.id]

      // when receiving the real list
      // we need to remove the old temporary id
      // and add the real id instead
      case 'RECEIVE_CREATED_LIST':
        return ids
          .filter(id => id !== action.meta.oldId)
          .concat([action.payload.list.id])
      default:
        return ids
    }
  })

  const byId = (byId = {}, action = {}) => ({
    switch (action.type) {
      // same as above, when the the temp list
      // gets created we store it indexed by
      // its temp id
      case 'TEMP_LIST_CREATED':
        return {
          ...byId,
          [action.payload.list.id]: action.payload.list
        }

      // when we receive the real list we first
      // need to remove the old one before
      // adding the real list
      case 'RECEIVE_CREATED_LIST': {
        const {
          [action.meta.oldId]: oldList,
          ...otherLists
        } = byId
        return {
          ...otherLists,
          [action.payload.list.id]: action.payload.list
        }
      }

    }
  })

  return combineReducers({
    ids,
    byId
  })
})()

const items = (() => {
  const ids = (ids = [], action = {}) => ({
    switch (action.type) {
      case 'TEMP_ITEM_CREATED':
        return [...ids, action.payload.item.id]
      case 'RECEIVE_CREATED_ITEM':
        return ids
          .filter(id => id !== action.meta.oldId)
          .concat([action.payload.item.id])
      default:
        return ids
    }
  })

  const byId = (byId = {}, action = {}) => ({
    switch (action.type) {
      case 'TEMP_ITEM_CREATED':
        return {
          ...byId,
          [action.payload.item.id]: action.payload.item
        }
      case 'RECEIVE_CREATED_ITEM': {
        const {
          [action.meta.oldId]: oldList,
          ...otherItems
        } = byId
        return {
          ...otherItems,
          [action.payload.item.id]: action.payload.item
        }
      }

      // when we receive a real list
      // we need to reappropriate all
      // the items that are referring to
      // the old listId to the new one
      case 'RECEIVE_CREATED_LIST': {
        const oldListId = action.meta.oldId
        const newListId = action.payload.list.id
        const _byId = {}
        for (let id of Object.keys(byId)) {
          let item = byId[id]
          _byId[id] = {
            ...item,
            listId: item.listId === oldListId ? newListId : item.listId
          }
        }
        return _byId
      }

    }
  })

  return combineReducers({
    ids,
    byId
  })
})()

const reducer = combineReducers({
  lists,
  items
})

/* REDUCERS & ACTIONS */

0
投票

这就是我要解决这个问题的方法:

确保每个本地列表都有唯一的标识符。我不是在谈论后端ID。名称可能不足以识别列表?尚未持久化的“乐观”列表应该是唯一可识别的,并且用户可以尝试创建具有相同名称的2个列表,即使它是边缘情况。

在创建列表时,将后端标识的承诺添加到缓存

CreatedListIdPromiseCache[localListId] = createBackendList({...}).then(list => list.id);

在项目添加上,尝试从Redux商店获取后端ID。如果它不存在,那么尝试从CreatedListIdCache获取它。返回的id必须是异步的,因为CreatedListIdCache返回一个promise。

const getListIdPromise = (localListId,state) => {
  // Get id from already created list
  if ( state.lists[localListId] ) {
    return Promise.resolve(state.lists[localListId].id)
  }
  // Get id from pending list creations
  else if ( CreatedListIdPromiseCache[localListId] ) {
    return CreatedListIdPromiseCache[localListId];
  }
  // Unexpected error
  else {
    return Promise.reject(new Error("Unable to find backend list id for list with local id = " + localListId));
  }
}

addItem中使用此方法,以便您的addItem将自动延迟,直到后端ID可用

// Create item, but do not attempt creation until we are sure to get a backend id
const backendListItemPromise = getListIdPromise(localListId,reduxState).then(backendListId => {
  return createBackendListItem(backendListId, itemData);
})

// Provide user optimistic feedback even if the item is not yet added to the list
dispatch(addListItemOptimistic());
backendListItemPromise.then(
  backendListItem => dispatch(addListItemCommit()),
  error => dispatch(addListItemRollback())
);

您可能想要清理CreatedListIdPromiseCache,但对于大多数应用程序来说它可能不是很重要,除非您有非常严格的内存使用要求。


另一种选择是后端id在前端计算,类似于UUID。你的后端只需要验证这个id的唯一性。因此,即使后端尚未回复,您仍然会为所有乐观创建的列表提供有效的后端ID。


0
投票

我遇到了类似的问题。我需要一个队列来保证将乐观操作提交或最终提交(如果出现网络问题),并按照相同的顺序创建远程服务器,如果不可能则回滚。我发现仅使用Redux,因为我认为它不是为此而设计的,并且只使用promises这样做可能真的是一个难以理解的问题,除了你需要以某种方式管理你的队列状态这一事实。 .. 恕我直言。

我认为@ Pcriulan关于使用redux-saga的建议很好。乍一看,redux-saga在你到达channels之前不提供任何帮助。这为您打开了一扇门来处理其他语言所做的其他语言的并发性,特别是CSP(例如,参见Go或Clojure的async),感谢JS生成器。甚至有questions为什么以Saga模式命名而不是CSP哈哈......无论如何。

所以这里是一个传奇如何帮助你的队列:

export default function* watchRequests() {
  while (true) {
    // 1- Create a channel for request actions
    const requestChan = yield actionChannel('ASYNC_ACTION');
    let resetChannel = false;

    while (!resetChannel) {
      // 2- take from the channel
      const action = yield take(requestChan);
      // 3- Note that we're using a blocking call
      resetChannel = yield call(handleRequest, action);
    }
  }
}

function* handleRequest({ asyncAction, payload }) {
  while (true) {
    try {
      // Perform action
      yield call(asyncAction, payload);
      return false;
    } catch(e) {

      if(e instanceof ConflictError) {
        // Could be a rollback or syncing again with server?
        yield put({ type: 'ROLLBACK', payload });
        // Store is out of consistency so
        // don't let waiting actions come through
        return true;
      } else if(e instanceof ConnectionError) {
        // try again
        yield call(delay, 2000);
      }

    }
  }
}

所以这里有趣的部分是通道如何充当缓冲区(队列),它保持“监听”传入的操作,但在完成当前操作之前不会继续执行将来的操作。您可能需要查看他们的文档以便更好地掌握代码,但我认为这是值得的。重置频道部分可能或不适合您的需求:思考:

希望能帮助到你!

© www.soinside.com 2019 - 2024. All rights reserved.