资讯专栏INFORMATION COLUMN

我是如何一步步“改造”redux的

jemygraw / 1916人阅读

摘要:但是在使用开发的过程中还是感觉不太顺手,本文将阐述我是如何对进行一步步改造以适应个人和团队开发需求的。所以说,我们如何在保证的设计原则以及项目规范性上,对其进行简化改造,是我这里需要解决的问题。

从Vue换到React+Redux进行开发已经有半年多的时间,总的来说体验是很好的,对于各种逻辑和业务组件的抽象实在是方便的不行,高阶组件,洋葱模型等等给我带来了很多编程思想上的提升。但是在使用Redux开发的过程中还是感觉不太顺手,本文将阐述我是如何对Redux进行一步步“改造”以适应个人和团队开发需求的。
过程中的示例和结果放在了easy-redux,欢迎star。
原文链接

问题

在使用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请求样板代码, 通过封装了一个redux中间件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
, 对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的转化过程,交由中间件处理

性能问题,目前相当于多做了一层转化,但是目前影响不大

reducer,action复用

有兴趣的话,欢迎探讨~ 附上github easy-redux

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/95246.html

相关文章

  • 手挽手带你学React:四档(下篇)步学会react-redux

    摘要:手挽手带你学入门四档用人话教你,理解架构,以及运用在中。学完这一章,你就可以开始自己的项目了。结合搭建基础环境我们上一章讲过了的原理,内部是有一个的,只有才可以控制它变化。 手挽手带你学React入门四档,用人话教你react-redux,理解redux架构,以及运用在react中。学完这一章,你就可以开始自己的react项目了。 视频教程 上一篇我们自己实现了Redux,这一篇我们来...

    FullStackDeveloper 评论0 收藏0
  • 简单梳理Redux源码与运行机制

    摘要:然后循环调用中的更新函数,更新函数一般是我们的渲染函数,函数内部会调用来获取数据,所以页面会更新。前言 前几天写了一篇react另一个状态管理工具Unstated的源码解析。 开启了我的看源码之路。想一想用了好长时间的redux,但从没有深究过原理,遇到报错更是懵逼,所以就啃了一遍它的源码,写了这篇文章, 分享我对于它的理解。 API概览 看一下redux源码的index.js,看到了我们最...

    刘东 评论0 收藏0
  • 简单梳理Redux源码与运行机制

    摘要:然后循环调用中的更新函数,更新函数一般是我们的渲染函数,函数内部会调用来获取数据,所以页面会更新。 欢迎访问个人网站:https://www.neroht.com/ 前言 前几天写了一篇react另一个状态管理工具Unstated的源码解析。开启了我的看源码之路。想一想用了好长时间的redux,但从没有深究过原理,遇到报错更是懵逼,所以就啃了一遍它的源码,写了这篇文章,分享我对于它的理...

    betacat 评论0 收藏0
  • 3分钟教你写精炼 React Components

    摘要:最近在做一些梳理,把平时记录的一些笔记和实践整理成完整的短篇技术文章。下一步我们要处理的是样式。相比最初的版本多了一些代码,因为我们把一些关键逻辑拆分到了不同的组件中,干净并不意味着少。 最近在做一些梳理, 把平时记录的一些笔记和实践整理成完整的短篇技术文章。 这篇主要说一下如何精简你的React Components showImg(https://segmentfault.com...

    zhoutk 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<