Эта статья является первой статьей в серии «Управление состоянием приложения с помощью 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())
}
}
Видно, что он включаетпараметры запросаНесколько компонентов, таких как фильтрация элементов, пейджинг, поиск и т. д., когда они отправляют действие для изменения соответствующего бизнес-состояния,Вам также необходимо вручную отправить действие, которое перезапускает опрос, чтобы завершить предыдущий опрос и начать следующий опрос..
Возможно, сложность этого сценария для вас приемлема, но предположим, что у нас есть более крупный проект, или текущий проект сильно расширится в будущем, тогда будет все больше и больше компонентов, и разработчиков, участвующих в совместной работе, тоже будет все больше и больше. Совместным разработчикам необходимо всегда обращать внимание на то, будут ли компоненты, написанные ими самими, факторами влияния компонентов, написанных другими разработчиками, и если да, то насколько велико влияние и что следует делать?
Упомянутые здесь компоненты относятся не только к компоненту пользовательского интерфейса, но также включают в себя экологию данных, задействованную в компоненте. Потому что, когда большинство фронтенд-разработчиков пишут бизнес-компоненты, в дополнение к пользовательскому интерфейсу им также необходимо реализовать бизнес-логику, связанную с пользовательским интерфейсом.
Мы суммируем проблемы, с которыми сталкиваются при использовании традиционных шаблонов для сортировки потока данных и побочных эффектов:
- процедурное программирование, код подробный
- управление гонкойЕго нужно контролировать вручную по количеству знаков и т.д.
- Связь между компонентамибольшие, вложенные друг в друга.
Режим 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, я бы порекомендовал начать:
- learnrxjs.io
- Андре Штальц вegghead.ioсерия курсов
- Ченг Мо"Углубленный RxJS"
-
Эта серия не является учебным пособием. Она просто знакомит с некоторыми идеями по применению RxJS в Redux. Я надеюсь, что больше людей смогут указать на недоразумения или обменяться более элегантными практиками.
-
Я искренне благодарю некоторых старших братьев за их помощь на пути практики, особенно за руководство старшим квестгуо Tencent Cloud по модели. reobservable рождается из среды React, которую возглавляет Tencent Cloud questguo — TCFF, с нетерпением ожидая открытого исходного кода TCFF в будущем.
-
Спасибо Xiaoyu за ее поддержку дизайна.