阅读 357

春招面试复盘,拥抱redux数据流管理

在这里插入图片描述

一提到redux,你会想到什么

  • 数据状态管理
  • store action dispatch reducer (redux)
  • Provider Connect (react-redux)

redux确实里面涉及很多API,但是仅仅知道这些API是不够的的,要理解redux如何对项目中的数据流进行管理,才是真正要去学习的。

因此这篇文章,我就当是自己把这个redux理通一遍,从why(这个以前写过就不再重复了) what how 三个角度,尽量去讲通一下。可能有些地方写的并不是很全面,也可能有一些地方说的有些错误,欢迎评论区指正。

文章大体结构: 在这里插入图片描述

但是在这,我不会按一步步讲下来,有关why我想大家应该都大致了解它为什么要出来,为了更方便的管理数据,之前也写过文章围绕组件化,带你入坑Raect学习 【详】,里面讲了传统组件传值,以及为什么需要redux来进行数据管理,但是那时候还没有搞懂redux里面的具体操作。

现在也是借这个机会,把有关redux操作的流程理一遍。其中很多知识点都在过程中写到了。

先声明👇👇👇

在初体验篇部分,我会单单使用 redux 来理解里面的一些概念, 以一个简单的todolist 来举例。

在完善篇部分,我会使用 redux + react-redux(更方便的使用redux) + axios + redux-thunk 实现进行异步请求数据。

好的,废话不多说,开干吧。

先导篇 —— 掌握一些redux知识点

一些关键要素和工作流:

Store:一个单一的数据源(里面存放了state),注意它是只读的

Action:对变化的描述

Reducer:一个函数,负责对变化(也就是action)进行分发和处理,最终把最新的数据返回给Store

三者紧密配合,形成了Redux工作流: 在这里插入图片描述

从图中我们可以看到,redux中的数据流式单向的,数据发生改变的唯一途径就是:

在view视图层派发Action,Action会被我们的Reducer读取,然后Reducer根据Action内容的不同执行不同的计算(会存在很多action),最后得很话生成新的state,而这个state会更新到store对象中,又进一步驱动视图层做出对应的改变。

初体验篇 —— 透过TODOLIST小项目走一遍流程

通过一个todolist项目区更好的理解redux中这些api中的数据流动。

1、初始化

  1. 首先的话,我们使用npx create-react-app redux-todolist 创建一个新的项目,因为我们这边使用的是redux,因此我们安装好 yarn add redux
  2. 修改一个src下index.js的代码:
import React from 'react';
import ReactDOM from 'react-dom';
import TodoList from './TodoList'
ReactDOM.render(
  <TodoList />,
  document.getElementById('root')
);

复制代码
  1. 把原先的app.js删掉重新创建一个TodoList.js文件:
import React, { Component } from 'react';

class TodoList extends Component {
  constructor(props) {
    super(props);
    this.state = {  }
  }
  render() { 
    return ( 
      <div>
        hello jingda
      </div>
     );
  }
}
export default TodoList;
复制代码

在这里插入图片描述

运行此刻的代码,我们可以成功在页面输出hello jingda

让我们修改一下代码,让页面有一个input框和一个提交按钮,当然我们可以选择初始化一些list数据,

在这里插入图片描述

代码如下:

import React, { Component } from 'react';
class TodoList extends Component {
  constructor(props) {
    super(props);
    this.state = { 
      list:[
        '1',
        '2',
        '3'
      ]
     }
  }
  render() { 
    return ( 
      <div>
        <div>
          <input placeholder="请输入待办吧"></input>
          <button>提交</button>
        </div>
        <div>
          <ul>
            {
              this.state.list.map((item,index) => {
                return <li>{index} --- {item}</li>
              })
            }
          </ul>
        </div>
      </div>
     );
  }
}
 
export default TodoList;
复制代码

2、创建store 初始化数据

我们需要完成的功能就是,在输入框中写下自己的待办,然后按提交按钮,这个待办就到list中去了,之后便显示在页面上。

首先我们要创建一个store:

下面这样的目录结构,为什么store下面要index.js,和reducer.js两个文件呢,前面我们说到了,在当视图层派发一个action出来之后,使用由我们的reducer进行处理的,当它处理完后会把state传给store

在这里插入图片描述

因此这里面的代码如下:

store/index.js:在这个文件里面,我们引用了redux中的createStore创建一唯一的store,它接收了一个参数reducer,而这个reducer返回的就是处理完由视图层派发过来的action之后得到的state状态。

import { createStore } from 'redux'
import reducer from './reducer'
const store = createStore(reducer)
export default store;
复制代码

store/reducer.js:

const defaultState = {
inpuetValue:'请输入',
  list:[
    'jing1',
    'jing2'
  ]
}//默认数据
export default (state = defaultState,action) => {
  return state
}
复制代码

到这里,我们创建好了项目的store,也初始化了数据。让我们回到TodoList组件中去引用store

import store from './store'
复制代码

测试一下有没有引入:

在constructor方法中打印一下传过来的store里面的数据:

console.log(store.getState())
复制代码

在这里插入图片描述

3、触发action

可以看到组件已经可以拿到store里面的数据了,下面我们要做的就是:

1、将这些数据显示在页面上。

2、把输入的数据先保存。

3、当input输入数据,点击提交,list多出一条记录的功能。

4、点击list每条数据的删除按钮,实现删除功能。

把store的数据展示在页面上只需要修改一下之前的代码:

constructor(props) {
    super(props);
    // console.log(store.getState())
    this.state = store.getState()
  }
、、、、、

<ul>
       {
           this.state.list.map((item,index) => {
          return <li>
                  {index} --- {item} 
                  <button>删除</button>
                 </li>
        	})
        }
 </ul>
复制代码

同时我们要先保存一下输入的数据:

 <input
  placeholder={this.state.inpuetValue} 
onChange={this.onChangeValue.bind(this)
 </input>
复制代码

在这里插入图片描述

4、具体的redux数据流管理

后面三步都要经历这样的过程:创建action,并通过store.dispatch自动派发给reducer,reducer处理state数据,再把state传回给store。这里我就一起写了。

准备工作就是在提交和删除按钮上分别绑定一个事件

<button onClick={() => addInput()}>提交</button>
 <button onClick={(index) => this.deleteInput(index)}>删除</button>
复制代码

创建一个actionType.js文件,action的type类型值先定义一下:

export const CHANGE_INPUT = "CHANGE_INPUT"
export const ADD_INPUT = "ADD_INPUT"
export const DELETE_INPUT = "DELETE_INPUT"
复制代码
  1. 创建action,并派发
  onChangeValue(e) {
    console.log(e.target.value)
    const action = {
      type:CHANGE_INPUT,
      value:e.target.value
    }
    store.dispatch(action)
  }
  addInput() {
   
    const action = {
      type:ADD_INPUT,
    }
    store.dispatch(action)
  }
  deleteInput(index) {
    const action = {
      type:DELETE_INPUT,
      index
    }
    store.dispatch(action)
  }
复制代码
  1. reducer处理state
export default (state = defaultState,action) => {
  if(action.type === CHANGE_INPUT) {
    let newState = JSON.parse(JSON.stringify(state)) //深度拷贝state
    newState.inpuetValue =  action.value//对应组件传过来的index
    //console.log(newState.value)
    return newState
  }
  if(action.type === ADD_INPUT) {
    let newState = JSON.parse(JSON.stringify(state)) //深度拷贝state
    newState.list.push(action.inpuetValue) //对应我们组件传过来的value
    return newState
  }
  if(action.type === DELETE_INPUT) {
    let newState = JSON.parse(JSON.stringify(state)) //深度拷贝state
    newState.list.splice(action.index,1) //对应组件传过来的index
    return newState
  }
  return state
}
复制代码

到这里我们似乎已经完成了前面说到的那些步骤,但是此时去页面上添加数据,是不能成功的,我们还需要完成一个操作-订阅Redux的状态

 this.storeChange = this.storeChange.bind(this)
 store.subscribe(this.storeChange)//订阅redux的状态
复制代码
storeChange() {
    this.setState(store.getState())
}
复制代码

好啦,基本功能就实现啦,稍微整理一下代码。我们可以发现在组件页面上,每次有个事件我们就要创建一个action,每次修改我们要去组件里面翻,现在让我们把这些个action分离出来,单独写在一个文件里面,更加有助于管理。

在store/actionCreatores.js里面:

import { CHANGE_INPUT,ADD_INPUT,DELETE_INPUT } from './actionType'
export const changeInputAction = (value) => ({
  type:CHANGE_INPUT,
  value
})
export const addInputAction = () => ({
  type:ADD_INPUT,
})
export const deleteInputAction = (index) => ({
  type:DELETE_INPUT,
  index
})
复制代码

修改组件页面的方法代码:

 onChangeValue(e) {
    console.log(e.target.value)
    const action = changeInputAction(e.target.value)
    store.dispatch(action)
  }
  addInput() {
    const action = addInputAction()
    store.dispatch(action)
  }
  deleteInput(index) {
    const action = deleteInputAction(index)
    store.dispatch(action)
  }
复制代码

5、还存在一些问题

一个简单的todolist到这里就完成了。稍微理了一下redux中的数据流向。也把一些文件稍微整理了一下,但还没有完哦。虽然能够正确的实现功能,但项目中我们可能需要的更多,比如涉及下面的一些问题:

  1. 使用redux还要订阅之类的,有些复杂,react-redux又是如何去简化操作?
  2. 在reducer里面我使用的是深拷贝创建一个新的数据,但深拷贝本身是耗性能的,如何解决,immutable如何实现不可变数据?
  3. 上面我只有一个组件页面,因此只用到一个reducer,如果有多个组件呢?state如何管理,redux又是如何解决,我们又如何设计目录结构去更好管理?
  4. 当使用axios数据请求时?异步操作如何处理,redux-thunk如何使用呢?........

还有一些其他的知识点,让我们开启下一个步吧:

完善篇 —— react-redux +redux-thunk 如何处理数据流

在上面的todolist小项目中,我想你已经大概了解了redux的使用基本流程,在这一步,我会把更多的篇幅写在结构化模块化,以及使用react-redux的一些新的知识上。

下面是一些要用到的API或知识点,稍微介绍一下,下面使用会更清楚一些:

1、涉及的知识点介绍

  • react-redux

在使用react-redux的时候,组件分成了两大类,UI(负责UI呈现)和容器组件(负责管理数据和逻辑)

  • Connect

react-redux提供的一个方法,用于从UI组件中生成容器组件。

为什么需要从UI组件中生成?因为如果一个组件既有UI又有逻辑,那么我们会把它拆分成外层是容器组件负责逻辑,只要把数据传给它就好了,内层是UI组件接收外层传过来的数据,实现视图渲染。

connect方法有两个参数 mapStateToProps: 1、负责输入逻辑,将state映射到UI组件的参数(props上) 2、mapStateToProps会订阅Store,没带state更新,就会自动执行,重新计算UI组件的参数,从而触发UI组件的重新渲染。 mapDispatchToProps: 1、负责输出逻辑,将用户对UI组件的操作映射成Action 2、定义了那些操作应该当作Action,传给store

  • Provider
<Provider store={store}>
	<App />
<Provider/>
复制代码

当我们用connect方法生成容器组件后,需要让组件拿到state对象,才能生成UI组件的参数。选择在根组件外面包一层,这样子组件就可以默认拿到state

  • redux-thunk

实现异步操作的中间件,写一个返回函数的Action creator,然后使用redux-thunk中间件改造store.dispatch (具体的使用下面会提到)

  • immutable

持久化数据,一旦被创建就不会被更改,修改immutable对象的时候返回新的immutable,但是原数据不会更改。我们知道在reducer里面,数据是不能直接变更的,这里可以使用immutable

2、主流程

  1. 在我们的3001 端口下是前端项目 react,在3000端口下是一个接口(自己用express搭的,勿喷。完全是为了效果),思考一下前端如何向后端发起请求,把数据显示到页面上?
  2. 数据请求过来了放哪?组件里面吗?还是收归Store 统一管理?
  3. axios 请求涉及异步,写这样的异步Action怎么写?

好吧,不多说啦,开始吧

(里面涉及的依赖请自行安装哦 axios redux react-redux redux-thunk immutable)

从项目结构开始:

在这里插入图片描述

  1. api负责数据请求
  2. pages负责组件管理,每个组件都有自己的store文件夹
  3. store这是一个全局的store,掌管了全部组件的state,和reducer

假设我们在做一个商品信息shoplist的展示, 刚开始为空,然后我需要从后端把他的信息请求过来,并且把shopList的值修改掉

2.1、数据请求:

在这里插入图片描述

  • redux-thunk使用:
import {createStore,applyMiddleware} from 'redux'
import thunk from 'redux-thunk' //异步处理
import reducer from './reducer'
const store = createStore(reducer,applyMiddleware(thunk))
export default store
复制代码
  • api/config.js
//axiox 请求的封装
import axios from 'axios';
//1、设置统一的地址范围
export const baseUrl = 'http://localhost:3000/shop';
//2、返回一个axios 实例
const axiosInstance = axios.create({
  baseURL:baseUrl
});
export {
  axiosInstance
}
复制代码

api/request.js

import {axiosInstance} from "./config";
//所以的请求都在这里统一管理
//axios 
//url 如果改了怎么办?
//首页 
export const getShopListRequest = id => {
  return axiosInstance.get('/shop')
}
复制代码

2.2、触发action :

当涉及异步请求的时候,我们需要设计两个action,一个用来同步修改action的值,一个用来发请求,返回一个函数,这个函数也具备dispatch的能力。

export const shopList = data => ({
  type:SHOP_LIST,
  data
})
//api 两个action axios 只在action
export const getShopList = () => {
  return dispatch => {
    getShopListRequest()
    .then(res => {
      console.log(res)
      dispatch(shopList(res));
    })
  }
}
复制代码

2.3、组件 接收处理:

在App.js中使用Provider:

<Provider store={store}>
    <Shop />
</Provider>
复制代码

在Shop组件中使用connect:

const Shop = (props) => {
    const { shoplist,shopListDispatch } = props
    console.log(shoplist)
    useEffect(() => {
        shopListDispatch();
    },[])
    return(
        <div></div>
    )
}
const mapStateToProps = state => ({
    shoplist: state.getIn(["shoplist"])
   
})
const mapDispatchToProps = dispatch => {
    return{
        shopListDispatch() {
          dispatch(getShopList())
        },
    }
}
export default connect(mapStateToProps,mapDispatchToProps)(Shop);
复制代码

2.4、reducer的处理与整合

  • reducer处理action:
const defaultStatus=fromJS({
   shopList:[]
})
export default (state=defaultStatus,action)=>{
    switch(action){
        case actionType.SHOP_LIST:
            return state.set('shopList',action.data);
        default:
            return state
    }
}
复制代码
  • 多个reducer怎么整合:
import {combineReducers} from 'redux-immutable'  //不可变的状态
import {reducer as userReducer} from '../pages/Shop/store'
// import {reducer as rankReducer} from '../pages/Food/store'

export default combineReducers({
    shop:shopReducer,
    // food:foodReducer
})
复制代码

到这里,理一下几个步骤:(非全部)

  1. 最开始的shoplist数据为空
  2. 在actionCreators.js里面创建了两个action,一个是同步的,一个是异步的
  3. 在异步的这个action里面return了一个函数,这个函数有一个dispatch参数,并且执行了,发起异步请求的 getShopListRequest()方法,
  4. 然后我们通过请求http://localhost:3000/shop/shop, 得到了下面的数据:

在这里插入图片描述 5. 在reducer.js里面可以接收action,可以打印一下我们的action:

在这里插入图片描述

总结

先总结一波:

在前面做todolist的项目的时候,我们说到几个点,现在来回答一下:

  • react-redux 使用了Provider 和Connect 把组件分成容器组件和UI组件,其中Connect的第一个参数为我们实现了订阅功能,因此我们可以不用再自己写订阅Redux的状态.
  • 在写reducer的时候,我们不使用深拷贝,而是使用了immutable不可变数据,获取你应该去了解一些API,像,.fromJS,.setIn,.getIn...
  • 为了更好的去管理若干个组件的数据,通过combineReducers 把组件的reducer整合起来。
  • 异步请求,在actionCreators内部编写逻辑,处理请求结果。而不只是单纯的返回一个action对象。(当然在此之前,你得先去创建store的地方引用一波)

个人觉得,第一个todolist小项目是理解redux数据流向比较好的例子,也是比较容易理解。但是在项目中肯定不是就单单那么简单,react-redux中使用的Provider Connect 可能更加适合功能比较复杂的项目,但前提也是需要一些成本。不过现在很多都是用的这个。所以我们也要了解,并且熟悉它们的用法。

最重要的中间的目录结构,我觉得我没有讲的很清楚,也许你可以拿一个你做的项目看,里面涉及的redux管理可能是应该考虑到了。

好啦,redux有关的,我就梳理到这里了,可能自己理的还是有些复杂,因为全程大部分是以项目为主展开。

  • 最后是自己实现的小白接口:

image.png

因为后端前端分别在:3000,:3001,中间加了一些简单的解决跨域的代码。

server.js:

const express = require('express');
const shopRouter = require('./shop');

const app = express();

app.all('*', (req, res, next) => {
  res.header("Access-Control-Allow-Origin", "*");
  res.header("Access-Control-Allow-Headers", "X-Requested-With");
  res.header("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS");
  res.header("X-Powered-By", 'Express');
  res.header("Content-Type", "application/json;charset=utf-8");
  next();
});


app.use('/shop',shopRouter);

app.listen(3000,() => {
  `server is running at port 8000 success`
})

复制代码

shop.js:

const express = require('express')

const Router = express.Router();

Router.get('/shop',(req,res) => {
  return res.json(
    {shoplist:[{
      id:111,
      name:'cloth1',
      price:123
    },
    {
      id:112,
      name:'cloth2',
      price:345
    }]
    }
    )
})

module.exports = Router
复制代码

我是婧大,一个大三学崽,目前正在准备实习面试。

这段时间应该主要是理理一些知识点,欢迎一起学习。wx:lj18379991972 💕💕💕

你的点赞是给我最大的支持🤞

(后面那部分,我感觉自己好像理解的并不是很好,如果你是大佬,麻烦看到错的地方指点以下我这个白菜。如果你也不是很懂,希望通过这个文章,知道你自己可能还需要去了解的东西。可能是我实践的也少,对问题看待的深度和广度也是有欠缺。会继续学习的。。)

在这里插入图片描述

参考文档:

技术胖 Redux免费视频

阮一峰 Redux 入门教程

Redux 中文文档

文章分类
前端
文章标签