Автономность компонентов с избыточным наблюдаемым

React.js Redux RxJS
Автономность компонентов с избыточным наблюдаемым

Эта статья является первой статьей в серии «Управление состоянием приложения с помощью RxJS + Redux», целью которой является представление возможностей автономии компонентов, которые версия v1, наблюдаемая с помощью redux, привносит в React + Redux.

Резюме адресов статей в этой серии:

Введение в редукционно-наблюдаемый

redux-observable— это промежуточное ПО для избыточности, которое использует RxJ для управления побочными эффектами действий. Аналогичные своему назначению, есть привычныеredux-thunkа такжеredux-saga. Интегрируя redux-observable, мы можем использовать возможности функционального реактивного программирования (FRP), предоставляемые RxJS в Redux, чтобы упростить управление нашими асинхронными побочными эффектами (при условии, что вы знакомы с RxJS).

EpicЭто основная концепция и базовый тип redux-observable, который содержит почти все redux-observable. Формально Epic — это функция, которая получаетaction stream, который выводит новый поток действий:

function (action$: Observable<Action>, state$: StateObservable<State>): Observable<Action>

Как видите, Epic выступает в роли преобразователя потоков.

С точки зрения redux-observable, Redux действует как центральный сборщик состояния.Когда действие отправляется, после синхронной или асинхронной задачи будет отправлено новое действие, перенося его полезную нагрузку на редуктор и так далее. С этой точки зрения Epic определяет причинно-следственную связь действий.

В то же время RxJS в режиме FRP также предоставляет следующие возможности:

  • Способность управлять гонкой
  • Декларативная обработка задач
  • Тест дружественный
  • Автономность компонентов(поддерживается с редукс-наблюдаемой версии 1.0)

упражняться

В этой серии статей предполагается, что читатель знаком с основами FRP и RxJS, поэтому я не буду вдаваться в подробности о RxJS и redux-observable.

Теперь давайте реализуем обычное бизнес-требование — страницу со списком. В этом примере будут показаны новые функции redux-observable 1.0, а также автономность компонентов, реализованная в версии 1.0.

Автономность компонентов: Компоненты должны сосредоточиться только на том, как управлять собой.

Сначала посмотрите привлекательность страницы со списком:

  • опросить список данных с интервалами
  • Поддержка поиска, при запуске поиска повторный опрос
  • Поддержка сортировки полей, изменение статуса сортировки, повторный опрос
  • Поддержка пейджинга, изменение емкости страницы, изменение статуса пейджинга, повторный опрос
  • Когда компонент выгружен, завершить опрос

В соответствии с идеей разработки компонентов внешнего интерфейса мы можем разработать следующие компоненты-контейнеры (Container), в которых базовые компоненты основаны наant design:

  • Таблица данных (с разбиением на страницы): на основеTableкомпоненты

  • панель поиска:: на основеInputкомпоненты

  • **Сортировка выбора:**на основе компонента **Выбор**

В архитектуре React + Redux компоненты контейнераconnectМетод выбирает нужное состояние из дерева состояний, поэтому сначала поймите, что эти компоненты-контейнеры должны иметьсвязь- Редукс:

列表页

Далее мы обсудим управление состоянием и обработку побочных эффектов приложений списка в двух разных режимах: традиционном режиме, основанном на redux-thunk или redux-saga, и режиме FRP, основанном на redux-observable. Вы можете увидеть разницу в связи между компонентами и их экологией данных (состояние и побочные эффекты) в разных режимах, в дополнение к базовой связи с Redux.

Конечно, для того, чтобы все лучше поняли статью, я также написалdemo, вы можете клонировать и запускать. Следующий код также получен из этой демонстрации. demo — это небольшое приложение на github, где видно, что список пользователей основан на модели FRP, а список репозиториев основан на традиционной модели:

Соединение компонентов в традиционном режиме

В традиционном режиме нам нужно столкнуться с реальностью, что получение состояния является **активным (упреждающим)**:

const state = store.getState()

то есть нам нужноСостояние активного доступаи не может отслеживать изменения состояния. Поэтому в этом режиме наше представление о компонентной разработке будет таким:

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

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

组件耦合

Предполагая, что мы используем redux-thunk для обработки побочных эффектов, код выглядит так:

let pollingTimer: number = null

function fetchUsers(): ThunkResult {
  return (dispatch, getState) => {
    const delay = pollingTimer === null ? 0 : 15 * 1000
    pollingTimer = setTimeout(() => {
      dispatch({
        type: FETCH_START,
        payload: {}
      })
      const { repo }: { repo: IState } = getState()
      const { pagination, sort, query } = repo
      // 封装参数
      const param: ISearchParam = {
        // ...
      }
      // 进行请求
      // fetch(param)...
  }, delay)
}}

export function polling(): ThunkResult {
  return (dispatch) => {
    dispatch(stopPolling())
    dispatch({
      type: POLLING_START,
      payload: {}
    })
    dispatch(fetchUsers())
  }
}

export function stopPolling(): IAction {
  clearTimeout(pollingTimer)
  pollingTimer = null
  return {
    type: POLLING_STOP,
    payload: {}
  }
}

export function changePagination(pagination: IPagination): ThunkResult {
  return (dispatch) => {
    dispatch({
      type: CHANGE_PAGINATION,
      payload: {
        pagination
      }
    })
    dispatch(polling())
  }
}

export function changeQuery(query: string): ThunkResult {
  return (dispatch) => {
    dispatch({
      type: CHANGE_QUERY,
      payload: {
        query
      }
    })
    dispatch(polling())
  }
}

export function changeSort(sort: string): ThunkResult {
  return (dispatch) => {
    dispatch({
      type: CHANGE_SORT,
      payload: {
        sort
      }
    })
    dispatch(polling())
  }
}

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

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

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

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

  1. процедурное программирование, код подробный
  2. управление гонкойЕго нужно контролировать вручную по количеству знаков и т.д.
  3. Связь между компонентамибольшие, вложенные друг в друга.

Режим FRP и автономность компонентов

В режиме FRP следуйтеpassiveрежим, состояние следует наблюдать и реагировать на него, а не приобретать его активно. Следовательно, redux-observable начинается с1.0Начало, устарелоstore.getState()Для получения состояния у Epic есть новая сигнатура функции, второй параметрstate$:

function (action$: Observable<Action>, state$: StateObservable<State>): Observable<Action>

С введением state$ redux-observable достиг своей вехи, и теперь мы можем продвинуть FRP на шаг вперед в Redux. Например, в следующем примере (из официального redux-observable), когдаgoogleDocumentПри изменении состояния мы автоматически сохраняем документы Google:

const autoSaveEpic = (action$, state$) =>
  action$.pipe(
    ofType(AUTO_SAVE_ENABLE),
    exhaustMap(() =>
      state$.pipe(
        pluck('googleDocument'),
        distinctUntilChanged(),
        throttleTime(500, { leading: false, trailing: true }),
        concatMap(googleDocument =>
          saveGoogleDoc(googleDocument).pipe(
            map(() => saveGoogleDocFulfilled()),
            catchError(e => of(saveGoogleDocRejected(e)))
          )
        ),
        takeUntil(action$.pipe(
          ofType(AUTO_SAVE_DISABLE)
        ))
      )
    )
  );

Оглядываясь назад, мы также можем резюмировать требования к странице со списком:

  • опросить список данных с интервалами
  • При изменении параметров (сортировка, пейджинг и т.д.) повторный запуск опроса
  • При активном поиске повторная инициация опроса
  • Окончание опроса при выгрузке компонента

В режиме FRP определяем эпик опроса:

const pollingEpic: Epic = (action$, state$) => {
  const stopPolling$ = action$.ofType(POLLING_STOP)
  const params$: Observable<ISearchParam> = state$.pipe(
    map(({user}: {user: IState}) => {
      const { pagination, sort, query } = user
      return {
        q: `${query ? query + ' ' : ''}language:javascript`,
        language: 'javascript',
        page: pagination.page,
        per_page: pagination.pageSize,
        sort,
        order: EOrder.Desc
      }
    }),
    distinctUntilChanged(isEqual)
  )

  return action$.pipe(
    ofType(LISTEN_POLLING_START, SEARCH),
    combineLatest(params$, (action, params) => params),
    switchMap((params: ISearchParam) => {
      const polling$ = merge(
        interval(15 * 1000).pipe(
          takeUntil(stopPolling$),
          startWith(null),
          switchMap(() => from(fetch(params)).pipe(
            map(({data}: ISearchResp) => ({
              type: FETCH_SUCCESS,
              payload: {
                total: data.total_count,
                list: data.items
              }
            })),
            startWith({
              type: FETCH_START,
              payload: {}
            }),
            catchError((error: AxiosError) => of({
              type: FETCH_ERROR,
              payload: {
                error: error.response.statusText
              }
            }))
          )),
          startWith({
            type: POLLING_START,
            payload: {}
          })
      ))
      return polling$
    })
  )
}

Вот несколько пояснений к этой эпопее.

  • Во-первых, мы объявляем конечный поток опроса.Когда конечный поток опроса сгенерирует значение, опрос будет завершен:
const stopPolling$ = action$.ofType(POLLING_STOP)
  • Параметры получены из состояния, и, поскольку состояние теперь можно наблюдать, мы можем передавать его из состоянияstate$отправить вниз по течению—поток параметров:
const params$: Observable<ISearchParam> = state$.pipe(
  map(({user}: {user: IState}) => {
    const { pagination, sort, query } = user
    return {
      // 构造参数
    }
  }),
  distinctUntilChanged(isEqual)
)

Мы ожидаем, что параметры потока являются актуальными параметрами, используйтеdinstinctUntilChanged(isEqual)судить о сходствах и различиях двух параметров

  • Активный поиск, либо при изменении параметров будет создан поток опроса (с помощьюcombineLatestоператор), и в конечном итоге новое действие зависит от результата извлечения данных:
return action$.pipe(
  ofType(LISTEN_POLLING_START, SEARCH),
  combineLatest(params$, (action, params) => params),
  switchMap((params: ISearchParam) => {
    const polling$ = merge(
      interval(15 * 1000).pipe(
        takeUntil(stopPolling$),
        // 自动开始轮询
        startWith(null),
        switchMap(() => from(fetch(params)).pipe(
          map(({data}: ISearchResp) => {
            // ... 处理响应
          }),
          startWith({
            type: FETCH_START,
            payload: {}
          }),
          catchError((error: AxiosError) => {
            // ...
          })
        )),
        startWith({
          type: POLLING_START,
          payload: {}
        })
      ))
    return polling$
  })
)

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

export function changePagination(pagination: IPagination): IAction {
  return {
    type: CHANGE_PAGINATION,
    payload: {
      pagination
    }
  }
}

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

Таким образом, использование FRP для обработки побочных эффектов дает:

  • декларативноОписывайте асинхронные задачи с помощью лаконичного кода
  • использоватьswitchMapобработка операторарасаЗадача
  • Минимизируйте взаимодействие компонентов, насколько это возможно, чтобы достичьАвтономность компонентов. Крупномасштабные проекты, которые способствуют совместной работе нескольких человек.

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

Доступ к избыточному наблюдению

redux-observable — это просто промежуточное ПО для redux, поэтому оно может сосуществовать с вашим текущим redux-thunk, redux-saga и т. д. Автор redux-observable может постепенно обращаться к redux-observable для обработки некоторой сложной бизнес-логики, когда вы знакомы с основами. с RxJS и шаблоном FRP вы обнаружите, что он может делать все.

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

Суммировать

В этой статье описывается, как redux-observable 1.0 обеспечиваетstate$, разделение бизнес-ассоциаций между компонентами и реализация бизнес-автономии одного компонента.

Далее я объясню вам концепцию дизайна и принцип реализации redux-observable, шаг за шагом реализуя промежуточное программное обеспечение, подобное redux-observable.

использованная литература


Об этой серии

  • Эта серия статей начнется с введения в redux-observable 1.0 и расскажет о моем опыте объединения RxJS с Redux. Содержимое будет представлять собой введение в практику redux-observable, исследование принципа реализации redux-observable, и, наконец, я представлю свою текущую структуру управления состоянием reobservable, основанную на архитектуре redux-observble + dva.

  • Эта серия не является введением в RxJS или Redux, не охватывает их основные концепции и не продвигает их основные сильные стороны. Если вы искали RxJS и наткнулись на эту серию и заинтересовались программированием RxJS и FRP, я бы порекомендовал начать:

  • Эта серия не является учебным пособием. Она просто знакомит с некоторыми идеями по применению RxJS в Redux. Я надеюсь, что больше людей смогут указать на недоразумения или обменяться более элегантными практиками.

  • Я искренне благодарю некоторых старших братьев за их помощь на пути практики, особенно за руководство старшим квестгуо Tencent Cloud по модели. reobservable рождается из среды React, которую возглавляет Tencent Cloud questguo — TCFF, с нетерпением ожидая открытого исходного кода TCFF в будущем.

  • Спасибо Xiaoyu за ее поддержку дизайна.