Решение промежуточного программного обеспечения Redux для запросов на гонки

React.js
Решение промежуточного программного обеспечения Redux для запросов на гонки

Эта проблема возникает из-за ошибки, которая была случайно обнаружена в проекте.Сцена заключается в том, что компонент будет повторно рендериться после переключения проектов.После каждого повторного рендеринга компонент будет запрашивать данные соответствующего текущего проекта после его монтирования , и сохраните его в магазине для отображения на странице.

Но длина сетевых запросов непредсказуема, что вызывает вопрос:

Переключитесь на проект B, запрос был выдан, но не вернулся, а затем переключитесь на проект A. Итак, теперь есть два запроса одновременно, предыдущий запрос B и новый запрос A.

Скорость запроса A относительно высока, и он выполняется немедленно, данные сохраняются в хранилище и отображаются.

Но через некоторое время возвращается запрос Б, и данные тоже сохраняются в хранилище. Но на этот раз он находится под проектом А, данные в начале А, а потом они будут заменены запросом Б. На этот раз есть проблема.

Это состояние гонки запроса, как сейчас:

before.gif

Для имитации эффекта в webpackDevServer добавлен интерфейс для управления временем отклика в соответствии с параметрами проекта

before(app, server){
  app.get('/api/request/:project', function (req, res) {
    const { project } = req.params
    if (project === 'B') {
      setTimeout(() => {
        res.send({result: 'B的结果'})
        res.status(200)
      }, 4000)
      return
    }
    setTimeout(() => {
      res.send({result: 'A的结果'})
      res.status(200)
    }, 1000)
  })
}

Решение

Только для этой проблемы есть простой способ сохранить запрошенные данные в компоненте, так как предыдущий компонент уничтожается после переключения проекта, отображается новый компонент, поэтому, естественно, проблем не будет.

Но есть одно замечание: этому методу нужно отменить запрос при уничтожении компонента, либо определить, был ли компонент уничтожен после успешного выполнения запроса, а затем установить состояние, потому что мы не можем установить его состояние в уничтоженном компонент.

более подходящий

Однако для существующих проектов данные, размещенные в хранилище, часто должны использоваться в других местах, и данные не могут храниться в компоненте.

В этом случае необходимо продумать, как его контролировать на основе как можно меньшего количества данных.

Теперь проблема в том, что один из двух запросов быстрый, а другой медленный, а данные медленного запроса перекрывают данные быстрого запроса.

Так где же точка контроля? Это когда данные сохраняются в хранилище после успешного запроса.

Как это контролировать? Обновлять ли данные успешного запроса на сохранение.

Итак, как вы судите, следует ли обновлять его в магазине? На самом деле лучше уточнить, переключение проектов приводит к двум запросам, один за другим, и запросы, отправленные позже, должны соответствовать текущим переключаемым проектам. Пока распознано, является ли текущий запрос более поздним запросом, можно судить о том, следует ли сохранять данные. О нем можно судить по времени, что требует хранения времени в хранилище для записи времени выдачи текущего запроса:reqStartLastTime

image.png

конкретные методы

Когда запрос сделан, временная метка получается как момент времени начала текущего запроса, а затем, когда запрос выполнен, этот момент времени записывается в хранилище. Таким образом, когда приходит следующий запрос, это время эквивалентно времени, когда был выдан последний запрос.

При успешном выполнении запроса последний момент времени, полученный из хранилища, сравнивается со временем отправки запроса, и только когда время отправки этого времени больше времени отправки последнего запроса, данные сохраняются в хранить.

В сочетании со сценарием переключитесь на проект B, отправьте запрос на запись момента времени, когда отправляется B, затем переключитесь на A, отправьте запрос на запись момента времени, когда будет отправлено A. В этот момент две точки времени A > B > точки времени, записанные в хранилище.

A просит сначала вернуться, судить, момент времени A> момент времени, записанный в хранилище, передать, сохранить данные, обновить момент времени A в хранилище, через некоторое время B возвращается, убедитесь, что момент времени B

export const fetchData = project => {
  return (dispatch, getState) => {
    const reqStartThisTime = Date.now()
    dispatch({ type: FETCH_BEGIN })
    fetch(`/api/request/${project}`)
      .then(res => res.json())
      .then(res => {
        // 从store中获取上一次的请求发出时间reqStartLastTime
        const { view: { reqStartLastTime } } = getState()
        // 进行比较,只有当前请求时间大于上一次的请求发出时间的时候,再存数据
        if (reqStartThisTime > reqStartLastTime) {
          dispatch({
            type: FETCH_SUCCESS,
            payload: res.result,
          })
        }
      }).finally(() => {
      const { view: { reqStartLastTime } } = getState()
       // 请求完成时,将当前更新时间为后发出请求的请求时间
        dispatch({
          type: RECORD_REQUEST_START_POINT,
          payload: reqStartThisTime > reqStartLastTime ? reqStartThisTime :reqStartLastTime
        })
    })
  }
}

Эффект следующий, ориентируясь на последовательность запросов 200:

after.gif

На данный момент проблема решена, но она все еще не идеальна, потому что есть не одно место, которое нужно лечить таким образом.

Сделайте еще один шаг вперед

Выше приведено решение для конкретного сценария. Должен ли результат запроса сохраняться в хранилище, контролируется установкой, получением и сравнением времени выдачи запроса в асинхронном действии, но таких сценариев может быть много, что приводит к повторяющейся логике. Максимально инкапсулируйте повторяющуюся логику и сосредоточьтесь на бизнесе, что приводит к концепции резервного ПО промежуточного слоя.

Цель мидлвара Redux — трансформировать диспетчеризацию, но ее не нужно преобразовывать.Вы можете поместить некоторую общую логику в мидлвар и, наконец, вызвать диспетчеризацию напрямую.

Просмотрите описанный выше процесс в асинхронном действии:

  • Когда запрос отправлен, запишите текущее время как время запроса
  • Когда запрос выполнен, сравните время отправки этого времени с последним временем отправки и запишите большее в хранилище.
  • Когда запрос выполнен успешно, сравните время отправки на этот раз с последним временем отправки.Как только первое больше второго, данные помещаются в хранилище.

Видно, что от бизнеса можно отделить только логику первых двух шагов. Поскольку действие результата записи завершения запроса полностью находится в обратном вызове успешного запроса, в настоящее время отправка является реальным действием и не может быть запущена.

Так что теперь можно дорабатывать, логика хранения времени запроса абстрагируется в middleware, а в бизнес-коде асинхронного действия из хранилища берется только текущее время запроса и время последнего запроса, и сравнивается, так как решить, сохранять ли запрос или нет результата.

Обратите внимание, что вышеприведенное только для хранения записи времени, является в последний раз запрос времени, но вот запись текущего времени на основе временного запроса абстрагировано, поэтому добавить поле в магазине, запрос, сделанный для текущее время записи

Создание нового промежуточного программного обеспечения может фактически интегрировать логику redux-thunk, а затем добавить абстрагируемую логику.

function reqTimeControl({ dispatch, getState }) {
  return next => {
    return action => {
      if (typeof action === 'function') {
        const result = action(dispatch, getState);
        if (result && 'then' in result) {
          // 请求开始时将当前时间记录为本次请求时间,放入store
          const thisTime = Date.now()
          next({
            type: '__RECORD_REQUEST_THIS_TIME__',
            payload: thisTime
          })
          const { reqStartLastTime, reqStartThisTime } = getState()
          result.finally(() => {
            // 请求完成将本次请求时间与上次请求时间进行比较,将store中的时间更新为大的
            if (reqStartThisTime > reqStartLastTime) {
              next({
                type: '__RECORD_REQUEST_START_POINT__',
                payload: reqStartThisTime
              })
            }
          })
        }
        return result
      }
      return next(action)
    }
  }
}

export default reqTimeControl

При переходе в applyMiddleware заменить redux-thunk

import { global, view, reqStartLastTime, reqStartThisTime } from './reducer'
import reqTimeControl from './middleware'

const store = createStore(
  combineReducers({ global, view, reqStartLastTime, reqStartThisTime }),
  applyMiddleware(reqTimeControl),
)
export default store

Чтобы промежуточное программное обеспечение знало статус запроса, выборка, которая возвращает обещание, должна быть возвращена в асинхронном действии. Теперь просто получите два значения времени и сравните их, чтобы решить, следует ли обновлять данные.

export const fetchData = project => {
  return (dispatch, getState) => {
    dispatch({
      type: FETCH_BEGIN,
    })
    return fetch(`/api/request/${project}`)
      .then(res => res.json())
      .then(res => {
        const { reqStartLastTime, reqStartThisTime } = getState()
        if (reqStartThisTime > reqStartLastTime) {
          dispatch({
            type: FETCH_SUCCESS,
            payload: res.result,
          })
        }
      })
  }
}

Видно, что абстрагирование логики в промежуточное ПО по-прежнему дает эффект, но заботится только о бизнес-обработке.

new.gif

избыточные изменения

redux.gif

не закончено

Текущая бизнес-логика хранилища хранилища находится в обратном вызове успешного запроса, и промежуточное ПО не может управлять здесь действием отправки. Таким образом, второй метод все еще не полностью избавляется от повторяющейся логики, ему все еще нужно взаимодействовать с промисом, который возвращает fetch в бизнесе, и ему нужно получить два раза из хранилища для сравнения.

ПО промежуточного слоя возвращается

Есть ли способ собрать всю логику обработки, связанную со временем, в одном месте, чтобы бизнес-код по-прежнему содержал только действия, а не воспринимал этот слой обработки? Конечно, вы можете, но вам нужно позволить промежуточному программному обеспечению взять на себя запрос. Из-за этого уровня обработки он проходит через весь цикл сетевого запроса.

Как позволить промежуточному программному обеспечению взять на себя запрос и как позволить промежуточному программному обеспечению выполнять эту серию операций управления? Вдумайтесь, результат асинхронного действия (то есть функцию диспетчеризации) можно получить в мидлваре, но в этом асинхронном действии запрос не будет отправлен, запрос будет передан в миддлвар для обработки, и ПО промежуточного слоя будет прослушивать возвращаемый результат. Когда есть сигнал на отправку запроса, запрос запускается.

Итак, чтобы преобразовать шаблонный код асинхронного действия, которое должно отправить запрос:

export const fetchData = project => {
  return dispatch => {
    return dispatch(() => {
      return {
       // FETCH就是告诉中间件,我要发起异步请求了,
       // 对应了三个请求的action: FETCH_BEGIN, FETCH_SUCCESS, FETCH_FAILURE,
       // 请求地址是url
        FETCH: {
          types: [ FETCH_BEGIN, FETCH_SUCCESS, FETCH_FAILURE ],
          url: `/api/request/${project}`
        },
      }
    })
  }
}

Глядя на обработку в промежуточном программном обеспечении, необходимо судить, следует ли отправлять запрос по возвращаемому результату, и модифицировать его:

function reqTimeControl({ dispatch, getState }) {
  return next => {
    return action => {
      if (typeof action === 'function') {
        let result = action(dispatch, getState);
        if (result) {
          if ('FETCH' in result) {
            const { FETCH } = result
            // dispatch请求中的action
            next({ type: FETCH.types[0] })
            // 这里将result赋值为promise是为了保证在组件中的调用函数是一个promise,
            // 这样能够在组件中根据promise的状态有更大的业务自由度,比如promise.all
            result = fetch(FETCH.url).then(res => res.json()).then(res => {
              next({
                type: FETCH.types[1],
                payload: res
              })
              return res
            }).catch(error => {
              next({
                type: FETCH.types[2],
                payload: error,
              })
              return error
            })
          }
        }
        return result
      }
      return next(action)
    }
  }
}

export default reqTimeControl

Здесь следует отметить, что для возврата выборки в качестве результата цель состоит в том, чтобы сделать функцию, которую мы вызываем в компоненте для получения данных, обещанием, чтобы мы могли более свободно управлять, например, нам нужно дождаться нескольких запросы на завершение и выполнение некоторых операций:

const ProjectPage = props => {
  const { fetchData, fetchOtherData, result, project } = props
  useEffect(() => {
    Promise.all([fetchData(), fetchOtherData()]).then(res => {
      // do something
    })
  }, [])
  return <div>
    <h1>{ result }</h1>
  </div>
}

Теперь, когда преобразование промежуточного программного обеспечения завершено, оно может управлять инициацией асинхронных запросов.В сочетании со сценариями, с которыми мы столкнулись, и приведенными выше решениями нам нужно только интегрировать реализованную выше логику в промежуточное программное обеспечение:

function reqTimeControl({ dispatch, getState }) {
  return next => {
    return action => {
      if (typeof action === 'function') {
        let result = action(dispatch, getState);
        if (result) {
          if ('FETCH' in result) {
            const { FETCH } = result
            const thisTime = Date.now()
            // dispatch请求中的action
            next({ type: FETCH.types[0] })
            result = fetch(FETCH.url).then(res => res.json()).then(res => {
              // 请求完成时根据时间判断,是否应更新数据
              const { reqStartLastTime } = getState()
              if (thisTime > reqStartLastTime) {
                next({
                  type: FETCH.types[1],
                  payload: res
                })
                return res
              }
            }).catch(error => {
              next({
                type: FETCH.types[2],
                payload: error,
              })
              return error
            }).finally(() => {
              // 请求完成时将本次的请求发出时间记录到store中
              const { reqStartLastTime } = getState()
              if (thisTime > reqStartLastTime) {
                next({
                  type: '__RECORD_REQUEST_START_POINT__',
                  payload: thisTime
                })
              }
            })
          }
        }
        return result
      }
      return next(action)
    }
  }
}

export default reqTimeControl

Стоит отметить, что поскольку оно находится в мидлваре, то достаточно напрямую получить текущее время, поэтому нет необходимости записывать время каждого запроса в хранилище, что избавляет от этого шага:

  next({
    type: '__RECORD_REQUEST_THIS_TIME__',
    payload: thisTime
  })

Эффект остался прежним, поэтому я не буду помещать сюда картинку.

К настоящему времени он должен был дать удовлетворительный эффект, абстрагируясь от повторяющейся логики, такой как время обслуживания и оценка того, хранятся ли данные в промежуточном программном обеспечении, касающемся только бизнес-кода, и может работать почти со всеми такими сценариями.

Суммировать

На этот раз повторяющаяся логика простой проблемы абстрагируется в промежуточное ПО, а избыточность используется для решения проблемы гонки запросов. При решении проблемы также реализован простой перехватчик запросов с промежуточным ПО, который может добавлять токены и обрабатывать статус загрузки. Пример в статье дает только общий эффект, на самом деле выборка в промежуточном программном обеспечении должна быть инкапсулирована для работы с большинством бизнес-сценариев, таких как различные методы запроса, тело и параметры заголовка. Кроме того, если вы хотите понять принцип работы промежуточного программного обеспечения, я подготовил для вас статью:Кратко разберем исходный код и механизм работы Redux

Если вы хотите узнать больше о моих технических статьях, вы можете обратить внимание на общедоступный номер: один интерфейс

qrcode-small.jpg