本文是 《使用 RxJS + Redux 管理應用狀態》系列第三篇文章,將介紹咱們在使用 Redux 時的困惑,如何從新思考 Redux 定下的範式,以及咱們能爲此作出的努力。返回第一篇:使用 redux-observable 實現組件自治html
本系列的文章地址彙總:前端
首先要明確的是,Redux 並非 React 獨有的一個插件,它是順應前端組件化開發潮流而誕生的一種狀態管理模型,你在 Vue 或者 Angular 中也可使用這個模型。react
目前,你們都比較承認的是,某一時刻的應用或者組件狀態,將對應此時應用或者組件的 UI:git
UI = f(state)
複製代碼
那麼,在前端組件化開發的時候,就須要思考兩個問題:github
組件所具備的狀態,一搬來源於兩個方面:編程
狀態源爲組件輸送了其須要的狀態,進而,組件的外觀形態也獲得了確認。在簡單工程和簡單組件中,咱們思考了狀態來源也就好了,若是引入額外的狀態管理方案(例如咱們爲一個使用 Redux 管理一個按鈕組件的狀態),反而會加劇每一個組件的負擔,形成了多餘的抽象和依賴。redux
而對於大型前端工程和複雜組件來講,其每每具備以下特色:後端
在這種場景下,樸素的狀態管理就顯得捉襟見肘了,主要體如今下面幾個方面:api
Redux 正是要去解決這些問題,從而讓大型前端工程的狀態更加可控。Redux 提出了一套約定模型,讓狀態的更新和派發都集中了:bash
Redux 所使用的模型是受到了 Elm 的啓發:
在 Elm 中,流動於應用中的是消息(msg) :一個由**消息類型(type)所標識,而且攜帶了內容(payload)**的數據結構。消息決定了數據模型(model)怎麼更新,而數據又決定了 UI 形態。
而在 Redux 中,消息被稱替代爲動做(action),而且使用 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 中,對不一樣 action type,經過 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 的負面影響時,切到其餘的狀態管理方案(例如 mobx 或者 mobx-state-stree),也不太現實,一方面是遷移成本大,一方面你也不知道新的狀態管理方案是否就是銀彈。可是,對 Redux 的負面影響無動於衷或者忍氣吞聲,也只會讓問題越滾越大,直到失控。
在開始討論如何更好地 Redux 以前,咱們須要明確一點,樣板代碼和異步能力的缺少,是 Redux 自身設計的結果,而非目的,換句話說,Redux 設計出來,並非要讓開發者去撰寫樣本代碼,或者去糾結怎麼處理異步狀態更新。
咱們須要再定義一個角色,讓他來代替咱們去寫樣板代碼,讓他給予咱們最優秀的異步任務處理能力,讓他負責一切 Redux 中惡心的事兒。所以,這個角色就是一個讓 Redux 變得更加優雅的框架,至於如何建立這個角色,須要咱們從單個組件開始,從新梳理下應用形態,並着眼於:
一個組件的生態大概是這樣的:
即:數據經處理造成頁面狀態,頁面狀態決定 UI 渲染。
而組件生態(UI + 狀態 + 狀態管理方式)的組合就構成了咱們應用:
這裏組件生態特地只展現了數據到狀態這一步,由於 Redux 處理的正是這個部分。咱們暫且能夠定義數據到狀態的過程爲 flow,即一個業務流的意思。
借鑑於 Elm,咱們能夠按數據模型對應用進行劃分:
其中,模型具備的屬性有:
name
: 模型名稱state
:模型的初始狀態reducers
:處理當前模型狀態的 stateselectors
:服務於當前模型的 state selectorsflows
:當前模型涉及的業務流(反作用)這個經典的劃分模型正是 Dva 的應用劃分手段,只是模型屬性略有不一樣。
假定咱們建立了 user 模型和 post 模型,那麼框架將掛載他們的狀態到 user 和 post 狀態子樹下:
有了模型這個概念後,框架就能定義一系列的約定去減小樣板代碼的書寫。首先,咱們回顧下之前咱們是怎麼定義的一個 action type 的:
例如,咱們這樣定義用戶數據拉取相關的 action type:
const FETCH = 'USRE/FETCH'
const FETCH_SUCCESS = 'USER/FETCH_SUCCESSE'
const FETCH_ERROR = 'USER/FETCH_ERROR'
複製代碼
其中, FETCH
對應的是一個異步 拉取數據的 action,FETCH_SUCCESS
和 FETCH_ERROR
則對應兩個同步修改狀態的 action。
同步 action 約定
對於同步的、不包含反作用的 action,咱們直接將其呈遞到 reducer,是不會破壞 reducer 純度的。 所以,咱們不妨約定: model 下 reducer 的名字映射一個直接對狀態操做的 action type:
SYNC_ACTION_TYPE = MODEL_NAME/REDUCER_NAME
複製代碼
例以下面這個 user model:
const userModel = {
name: 'user',
state: {
list: [],
total: 0,
loading: false
},
reducers: {
fetchStart(state, payload) {
return { ...state, loading:true }
}
}
}
複製代碼
當咱們派發了一個類型爲 user/fetchStart
的 action 以後,action 就帶着其 payload 進入到 user.fetchStart
這個 reducer 下,進行狀態變動。
異步 action 約定
對於異步的 action,咱們就不能直接在 reducer 進行異步任務處理,而 model 中的 flow 就是異步任務的集裝箱:
ASYNC_ACTION_TYPE = MODEL_NAME/FLOW_NAME
複製代碼
例以下面這個 model:
const user = {
name: 'user',
state: {
list: [],
total: 0,
loading: false
},
flows: {
fetch() {
// ... 處理一些異步任務
}
}
}
複製代碼
若是咱們在 UI 裏面發出了個 user/fetch
,因爲 user model 中存在一個名爲 fetch 的 flow,那麼就進入到這個flow 中進行異步任務的處理。
狀態的覆蓋與更新
若是每一個狀態的更新都去撰寫一個對應的 reducer 就太累了,所以,咱們能夠考慮爲每一個模型定義一個 change reducer,用於直接更新狀態:
const userModel = {
name: 'user',
state: {
list: [],
pagination: {
page: 1,
total: 0
},
loading: false
},
reducers: {
change(state, action) {
return { ...state, ...action.payload }
}
}
}
複製代碼
此時,當咱們派發了下面的一個 action,就將可以將 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 reducer,意爲對狀態的補丁更新,它只會影響到 action payload 中聲明的子狀態:
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 的 merge 是不夠的,實際項目中還要進行必定改造。
Dva 使用了 redux-saga 進行反作用(主要是異步任務)的組織,Rematch 則使用了 async/await 進行組織。從長期的實踐來看,我更偏向於使用 redux-observable,尤爲是在其 1.0 版本的發佈以後,更是帶來了可觀察的 state$
,使得咱們能更加透徹地實踐響應式編程。咱們回顧下前文中提到的該模式的好處:
所以,對於模型異步任務的處理,咱們選擇 redux-observable:
const user:Model<UserState> = {
name: 'user',
state: {
list: [],
// ...
},
reducers: {
// ...
},
flows: {
fetch(flow$, action$, state$) {
// ....
}
}
}
複製代碼
與 epic 的函數簽名略有不一樣的是,每一個 flow 多了一個 flow$
參數,以上例來講,它就至關於:
action$.ofType('user/fetch')
複製代碼
這個參數便於咱們更快的取到須要的 action。
前端工程中常常會有錯誤展現和加載展現的需求,
若是咱們手動管理每一個模型的加載態和錯誤態就太麻煩了,所以在根狀態下,單獨劃分兩棵狀態子樹用於處理加載態與錯誤態,這樣,便於框架去治理加載與錯誤,開發者直接在狀態樹上取用便可:
如圖,加載態和錯誤態還須要根據粒度進行劃分,有大粒度的 flow 級別,用於標識一個 flow 是否正在進行中;也有小粒度的 service 級別,用於標識某個異步服務是否在進行中。
例如,若:
loading.flows['user/fetch'] === true
複製代碼
即表示 user model 下的 fetch
flow 正在進行中。
若:
loading.services['/api/fetchUser'] === true
複製代碼
即表示 /api/fetchUser
這個服務正在進行中。
前端調用後端服務操縱數據是一個普遍的需求,所以,咱們還但願所謂的中間角色(框架)可以在咱們的業務流中注入服務,完成服務和應用狀態的交互:觀察調用情況,自動捕獲調用異常,適時地修改應用 loading 態和 error 態,方便用戶直接在頂層狀態取用服務運行情況。
另外,在響應式編程的範式下,框架提供的服務治理,在處理服務的成功和錯誤時應該也是響應式的,即成功和錯誤將是預約義的流(observable 對象),從而讓開發者能更好的利用到響應式編程的能力:
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'
}))
)
)
})
)
}
}
}
複製代碼
上面的種種思考,歸納下來其實就是 Dva architecture + redux-observable,前者可以打掉 Redux 冗長囉嗦的樣板代碼,後者則負責異步任務治理。
比較遺憾的是,Dva 沒有使用 redux-observable 進行反作用管理,也沒有相關插件實現使用 redux-observable 或者 RxJS 進行反作用管理,而且,經過 Dva 暴露的 hook 去實現一個 redux-observable 的 Dva 中間件也頗爲不順暢,所以,筆者嘗試撰寫了一個 reobservable 來實現上面提到框架,它與 Dva 不一樣的是:
若是你的應用使用了 Redux,你苦於 Redux 種種負面影響,而且你仍是一個響應式編程和 RxJS 的愛好者,你能夠嘗試下 reobservable。可是若是你偏心 saga,或者 async await,你仍是應該選擇 Dva 或者 Rematch,術業有專攻。