前言
Redux是一個非常實用的狀態管理庫,對於大多數使用React庫的開發者來說,Redux都是會接觸到的。在使用Redux享受其帶來的便利的同時, 我們也深受其問題的困擾。
redux的問題
之前在另外一篇文章Redux基礎中,就有提到以下這些問題
- 純淨。Redux只支持同步,讓狀態可預測,方便測試。 但不處理異步、副作用的情況,而把這個丟給了其他中間件,諸如redux-thunkredux-promiseredux-saga等等,選擇多也容易造成混亂~
- 囉嗦。那麼寫過Redux的人,都知道actionreducer以及你的業務代碼非常囉嗦,模板代碼非常多。但是~,這也是爲了讓數據的流動清晰明瞭。
- 性能。粗暴地、級聯式刷新視圖(使用react-redux優化)。
- 分型。原生 Redux-react 沒有分形結構,中心化 store
裏面除了性能這一塊可以利用react-redux進行優化,其他的都是開發者不得不面對的問題,對於代碼有潔癖的人,囉嗦這一點確實是無法忍受的。
方案目標
如果你使用過VUEX的話, 那麼對於它的API肯定會相對喜歡很多,當然,vuex不是immutable,所以對於時間旅行這種業務不太友好。不過,我們可以自己實現一個具有vuex的簡潔語法和immutable屬性的redux-x(瞎命名)。
先看一下我們想要的目標是什麼樣的?
首先, 我們再./models裏面定義每個子state樹,裏面帶有namespace、state、reducers、effects等屬性, 如下:
export default {
// 命名空間
namespace: 'common',
// 初始化state
state: {
loading: false,
},
// reducers 同步更新 類似於vuex的mutations
reducers: {
updateLoadingStatus(state, action) {
return {
...state,
loading: action.payload
}
},
},
// reducers 異步更新 類似於vuex的actions
efffects: {
someEffect(action, store) {
// some effect code
...
...
// 將結果返回
return result
}
}
}
通過上面的實現,我們基本解決了Redux本身的一些瑕疵
1.在effects中存放的方法用於解決不支持異步、副作用的問題
2.通過合併reducer和action, 將模板代碼大大減少
3.具有分型結構(namespace),並且中心化處理
如何實現
暴露的接口redux-x
首先,我們只是在外層封裝了一層API方便使用,那麼說到底,傳給redux的combineReducers還是一個redux對象。另外一個則是要處理副作用的話,那就必須使用到了中間件,所以最後我們暴露出來的函數的返回值應該具有上面兩個屬性,如下:
import reduxSimp from '../utils/redux-simp' // 內部實現
import common from './common' // models文件下common的狀態管理
import user from './user' // models文件下user的狀態管理
import rank from './rank' // models文件下rank的狀態管理
const reduxX = reduxSimp({
common,
user,
rank
})
export default reduxX
const store = createStore(
combineReducers(reduxX.reducers), // reducers樹
{},
applyMiddleware(reduxX.effectMiddler) // 處理副作用中間件
)
第一步, 我們先實現一個暴露出來的函數reduxSimp,通過他對model裏面各個屬性進行加工,大概的代碼如下:
const reductionReducer = function() { // somecode }
const reductionEffects = function() { // somecode }
const effectMiddler = function() { // somecode }
/**
* @param {Object} models
*/
const simplifyRedux = (models) => {
// 初始化一個reducers 最後傳給combinReducer的值 也是最終還原的redux
const reducers = {}
// 遍歷傳入的model
const modelArr = Object.keys(models)
modelArr.forEach((key) => {
const model = models[key]
// 還原effect
reductionEffects(model)
// 還原reducer,同時通過namespace屬性處理命名空間
const reducer = reductionReducer(model)
reducers[model.namespace] = reducer
})
// 返回一個reducers和一個專門處理副作用的中間件
return {
reducers,
effectMiddler
}
}
還原effects
對於effects, 使用的時候如下(沒什麼區別):
props.dispatch({
type: 'rank/fundRankingList_fetch',
payload: {
fundType: props.fundType,
returnType: props.returnType,
pageNo: fund.pageNo,
pageSize: 20
}
})
還原effects的思路大概就是先將每一個model下的effect收集起來,同時加上命名空間作爲前綴,將副作用的key即type 和相對應的方法value分開存放在兩個數組裏面,然後定義一箇中間件,每當有一個dispatch的時候,檢查key數組中是否有符合的key,如果有,則調用對應的value數組裏面的方法。
// 常量 分別存放副作用的key即type 和相對應的方法
const effectsKey = []
const effectsMethodArr = []
/**
* 還原effects的函數
* @param {Object} model
*/
const reductionEffects = (model) => {
const {
namespace,
effects
} = model
const effectsArr = Object.keys(effects || {})
effectsArr.forEach((effect) => {
// 存放對應effect的type和方法
effectsKey.push(namespace + '/' + effect)
effectsMethodArr.push(model.effects[effect])
})
}
/**
* 處理effect的中間件 具體參考redux中間件
* @param {Object} store
*/
const effectMiddler = store => next => (action) => {
next(action)
// 如果存在對應的effect, 調用其方法
const index = effectsKey.indexOf(action.type)
if (index > -1) {
return effectsMethodArr[index](action, store)
}
return action
}
還原reducers
reducers的應用也是和原來沒有區別:
props.dispatch({ type: 'common/updateLoadingStatus', payload: true })
代碼實現的思路就是最後返回一個函數,也就是我們通常寫的redux函數,函數內部遍歷對應命名空間的reducer,找到匹配的reducer執行後返回結果
/**
* 還原reducer的函數
* @param {Object} model 傳入的model對象
*/
const reductionReducer = (model) => {
const {
namespace,
reducers
} = model
const initState = model.state
const reducerArr = Object.keys(reducers || {})
// 該函數即redux函數
return (state = initState, action) => {
let result = state
reducerArr.forEach((reducer) => {
// 返回匹配的action
if (action.type === `${namespace}/${reducer}`) {
result = model.reducers[reducer](state, action)
}
})
return result
}
}
最終代碼
最終的代碼如下,加上了一些錯誤判斷:
// 常量 分別存放副作用的key即type 和相對應的方法
const effectsKey = []
const effectsMethodArr = []
/**
* 還原reducer的函數
* @param {Object} model 傳入的model對象
*/
const reductionReducer = (model) => {
if (typeof model !== 'object') {
throw Error('Model must be object!')
}
const {
namespace,
reducers
} = model
if (!namespace || typeof namespace !== 'string') {
throw Error(`The namespace must be a defined and non-empty string! It is ${namespace}`)
}
const initState = model.state
const reducerArr = Object.keys(reducers || {})
reducerArr.forEach((reducer) => {
if (typeof model.reducers[reducer] !== 'function') {
throw Error(`The reducer must be a function! In ${namespace}`)
}
})
// 該函數即redux函數
return (state = initState, action) => {
let result = state
reducerArr.forEach((reducer) => {
// 返回匹配的action
if (action.type === `${namespace}/${reducer}`) {
result = model.reducers[reducer](state, action)
}
})
return result
}
}
/**
* 還原effects的函數
* @param {Object} model
*/
const reductionEffects = (model) => {
const {
namespace,
effects
} = model
const effectsArr = Object.keys(effects || {})
effectsArr.forEach((effect) => {
if (typeof model.effects[effect] !== 'function') {
throw Error(`The effect must be a function! In ${namespace}`)
}
})
effectsArr.forEach((effect) => {
// 存放對應effect的type和方法
effectsKey.push(namespace + '/' + effect)
effectsMethodArr.push(model.effects[effect])
})
}
/**
* 處理effect的中間件 具體參考redux中間件
* @param {Object} store
*/
const effectMiddler = store => next => (action) => {
next(action)
// 如果存在對應的effect, 調用其方法
const index = effectsKey.indexOf(action.type)
if (index > -1) {
return effectsMethodArr[index](action, store)
}
return action
}
/**
* @param {Object} models
*/
const simplifyRedux = (models) => {
if (typeof models !== 'object') {
throw Error('Models must be object!')
}
// 初始化一個reducers 最後傳給combinReducer的值 也是最終還原的redux
const reducers = {}
// 遍歷傳入的model
const modelArr = Object.keys(models)
modelArr.forEach((key) => {
const model = models[key]
// 還原effect
reductionEffects(model)
// 還原reducer,同時通過namespace屬性處理命名空間
const reducer = reductionReducer(model)
reducers[model.namespace] = reducer
})
// 返回一個reducers和一個專門處理副作用的中間件
return {
reducers,
effectMiddler
}
}
export default simplifyRedux
思考
如何結合Immutable.js使用?