从 vue 换到 React+Redux 进行开发已经有半年多的时间, 总的来说体验是很好的, 对于各种逻辑和业务组件的抽象实在是方便的不行, 高阶组件, 洋葱模型等等给我带来了很多编程思想上的提升. 但是在使用 Redux 开发的过程中还是感觉不太顺手, 本文将阐述我是如何对 Redux 进行一步步 "改造" 以适应个人和团队开发需求的.
问题
在使用 Redux 开发的过程中逐渐发现, 虽然我们已经将 UI 组件和业务组件尽可能的进行抽离, 尽可能的保证 reduceractions 的复用性,
但是我们还是会花费大量的时间来书写近乎相同的代码. 尤其是我们组内希望秉承一个原则: 尽量将所有的操作及状态修改都交由 action 来执行, 方便我们对问题进行定位. 当我在某大型前端交流群里还看到 "不用 Redux, 不想加班" 的说法时, 不得不感叹, 需要做些努力来解决我目前的问题了.
是的, Redux 对我来说, 太复杂了
针对一个简单的操作, 我们需要进行以下步骤来完成:
1. 定义 action
export const CHANGE_CONDITION = 'CHANGE_CONDITION'
2. 定义一个对应的 action 创建函数
- export const changeCondition = condition => ({
- type: CHANGE_CONDITION,
- condition
- })
3. 引入 action, 定义 reducer, 在复杂的 switch 语句中, 对对象进行更改
- import {CHANGE_CONDITION} from '@actions'
- const condition = (state = initCondition, action) => {
- switch(action.type) {
- case CHANGE_CONDITION:
- return ...
- default:
- return state
- }
- }
4. 在需要时, 引入 action 创建函数, 并将对应的 state 进行连接
- import { changeCondition } from 'actions'
- @connect(...)
我只是想做一个简单的状态修改呀!
可能我们会说, 这样拆分能够保证我们整个项目的规范化, 增强业务的可预测性与错误定位能力.
但是随着项目的不断扩大, 每个页面都有一堆 action 需要我加的时候, 实在是让人头痛啊.
而且, 针对请求的修改, 我们往往要把 action 拆分成 START,SUCCESS,FAILED 三种状态, reducer 里需要进行三次修改. 而且往往
针对这些修改, 我们进行的处理都是大致相同的: 更新 loading 状态, 更新数据, 更新错误等等.
所以说, 我们如何在保证 redux 的设计原则以及项目规范性上, 对其进行 "简化改造", 是我这里需要解决的问题.
使用 middleware 简化请求
针对请求的处理, 我之前也写过一篇文章优雅地减少 redux 请求样板代码 https://juejin.im/post/5ac1f428f265da23884d3997 , 通过封装了一个 redux 中间件 https://github.com/callmedadaxin/react-fetch-middleware
来对请求代码进行优化.
大致思路如下:
1.action 创建函数返回的内容为一个包含请求信息的对象, 并包含需要分发的三个 action, 这三个 action 可以通过 actionCreator 进行创建
- import { actionCreator } from 'redux-data-fetch-middleware'
- // create action types
- export const actionTypes = actionCreator('GET_USER_LIST')
- export const getUserList = params => ({
- url: '/api/userList',
- params: params,
- types: actionTypes,
- // handle result
- handleResult: res => res.data.list,
- // handle error
handleError: ...
})
2. 在 redux 中间件中, 针对以上格式的 action 进行处理, 首先进行请求, 并分发请求开始的 action,
在请求成功和失败时, 分别分发对应的 action
- const applyFetchMiddleware = (
- fetchMethod = fetch,
- handleResponse = val => val,
- handleErrorTotal = error => error
- ) =>
- store => next => action => {
- // 判断 action 的格式
- if (!action.url || !Array.isArray(action.types)) {
- return next(action)
- }
- // 获取传入的三个 action
- const [ START, SUCCESS, FAILED ] = action.types
- // 在不同状态分发 action, 并传入 loading,error 状态
- next({
- type: START,
- loading: true,
- ...action
- })
- return fetchMethod(url, params)
- .then(ret => {
- next({
- type: SUCCESS,
- loading: false,
- payload: handleResult(ret)
- })
- })
- .catch(error => {
- next({
- type: FAILED,
- loading: false,
- error: handleError(error)
- })
- })
- }
3. 将 reducer 进行对应的默认处理, 使用 reducerCreator 创建的函数中自动进行对应处理, 并且提供二次处理的机制
- const [ GET, GET_SUCCESS, GET_FAILED ] = actionTypes
- // 会在这里自动处理分发的三个 action
- const fetchedUserList = reducerCreator(actionTypes)
- const userList = (state = {
- list: []
- }, action => {
- // 二次处理
- switch(action.type) {
- case GET_SUCCESS:
- return {
- ...state,
- action.payload
- }
- }
- })
- export default combineReducers({
- userList: fetchedUserList(userList)
- })
再进一步, 简化 Redux Api
经过前一步对请求的简化, 我们已经可以在保证不改变 redux 原则和书写习惯的基础上, 极大的简化请求样板代码.
针对普通的数据处理, 我们是不是可以更进一步?
很高兴看到这个库: Rematch https://hk.saowen.com/a/904ec76cd8897fc055a07d37b2cc37228a315575c14c8bfdedc727886b736292
, 对 Redux Api 进行了极大的简化.
但是有些功能和改进并不是我们想要的, 因此我仅对我需要的功能和改进点进行说明, 并用自己的方式进行实现. 我们来一步步看看
我们需要解决的问题以及如何解决的.
1. 冗长的 switch 语句
针对 reducer, 我们不希望重复的引用定义的各个 action, 并且去掉冗长的 switch 判断. 其实我们可以将其进行反转拆分, 将每一个 action 定义为标准化的 reducer, 在其中对 state 进行处理.
- const counter = {
- state: 1,
- reducers: {
- add: (state, payload) => state + payload,
- sub: (state, payload) => state - payload
- }
- }
2. 复杂的 action 创建函数
去掉之前的 action 和 action 创建函数, 直接在 actions 中进行数据处理, 并与对应的 reducer 进行 match
export const addNum = num => dispatch => dispatch('/counter/add', num)
我们会看到, 与 reducer 进行 match 时, 我们使用了'/counter/add'这种命名空间的方式,
目的是在保证其直观性的同时, 保证 action 与其 reducer 是一一对应的.
我们可以通过增强的 combinceReducer 进行命名空间的设定:
- const counter1 = {
- ...
- }
- const counter2 = {
- ...
- }
- const counters = combinceReducer({
- counter1,
- counter2
- })
- const list = {
- ...
- }
- // 设置大 reducer 的根命名空间
- export default combinceReducer({
- counters,
- list
- }, '/test')
- // 我们可以通过这样来访问
- dispatch('/test/counters/counter1/add')
3. 别忘了请求
针对请求这些异步 action, 我们可以参考我们之前的修改, dispatch 一个对象
- export const getList = params => dispatch => {
- return dispatch({
- // 对应到我们想要 dispatch 的命名空间
- action: '/list/getList',
- url: '/api/getList',
- params,
- handleResponse: res => res.data.list,
- handleError: error => error
- })
- }
同时, 我们在 reducer 中进行简单的处理即可, 依旧可以进行默认的三个状态处理
- const list = {
- // 定义 reducer 头, 会自动变为 getList(开始请求),getListSuccess,getListFailed
- // 并进行 loading 等默认处理
- fetch: 'getList'
- state: {
- list: []
- },
- reducers: {
- // 二次处理
- getListSuccess: (state, payload) => ({
- ...state,
- list: payload
- })
- }
- }
与项目进行整合
我们会看到, 我们已经将 redux 的 api 进行了极大的简化, 但是依旧保持了原有的结构. 目的有以下几点:
依旧遵循默认原则, 保证项目的规范性
通过约定和命名空间来保证 action 和 reducer 的 match
底层还是使用 redux 实现, 这些只不过是语法糖
保证与老项目的兼容性
原有的数据流变成了这样:
因此, 我们是在 redux 的基础上进行二次封装的, 我们依然保证了原有的 Redux 数据流, 保证数据的可回溯性, 增强业务的可预测性与错误定位能力. 这样能极大的保证与老项目的兼容性, 所以我们需要做的, 只是对 action 和 reducer 的转化工作
1.combinceReducer 返回原格式的 reducer
我们通过新的 combinceReducer, 将新的格式, 转化为之前的 reducer 格式, 并保存各个 reducer 其和对应的 action 的命名空间.
代码简单示意:
- // 获取各 reducers 里的方法
- const actionNames = Object.keys(reducers)
- const resultActions = actionNames.map(action => {
- const childNamespace = `${namespace}/${action}`
- // 将 action 存入 namespace
- Namespace.setActionByNamespace(childNamespace)
- return {
- name: Namespace.toAction(childNamespace),
- fn: reducers[action]
- }
- })
- // 返回默认格式
- return (state = inititalState, action) => {
- // 查询 action 对应的新的 reducer 里的方法
- const actionFn = resultActions.find(cur => cur.name === action.type)
- if (actionFn) {
- return actionFn.fn && actionFn.fn(state, action.payload)
- }
- return state
- }
2. 新的 action 创建函数最终 dispatch 出原格式的 action
我们需要把这样格式的函数, 转化成这样
- count => dispatch => dispatch('/count/add', count)
- //or
- params => dispatch => { dispatch('/count/add', 1), dispatch('/count/sub', 2) }
- // 结果
- count => ({ type: 'count_add', payload: count })
这里的处理比较复杂, 其实就是改造我们的 dispatch 函数
- action => params => (dispatch, getstate) => {
- const retDispatch = (namespace, payload) => {
- return dispatch({
- type: Namespace.get(namespace),
- payload
- })
- }
- return action(params)(retDispatch, getstate)
- }
总结
通过对 Redux Api 的改造, 相当于二次封装, 已经很大的简化了目前在项目中的样板代码, 并且在项目中很顺畅的使用.
针对整个过程, 其实还有几个可以改进的地方:
actions 的转化过程, 交由中间件处理
性能问题, 目前相当于多做了一层转化, 但是目前影响不大
来源: https://segmentfault.com/a/1190000015035012