大纲:
本篇理论实际应用于antd-custom框架
1.背景
本篇主要阐述redux最佳实践的进阶篇,如果你对redux+redux-saga基础不是很了解,请先阅读上一篇redux最佳实践-前世今生1
上一篇我们简单阐述了redux+redux-saga的基础应用, increment.reducer.js和increment.saga.js是分开在不同的文件的,随着项目越来越大,开发要在不同文件中切换,十分繁琐,而且不好维护和管理,因此需要对reducer和saga进行整合,节省维护成本,提高开发效率。
接下来我们将借鉴dva的设计思想,改造redux+redux-saga实际应用,对reducer和saga进行整合。
提取reducer和saga,借鉴vue模式,使用mvvm模式处理模块化组件。
初步目标如下:
// increment.model.js
const model = {
// model名称,view层用于提取state的key,需要保证唯一
name: 'increment',
// 初始state状态
state: {},
// reducer
reducers: {},
// saga
effects: {},
}
export default model
处理reducer
1)原reducer
让我们先回顾下原reducer是如何注册到rootReducer中去的:
// increment.reducer.js
const incrementReducer = (state = {
count: 0,
loading: false,
}, action) => {
const { type } = action
switch(type) {
case 'INCREMENT':
return {
...state,
count: state.count + 1,
}
case 'INCREMENT_ASYNC':
return {
...state,
loading: true,
}
case 'INCREMENT_ASYNC_SUCCESS':
const { payload } = action
return {
...state,
count: state.count + payload,
loading: false,
}
default:
return state
}
}
export default incrementReducer
// rootReducer.js
import { combineReducers } from 'redux'
import incrementReducer from './increment.reducer'
const rootReducer = combineReducers({
incrementReducer,
})
export default rootReducer
对比上述reducer,我们的目标是将reducer注册在rootReducer当中。
2)目标reducer
假设我们的increment.model.js是这样:
// increment.model.js
const model = {
// model名称,view层用于提取state的key,需要保证唯一
name: 'increment',
// 初始state状态
state: {
loading: false,
count: 0,
},
// reducer
reducers: {
'INCREMENT': function(state, action) {
return { ...state, loading: true }
},
'INCREMENT_ASYNC': function(state, action) {
const { payload } = action
return { ...state, loading: false, count: state.count + payload }
},
},
// saga
effects: {},
}
export default model
3)整合reducer
根据上述目标reducer,那么我们需要将state和reducer像increment.reducer.js文件一样,注册到rootReducer.js文件中去。所以在注册前我们需要这样处理:
// rootReducer.js
import { combineReducers } from 'redux'
import { handleActions } from 'redux-actions'
// 用于缓存所有reducer(即state)
const reducer = {}
// 读取所有.model.js结尾的文件
const models = require.context('../', true, /\.model\.js$/)
models.keys().map(path => {
// 引入model
const item = models(path).default
// console.dir(item)
// 对每个model进行操作-处理对应的state和reducer
if (item && item.name) {
const { name, state={}, reducers={} } = item
// 使用handleActions整合所有的reducer函数
reducer[name] = handleActions(reducers, state)
}
})
const rootReducer = combineReducers({
...reducer,
})
export default rootReducer
这样就将reducer整合完毕了
处理saga
1)原saga
同理,我们先回顾下原saga是如何注册到rootSaga中去的:
// increment.saga.js
import { put, takeEvery, call, takeLatest } from 'redux-saga/effects'
import { increment } from '../api/increment.api'
export function* incrementAsync(obj) {
const resp = yield call(increment, obj.payload)
console.log("resp", resp)
yield put({ type: 'INCREMENT_ASYNC_SUCCESS', payload: resp.data })
}
export function* watchIncrementAsync() {
yield takeLatest('INCREMENT_ASYNC', incrementAsync)
}
// rootSaga.js
import { all, put, call, takeLatest } from 'redux-saga/effects'
import { watchIncrementAsync } from './increment.saga'
export default function *rootSaga() {
yield all([
watchIncrementAsync()
])
}
2)目标saga
对比上述saga,我们的目标是将saga注册在rootSaga当中。
假设我们的increment.model.js是这样:
// increment.model.js
import { increment } from './index.api'
const model = {
// ...
// saga
effects: {
'INCREMENT': function*({ payload }, { call, put }) {
const resp = yield call(increment, payload)
if (resp) {
console.log("resp", resp)
yield put({ type: 'INCREMENT_ASYNC', payload: resp.data })
} else {
throw resp
}
},
},
}
export default model
3)整合saga
根据上述目标saga,那么我们需要将saga像increment.saga.js文件一样,注册到rootSaga.js文件中去。所以在注册前进行预处理,如下:
// rootSaga.js
import { all, put, call, takeLatest } from 'redux-saga/effects'
// 用于缓存所有effects函数
const sagas = []
// 读取所有.model.js结尾的文件
const models = require.context('../', true, /\.model\.js$/)
models.keys().map(path => {
// 引入model
const item = models(path).default
// 对每个model进行操作-处理对应的effects
if (item && item.effects) {
const { effects } = item
for(let key in effects) {
const watch = function* () {
yield takeLatest(key, function*(obj) {
// 第二个参数只传递了最常用的call,put进去,
// 如果想用更多其他'redux-saga/effects'的API,可自行引入
try {
yield effects[key](obj, { call, put })
} catch(e) {
// 统一处理effects抛出的错误
}
})
}
sagas.push(watch())
}
}
})
export default function *rootSaga() {
yield all(sagas)
}
这样saga就处理完成了
4.总结
最后,reducer和saga就整合在一个文件incrment.model.js中,如下: // incrment.model.js
import { increment } from './index.api'
const model = {
// model名称,view层用于提取state的key,需要保证唯一
name: 'increment',
// 初始state状态
state: {
loading: false,
count: 0,
},
// reducer
reducers: {
'INCREMENT': function(state, action) {
return { ...state, loading: true }
},
'INCREMENT_ASYNC': function(state, action) {
const { payload } = action
return { ...state, loading: false, count: state.count + payload }
},
},
// saga
effects: {
'INCREMENT': function*({ payload }, { call, put }) {
const resp = yield call(increment, payload)
if (resp) {
console.log("resp", resp)
yield put({ type: 'INCREMENT_ASYNC', payload: resp.data })
} else {
throw resp
}
},
},
}
export default model
view层使用,如下: // Home.jsx
import React from 'react'
import { Button } from 'antd'
import { connect } from 'react-redux'
const Home = (props) => {
const onIncrementAsync = (e) => {
props.dispatch({
type: 'INCREMENT',
payload: {
data: 20,
},
})
}
return (
<aside>
<Button type='primary' onClick={onIncrementAsync} loading={props.loading}>+20</Button>
<h4>count: { props.count }</h4>
</aside>
)
}
const mapDispatchToProp = (dispatch) => {
return { dispatch }
}
const mapStateToProp = state => {
const { increment } = state
return increment
}
export default connect(mapStateToProp, mapDispatchToProp)(Home)
本例子的其他文件可在上一篇redux最佳实践-前世今生1中找到
效果图如下:
本篇理论实际应用于antd-custom框架
与dva的区别?
dva:黑盒子,功能丰富,整合了更多其他的功能,包含了reducer, saga, router等,但也因此有很多的规则,灵活性降低。
本篇:白盒子,功能简单,这里只是简单整合了reducer和saga,配置更灵活,并且完全可控,规则可自行修改,只有几十行的源码