redux最佳实践-前世今生2

139 阅读3分钟

大纲:

本篇理论实际应用于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中找到

效果图如下:

res4
res5

本篇理论实际应用于antd-custom框架

与dva的区别?

dva:黑盒子,功能丰富,整合了更多其他的功能,包含了reducer, saga, router等,但也因此有很多的规则,灵活性降低。

本篇:白盒子,功能简单,这里只是简单整合了reducer和saga,配置更灵活,并且完全可控,规则可自行修改,只有几十行的源码