Эта статья — третья статья из серии «Управление состоянием приложения с помощью RxJS + Redux». Вернуться к первой статье:Автономность компонентов с избыточным наблюдаемым
Резюме адресов статей в этой серии:
Зачем нам нужен Redux?
Прежде всего, следует понимать, что Redux — это не плагин, уникальный для React, это модель управления состоянием, рожденная в соответствии с тенденцией разработки фронтенд-компонентов, которую вы также можете использовать в Vue или Angular.
В настоящее время все согласны с тем, что состояние приложения или компонента в определенный момент будет соответствовать пользовательскому интерфейсу приложения или компонента в это время:
UI = f(state)
Затем при разработке компонентов интерфейса вам нужно подумать о двух проблемах:
- Источник статуса
- государственное управление
Состояние компонента исходит из двух аспектов:
- собственное государство: Например, сам компонент Button имеет счетчик состояний count, который представляет количество нажатий на него.
- Состояние внешнего впрыска: например, модальному компоненту необходимо внедрить видимое состояние. React относится к внешнему введенному состоянию какprops.
Источник состояния передает требуемое состояние компоненту, и, в свою очередь, подтверждается внешний вид компонента. В простых проектах и простых компонентах нам нужно думать только об источнике состояния.Если мы введем дополнительные решения по управлению состоянием (например, мы используем Redux для управления состоянием компонента кнопки), это увеличит нагрузку на каждый компонент, что приводит к излишним абстракциям и зависимостям.
И дляБольшой фронт-энд инжинирингДля сложных компонентов они часто имеют следующие характеристики:
- сложные данные
- Богатые компоненты
В этом сценарии простое управление состоянием растягивается, что в основном отражается в следующих аспектах:
- Когда компонентыслишком глубокий уровеньКогда, как элегантно представить состояние, требуемое компонентом, или как компоненту проще получить нужное ему состояние
- какотступлениев состояние
- как стать лучшетестовое заданиегосударственное управление
Redux собирается решить эти проблемы, чтобы сделать состояние крупных интерфейсных проектов более контролируемым. Redux предлагает условную модель, которая централизует обновления состояния и отправки:
Модель, используемая Redux, вдохновлена Elm:
В Elm то, что проходит через приложение,сообщение (msg): A by ** тип сообщения (тип)идентифицированы и перенесеныСтруктура данных контента (полезной нагрузки)**. Сообщение определяет модель данных (model), как обновлять, и данные определяют форму пользовательского интерфейса.
В то время как в Redux сообщения вызываются какдействиеи использоватьreducerдля описания изменения состояния с поведением. Кроме того, в отличие от Elm, Redux фокусируется на управлении состоянием и больше не имеет дело с представлениями (View), поэтому Redux не является типизированным (для ознакомления с типизированной архитектурой см.Сообщение блога).
Узнав о преимуществах Redux или будучи привлеченными популярностью Redux, мы представили Redux в качестве менеджера состояния приложения, что сделало изменения состояния всего приложения очень четкими, состояние, скачкообразное по ссылке, мы даже можем вернуться или перейти к определенному состоянию. Однако действительно ли Redux безупречен?
Несовершенный редукс
Конечно, Redux не идеален, нас больше всего беспокоят следующие два аспекта:
- подробный шаблонный код
- Низкая возможность асинхронной обработки задач
Предполагая, что внешнему интерфейсу необходимо получить некоторые данные с сервера и отобразить их в режиме Redux, чтобы завершить процесс от получения данных до обновления статуса, вам необходимо пройти:
(1) Определите количествоaction type:
const FETCH_START = 'FETCH_START'
const FETCH_SUCCESS = 'FETCH_SUCCESSE'
const FETCH_ERROR = 'FETCH_ERROR'
(2) Определите некоторыеaction creator, предполагая, что мы используем redux-thunk для управления асинхронными задачами:
const fetchSuccess = data => ({
type: FETCH_START,
payload: { data }
})
const fetchError = error => ({
type: FETCH_ERROR,
payload: { error }
})
const fetchData = (params) => {
return (dispatch, getState) => {
return api.fetch(params)
.then(fetchSuccess)
.catch(fetchError)
}
}
(3) вreducer, для разных типов действий объявите разные методы обновления состояния через switch-case:
function reducer(state = initialState, action) {
const { type, payload } = action
switch(action.type){
case FETCH_START: {
return { ...state, loading: true }
}
case FETCH_SUCCESS: {
return { ...state, loading: false, data: payload.data }
}
case FETCH_ERROR: {
return { ...state, loading: false, data: null, error: payload.error}
}
}
}
Проблемы с этим процессом:
- Недостаточно внимания к личному развитию: В машиностроении мыДецентрализованное управлениеДля типа действия, действия и редуктора, после прохождения набора процессов нужно продолжать прыгать в процессе, а мышление недостаточно сконцентрировано.
- Сотрудничество нескольких человек недостаточно эффективно: Также из-за рассредоточенности типов действий, действий и редюсеров при совместной работе нескольких людей будут возникать такие проблемы, как конфликты имен и дублирование схожих бизнес-процессов. Это выдвигает относительно высокие требования к дизайну состояния нашего приложения. В хорошем дизайне легко найти состояние, процесс перехода ясен и нет избыточного состояния, в то время как в плохом дизайне будет сложно найти расширение состояния, процесс перехода сложен, и можно увидеть избыточное состояние. где угодно.
Как правильно использовать Redux
Когда мы страдаем от негативных последствий Redux, переключаемся на другие решения для управления состоянием (такие какmobxилиmobx-дерево состояний), и это не реально.С одной стороны, стоимость миграции высока, а с другой стороны, вы не знаете, является ли новое решение по управлению состоянием панацеей. Однако безразличие или поглощение негативных эффектов Redux только позволит проблеме выйти из-под контроля.
Прежде чем мы начнем говорить о том, как сделать Redux лучше, нам нужно прояснить, что отсутствие шаблонного кода и асинхронных возможностей,Это результат собственного дизайна Redux, а не цель, другими словами, Redux не был разработан, чтобы позволить разработчикам писать примеры кода или бороться с тем, как обрабатывать асинхронные обновления состояния.
Нам нужно определить другую роль, пусть он пишет шаблонный код вместо нас, пусть он дает нам лучшие возможности обработки асинхронных задач, и пусть он отвечает за все гадости в Redux. Таким образом, эта роль является фреймворком, который делает Redux более элегантным.Что касается того, как создать эту роль, нам нужно начать с одного компонента, реорганизовать форму приложения и сосредоточиться на:
- Как победить шаблонный код в Redux
- Как более изящно обрабатывать асинхронные задачи
как выглядит компонент
Экология компонента, вероятно, такова:
который:Данные обрабатываются для формирования состояния страницы, а состояние страницы определяет отрисовку пользовательского интерфейса..
как выглядит приложение
Сочетание компонентной экологии (пользовательский интерфейс + состояние + метод управления состоянием) составляет наше приложение:
Экология компонента здесь преднамеренно показывает толькоданные для утвержденияЭтот шаг, потому что Redux обрабатывает эту часть. Мы можем временно определить процесс данных для состояния какflow, что означает бизнес-поток.
Отдел приложений
Заимствуя у Elm, мы можем разделить приложения по модели данных:
Среди них атрибутами модели являются:
-
name
: название модели -
state
: начальное состояние модели -
reducers
: состояние, которое обрабатывает текущее состояние модели. -
selectors
: селекторы состояния, которые обслуживают текущую модель -
flows
: бизнес-поток, задействованный в текущей модели (побочные эффекты).
Эта классическая модель перегородок точноDvaРазделение приложений означает, что только свойства модели немного отличаются.
Предполагая, что мы создали модели пользователей и постов, фреймворк смонтирует их состояния в поддеревьях пользователей и постов:
Соглашения - избавьтесь от шаблонного кода
Вооружившись концепцией модели, фреймворк может определить набор соглашений для сокращения написания стандартного кода. Во-первых, давайте рассмотрим, как мы ранее определили тип действия:
- название действия
- Укажите пространство имен, чтобы предотвратить конфликты имен
Например, мы определяем пользовательские данные для извлечения соответствующего типа действия:
const FETCH = 'USRE/FETCH'
const FETCH_SUCCESS = 'USER/FETCH_SUCCESSE'
const FETCH_ERROR = 'USER/FETCH_ERROR'
в,FETCH
соответствуетасинхронныйдействие по извлечению данных,FETCH_SUCCESS
а такжеFETCH_ERROR
соответствует двумСинхронизироватьДействие, изменяющее состояние.
Соглашение о синхронном действии
Для действий, которые являются синхронными и не содержат побочных эффектов, мы напрямую доставляем их редьюсеру, не нарушая чистоту редьюсера. Поэтому вполне можно согласиться: редуктор под модельимяСопоставьте тип действия, который работает непосредственно с состоянием:
SYNC_ACTION_TYPE = MODEL_NAME/REDUCER_NAME
Например, следующая пользовательская модель:
const userModel = {
name: 'user',
state: {
list: [],
total: 0,
loading: false
},
reducers: {
fetchStart(state, payload) {
return { ...state, loading:true }
}
}
}
Когда мы отправляем типuser/fetchStart
После действия действие входит в свою полезную нагрузку.user.fetchStart
Под этим редуктором производятся изменения состояния.
Соглашение об асинхронных действиях
Для асинхронных действий мы не можем напрямую обрабатывать асинхронные задачи в редюсере, а поток в модели — это контейнер асинхронных задач:
ASYNC_ACTION_TYPE = MODEL_NAME/FLOW_NAME
Например, следующая модель:
const user = {
name: 'user',
state: {
list: [],
total: 0,
loading: false
},
flows: {
fetch() {
// ... 处理一些异步任务
}
}
}
Если мы выдадимuser/fetch
, поскольку в пользовательской модели есть поток с именем fetch, то введите этот поток для обработки асинхронных задач.
Переопределение и обновление статуса
Если слишком утомительно писать соответствующий редюсер для каждого обновления состояния, мы можем рассмотреть возможность определения редьюсера изменений для каждой модели длянепосредственныйобновить состояние:
const userModel = {
name: 'user',
state: {
list: [],
pagination: {
page: 1,
total: 0
},
loading: false
},
reducers: {
change(state, action) {
return { ...state, ...action.payload }
}
}
}
На этом этапе, когда мы отправим одно из следующих действий, мы сможемloading
Установите статус в true:
dispatch({
type: 'user/change',
payload: {
loading: true
}
})
Тем не менее, это обновлениеПокрытый, предполагая, что мы хотим обновить текущую информацию о странице в состоянии:
dispatch({
type: 'user/change',
payload: {
pagination: { page: 1 }
}
})
Статус станет:
{
list: [],
pagination: {
page: 1
},
loading: false
}
pagination
Состояние полностью затирается, общее количество состояний в немtotal
потерян.
Поэтому мы также определяем patch-редьюсер, что означаетОбновление патча, который влияет только на подсостояния, объявленные в полезной нагрузке действия:
import { merge } from 'lodash.merge'
const userModel = {
name: 'user',
state: {
list: [],
pagination: {
page: 1,
total: 0
},
loading: false
},
reducers: {
change(state, action) {
return {
{ ...state, ...action.payload }
}
},
patch(state, action) {
return deepMerge(state, action.payload)
}
}
}
Теперь попробуем обновить только пагинацию:
dispatch({
type: 'user/patch',
payload: {
pagination: { page: 1 }
}
})
Новое состояние:
{
list: [],
pagination: {
page: 1,
total: 0
},
loading: false
}
Примечание: Реализация здесь не является реализацией производственной среды.Недостаточно использовать слияние lodash напрямую, и требуются некоторые преобразования в реальном проекте.
Организация асинхронных задач
Dva использует redux-saga для организации побочных эффектов (в основном асинхронных задач), а Rematch использует для организации async/await. С точки зрения долгосрочной практики я предпочитаю использовать redux-observable, особенно после выхода его версии 1.0, он приносит observable.state$
, что позволяет нам более тщательно практиковать реактивное программирование. Давайте рассмотрим преимущества этого режима, упомянутые ранее:
- Унифицированный источник данных, компонуемый между наблюдаемыми
- Декларативное программирование, простой и лаконичный код
- Отличная управляемость в гонках
- Тест дружественный
- Облегчает автономность компонентов
Поэтому для обработки модельных асинхронных задач выбираем redux-observable:
const user:Model<UserState> = {
name: 'user',
state: {
list: [],
// ...
},
reducers: {
// ...
},
flows: {
fetch(flow$, action$, state$) {
// ....
}
}
}
Немного отличается от сигнатуры функции epic, у каждого потока есть еще одна функция.flow$
параметры, в приведенном выше примере это эквивалентно:
action$.ofType('user/fetch')
Этот параметр облегчает нам получение желаемого действия.
Обработка состояний загрузки и ошибок
Во фронтенд-инжиниринге часто возникают требования к неправильному отображению и отображению загрузки.
Было бы слишком хлопотно, если бы мы вручную управляли состоянием загрузки и состоянием ошибки каждой модели, поэтому в корневом состоянии два поддерева состояния разделены отдельно для обработки состояния загрузки и состояния ошибки, чтобы фреймворку было удобно управлять загрузкой и ошибками. Разработчики могут использовать его прямо в дереве состояний:
- loading
- error
Как показано на рисунке, состояние загрузки и состояние ошибки также необходимо разделить в соответствии с уровнем детализации. уровень детализации обслуживания, который используется для определения того, выполняется ли асинхронная служба.
Например, если:
loading.flows['user/fetch'] === true
То есть под пользовательскую модельfetch
поток идет.
подобно:
loading.services['/api/fetchUser'] === true
означает/api/fetchUser
Эта услуга находится в процессе.
Оперативное управление услугами
Фронтенд вызывает внутренние сервисы для манипулирования данными, поэтому мы также надеемся, что так называемая промежуточная роль (фреймворк) сможет внедрить сервисы в наш бизнес-процесс и завершить взаимодействие между сервисами и статусом приложения: наблюдайте за статус вызова и автоматически перехватывать вызов. Если есть исключение, своевременно измените состояние загрузки приложения и состояние ошибки, чтобы пользователи могли напрямую получить доступ к статусу работы службы в состоянии верхнего уровня.
Кроме того, в соответствии с парадигмой реактивного программирования управление сервисом, предоставляемое платформой, также должно быть реактивным при работе с успехом и ошибкой сервиса, то есть успех и ошибка будут предопределенными потоками (наблюдаемыми объектами), так что разработчики могут лучше использовать возможности реактивного программирования:
const user:Model<UserState> = {
name: 'user',
state: {
list: [],
total: 0
},
reducers: {
fetchSuccess(state, payload) {
return { ...state, list: payload.list, total: payload.total }
},
fetchError(state, payload) {
return { ...state, list:[] }
}
},
flows: {
fetch(flow$, action$, state$, dependencies) {
const { service } = dependencies
return flow$.pipe(
withLatestFrom(state$, (action, state) => {
// 拼装请求参数
return params
}),
switchMap(params => {
const [success$, error$] = service(getUsers(params))
return merge(
success$.pipe(
map(resp => ({
type: 'user/fetchSuccess',
payload: {
list: resp.list,
total: resp.total
}
}))
),
error$.pipe(
map(error => ({
type: 'user/fetchError'
}))
)
)
})
)
}
}
}
reobservable
Вышеприведенные мысли можно резюмировать как архитектура Dva + наблюдаемая редукция, первая может устранить длинный и многословный шаблонный код Redux, а вторая отвечает за асинхронное управление задачами.
Жаль, что Dva не использует redux-observable для управления побочными эффектами и не имеет соответствующих плагинов для использования redux-observable или RxJS для управления побочными эффектами, и весьма полезно реализовать промежуточное ПО Dva с redux-observable. через выставленный Двай крючок не гладкий, поэтому автор попытался написатьreobservableДля реализации упомянутого выше фреймворка он отличается от Dva тем, что:
- Сосредоточьтесь только на статусе приложения, а не на другой экологии маршрутизации компонентов
- Интеграция загрузки и обработки ошибок
- Используйте redux-observable вместо redux-saga для обработки побочных эффектов.
- Отзывчивая обработка услуг, поддержка применения пользовательских деталей услуги
Если ваше приложение использует Redux, вы страдаете от всех негативных эффектов Redux, и вы являетесь поклонником реактивного программирования и RxJS, вы можете попробовать reobservable. Но если вы предпочитаете сагу, или асинхронный ожидание, вам все равно стоит выбрать Два или Рематч, в арте есть специализации.
использованная литература
Об этой серии
- Эта серия статей начнется с введения в 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 за поддержку дизайна.