深圳幻海软件技术有限公司 欢迎您!

《彻底掌握Redux》之开发一个任务管理平台

2023-02-28

前言redux是上手react开发的必经之路,也是目前react项目中使用的最流行状态管理库。虽然我们不使用redux也可以通过react的state和父子props进行基本的数据通信和项目开发,但是对于一个大型项目而言,往往考虑的更多的是代码结构和组件之间的通信,我们需要一种很优雅且有利于扩展的方

前言

redux是上手react开发的必经之路,也是目前react项目中使用的最流行状态管理库。虽然我们不使用redux也可以通过react的state和父子props进行基本的数据通信和项目开发,但是对于一个大型项目而言,往往考虑的更多的是代码结构和组件之间的通信,我们需要一种很优雅且有利于扩展的方式去开发我们的复杂系统,所以这种情况下使用redux是最佳的选择。

由于之前有朋友希望快速上手一下redux开发,所以笔者特意开发了一个小项目,希望通过这个项目可以让大家快速掌握redux及其生态的使用方式,以便今后在技术选型上有更多的空间。

你将收获

  • redux的工作机制和基本概念。
  • redux的使用模式。
  • redux相关生态的使用(react-redux, keymirror, reduce-reducers)。
  • 异步action解决方案redux-thunk。
  • 项目技术选型和架构。
  • 基于react实现一个可用的任务管理平台。
  • 如何实现自己的js工具库。

正文

1、redux的工作机制和基本概念

以上是笔者画的一个草图,描述了redux的数据流转机制。首先是用户触发action(在代码层面只有dispatch才能触发action),这时store会自动调用reducer函数并传入上一个状态的state和action,reducer函数会返回一个新的state,这个时候store会监听state的变化并调用监听函数,此时我们的react组件就会重新渲染并生成新的view。

redux的设计思想核心就是把web应用当作一个状态机,视图和状态一一对应,所有的状态都保存在一个对象里。

由上图可以看出redux几个核心api就是

  • store 保存数据的容器。
  • state 某个时刻store的快照。
  • action 标识当前要执行的动作。 action是改变 State 的唯一方式。
  • dispatch 执行action的唯一方式。
  • reducer 计算并生成一个新state的方式。

我们只要理清它们的关系和工作机制,redux也就能轻松使用了。

2、redux的使用模式

redux的基本工作流程熟悉之后,我们来看看如何将redux运用在项目中。以下是使用redux的基本步骤,大家可以参考一下:

  1. 定义初始化的state。
  2. 定义action。
  3. 编写reducer函数。
  4. 使用dispatch触发action。

基本代码如下:

// 1. 定义初始化的state
const initSate = {
    num: 0
}
// 2. 定义action
function add() {
    return {
        type: 'INCREMENT'
    }
}
function dec() {
    return {
        type: 'DECREMENT'
    }
}
// 3. 编写reducer函数
const reducer = (state = initState, action) => {
  switch (action.type) {
    case 'INCREMENT': return {...state, {num: state.num + 1}}
    case 'DECREMENT': return {...state, {num: state.num - 1}}
    default: return state;
  }
}
// 创建store
const store = createStore(reducer)
// 4. 使用dispatch触发action
const renderView = () => {
  ReactDOM.render(
    <YourComponent
      value={store.getState()}
      add={() => store.dispatch(add())}
      dec={() => store.dispatch(dec())}
    />,
    document.getElementById('root')
  );
};
renderView();
store.subscribe(renderView);
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.

通过以上的步骤我们就可以基本开始redux开发了,redux还提供了中间件机制,暴露了applyMiddleware, compose等API,这里我们先简单提一下,后续会涉及到相关的使用。

实际项目中我们往往不会直接使用redux,我们会搭配使用react-redux等库,通过将react和redux以更优雅的方式结合到一起来开发更加可维护的项目。

3、redux相关生态的使用(react-redux, keymirror, reduce-reducers)

(1)react-redux

react-redux的核心思想是将所有组件分成渲染组件(纯组件)和容器组件(负责处理业务逻辑和状态),渲染组件只负责展示,没有状态,容器组件负责处理各种状态和逻辑。所以用户只需要提供渲染组件来呈现视图,容器组件会由react-redux自动生成。所以整个过程看上去像这样:

我们来看看如何使用react-redux。首先它提供了connect方法用于从 UI 组件生成容器组件,并将UI组件和容器组件连接在一起,具体用法如下:

import { connect } from 'react-redux'
const HomeContainer = connect(
  mapStateToProps,
  mapDispatchToProps
)(Home)
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

home是我们的UI组件,通过mapStateToProps, mapDispatchToProps这两个函数参数,我们可以将redux的store和action映射到UI组件的props上,这样我们就可以实现正常的数据单向流转。

mapStateToProps的作用就是建立一个从(外部的)state对象到(UI 组件的)props对象的映射关系,我们一般可以这么定义:

const mapStateToProps = (state) => {
    let { capacity } = state
    return { capacity }
}
  • 1.
  • 2.
  • 3.
  • 4.

返回的capacity就是我们要传给某个UI组件的props里的某个属性。

mapDispatchToProps用来建立 UI 组件的参数到store.dispatch方法的映射,我们可以这么定义它:

const mapDispatchToProps = dispatch => {
    return {
      createTodo(data, cb) {
        dispatch(createTodo(data, cb))
      },
      editTodo(data, cb) {
        dispatch(editTodo(data, cb))
      }
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.

那么用户就可以在props中拿到createTodo,editTodo这两个方法来触发store对应的action。

当然如果只使用以上几种方式我们还是不能将state传递给容器组件,我们需要react-redux提供的Provider组件,它可以让容器组件拿到state。

import { Provider } from 'react-redux'
// ...
render(
  <Provider store={store}>
    <Container />
  </Provider>,
  document.getElementById('root')
)
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

所以完整的用法应该长这样:

// Container.jsx
import React from 'react'
import { connect } from 'react-redux'
import { createTodo, editTodo } from 'store/actions'
const mapStateToProps = (state) => {
    let { capacity } = state
    return { capacity }
}
const mapDispatchToProps = dispatch => {
    return {
      createTodo(data, cb) {
        dispatch(createTodo(data, cb))
      },
      editTodo(data, cb) {
        dispatch(editTodo(data, cb))
      }
    }
}
class Home extends React.Component {
  // ...
}
export default connect(mapStateToProps, mapDispatchToProps)(Home)
// index.js
import Container from './Container'
import { Provider } from 'react-redux'
// ...
render(
  <Provider store={store}>
    <Container />
  </Provider>,
  document.getElementById('root')
)
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.

(2)keymirror

keymirror这个库不是必选项,它主要是用来生成 key == value 结构的,比如我们定义了一堆action类型:

const actionType = {
    A: null,
    B: null,
    C: null
}
// => keymirror
console.log(keymirror(actionType))
// ==>
 {
    A: 'A',
    B: 'B',
    C:'C'
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.

通过keymirror我们就可以生成key == valuue这样的结构了,有人可能会说这是多此一举?其实它可以帮助我们优化代码,谷歌的Closure Compiler有一种编译模式叫最优处理( Advanced ),会将 Map<K, V> 格式的 K 进行压缩,比如说以下代码:

const Type = {
    ASVANCED: null
}
// 经过编译后
const Type = {a: null}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

这样会使得我们的代码体积更小,执行的更快。关于actionType的定义方式,接下来我会继续介绍。

(3)reduce-reducers

reduce-reducers主要是用来拆分reducer用的。想想如果我们的项目变得庞大而复杂起来了,要处理的状态非常多,那么我们都写在一个reducer里是非常不优雅且不利于维护的,如下代码所示:

const reducer = (state, action) => {
switch(action.type){
case actionType.CHECK_FAIL_TODO: 
// ...
case actionType.CHECK: 
// ...
case actionType.GET_FAIL_TODO: 
// ...
case actionType.GET_NUM: 
// ...
case actionType.COMPUTE_MONEY: 
// ...
//...
default:
return state;
  }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.

我们把不同业务场景的reducer都写在一起,后期往往很难管理和维护,我们期望将不同业务场景下的reducer进行划分,放到特定的reducer中,如下:

// reducerA.js
const reducerA = (state, action) => {
switch(action.type){
case actionType.CHECK_FAIL_TODO: 
// ...
case actionType.CHECK: 
//...
default:
return state;
  }
}
// reducerB.js
const reducerB = (state, action) => {
switch(action.type){
case actionType.CHECK_FAIL_TODO: 
// ...
case actionType.CHECK: 
//...
default:
return state;
  }
}
// reducerC.js
// ...
// 合并
reducer( reducerA, reducerB, reducerC)
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.

往往这种方式是更利于管理和维护的,我们使用reduce-reducers可以很好的实现这一点,具体用法如下:

import createTodoReducer from './createTodoReducer'
import editTodoReducer from './editTodoReducer'
import doneTodoReducer from './doneTodoReducer'
import delTodoReducer from './delTodoReducer'
import checkFailTodoReducer from './checkFailTodoReducer'
// 将多个reducer合并成一个,并将state合并
import reduceReducers from 'reduce-reducers'
const initialState = {
 capacity: 5 * 1024 * 1024,  // localStorage总容量,单位bt
 curCapacity: 0, // 当前使用容量, 单位bt
 hasDoneTodos: [], // 已经完成的todo
 hasFailTodos: [], // 已经失败的todo
 unDoneTodos: [], // 未完成的todo
 // todo创建
 ctLoading: false,
 ctErrorMes: ''
}
const reducer = reduceReducers(
 (state = initialState, action) => createTodoReducer(state, action),
 (state = initialState, action) => editTodoReducer(state, action),
 (state = initialState, action) => doneTodoReducer(state, action),
 (state = initialState, action) => delTodoReducer(state, action),
 (state = initialState, action) => checkFailTodoReducer(state, action)
);
export default reducer
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.

4、异步action解决方案redux-thunk

在了解异步action之前我想先来聊聊redux的中间件机制。类似于koa的中间件,redux同样也支持中间件,并且提供了使用中间件的API,其实原理就是重写action的派发过程,即重写dispatch。关于具体如何写一个中间件,这里不会详细介绍,我们主要来说说如何使用redux的中间件机制。

redux提供的applyMiddleware, createStore这两个API,就是我们使用中间件的关键。我们在使用中间件时要把中间件传入applyMiddleware函数中,并将applyMiddleware作为createStore的最后一个参数,具体用法如下:

const store = createStore(
  reducer,
  initial_state,
  applyMiddleware(thunk)
);
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

值得注意的是中间件的使用顺序要注意,一定要按照官方的规则和具体业务的顺序来排列中间件。

接下来我们看看异步action。使用异步action的基本模式如下:

我们在异步开始时,成功时,失败时都会派发一个action,来通知用户操作的状态。我们可以想到的是在请求拿到结果之后派发成功/失败的action,一共有两种方式实现如上步骤:

  1. 在业务代码中的请求回调中触发同步的action。
  2. 使用异步action 对于简单应用我们完全可以采用第一种方式来做,也就不需要异步action了,但是每次异步请求时都手动调用两个action未免太粗鲁了,所以对项目温柔以待的最佳方式就是使用异步action。

异步action本质上是返回一个函数,在函数里面执行相关操作,但是普通的action返回的是一个对象,那么如何去处理呢?想一想我们上面介绍的redux中间件机制,我们可以重写dispatch呀,的确,redux-thunk的源码就是对dispatch进行了加工,返回了一个高阶函数,具体源码就不带大家细读了,redux-thunk源码非常简单,十几行代码,非常值得借鉴。下面教大家如何使用redux-thunk:

import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import reducer from './reducers';
const store = createStore(
  reducer,
  applyMiddleware(thunk)
)
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.

这样我们就可以愉快的编写异步action啦,异步action的伪代码如下:

export default (data, cb) => {
return (dispatch, getState) => {
        dispatch(Actions.start());
  delay(0.5).then(() => {
    dispatch(Actions.ok(data, cb));
  }).catch(error => dispatch(Actions.fail(error || '创建失败', cb)))
  }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

5、项目技术选型和架构

之前介绍的一大堆都是redux的核心和生态使用方法,也是开发项目的基础,接下啦我们通过一个实际项目,来带大家彻底掌握redux开发。我们的任务管理平台采用SPA模式开发,脚手架采用笔者自己搭建的webpack,代码可以兼容到ie9. 目录结构如下:

大家在实际项目开发中也可以按照自己团队的风格,根据项目体量来量身定制自己的项目结构。store就是存放我们redux工作流的地方,也是整个状态的管理中心。UI库笔者采用自己开发的XUI组件库,目前已迭代了5个版本,后续会继续优化。具体参考地址如下:xui——基于react的轻量级UI组件库。

6、基于react实现一个可用的任务管理平台

实现后的截图如下:

我们通过这个任务管理平台,可以实现:

  • 创建任务
  • 编辑修改任务
  • 删除任务
  • 任务到期自动提醒⏰
  • 任务效率分析
  • 任务记录
  • 空间占用分析

创建任务:

操作任务:

任务效率分析,任务记录,空间占用分析。

项目体验地址:XOA任务管理平台。

根据以上总结的redux知识点,我们已经可以开发出如上的任务管理平台了,下一篇文章将具体介绍如何实现这样一个平台以及开发的注意事项和部署相关的知识,感兴趣的朋友可以查看demo尝试研究一下。

​​​​

本文转载自微信公众号「趣谈前端」,可以通过以下二维码关注。转载本文请联系趣谈前端公众号。