React全家桶之Redux使用

2019-07-18 17:17:21 浏览数 (1)

使用redux

让我们闭上眼睛想想,如果用一个词描述React 和Redux 给我们留下了什么印象,我想到的不是难学,不是繁琐,而是“限制”。

“限制”在这里绝不是贬义词,恰恰相反,是对技术框架的最高夸奖,因为限制能够确保程序按照可控的方式进化。

在计算机软件世界里,造物主就是人类自己,没有物理化学的限制,一切皆有可能。也正因为一切皆有可能,一个问题即使没有无数种解法,也会有很多很多种解法。

但是,拥有很多方案并不表示我们应该使用所有的方案。

软件要由程序员来维护和开发,研发部门管理也是程序员。而程序员是人,不是机器。当负担多个开发任务的时候,牵一发而动全身,bug 层出不穷,即使最专业的程序员,我想也会丧失勇气吧。

React和Redux技术框架最大的好处,并不是让我们无所不能,而是设定了一规矩,让每个模块只做最单一的事情。让开发者只能按照这套规矩来完成代码。这样,只要理解了这套规矩,无论产生的代码由谁来维护由谁来继续开发都不会有大问题。

redux其实借鉴与flux的思想,它是单向数据流的最佳实践(也许吧)。

和vuex的区别: 没有getters和actions,仅仅关注状态的变更。更加纯粹(dispatch),vuex包括dispatch和commit。 而且redux的dispatch是同步操作。redux并非react独有,适用范围非常广。但vuex高度依赖于vue。

本文将基于上一讲的水果购物车(Hook.js)继续开发。再次展示一段代码重构的过程。

源代码的注释里阐述了redux三大原则:

代码语言:javascript复制
* Creates a Redux store that holds the state tree.
 * The only way to change the data in the store is to call `dispatch()` on it.
 * There should only be a single store in your app. To specify how different
 * parts of the state tree respond to actions, you may combine several
 * reducers

应用中所有的 state 都以一个对象树的形式储存在一个单一的 store 中。 惟一改变 state 的办法是触发 action,一个描述发生什么的对象。 为了描述 action 如何改变 state 树,你需要编写 reducers。

安装:

代码语言:javascript复制
npm i redux react-redux -S

在react下,还需要创建reac相关依赖

代码语言:javascript复制
npm install --save react-redux
npm install --save-dev redux-devtools

创建 store实例,在根组件比如 App.js中注册store,通过上下文(react-redux提供的Provider)的方式注入进去。

创建store实例:createStore

createState创建了状态并储存。全局应用中只能有一个。创建一个 store.js

store同时必须对应一个 reducer函数:他接收两个参数,分别是当前的 state 树和要处理的 action,返回新的 state 树。

代码语言:javascript复制
import {createStore} from 'redux'

function fruitReducer(state={
    list:[]
}, action) {
    switch (action.type) {
        case "init": // 初始化fruits
            return {state,list:action.payload}
        case "add": // 新增
            return {...state, list:[...state.list,action.payload]};
        default:
            return state;
    }
}

//创建一个全局实例
const store = createStore(fruitReducer)
export default store

Store的返回值: 保存了应用所有 state 的对象。改变 state 的惟一方法是 dispatch action。你也可以 subscribe 监听 state 的变化,然后更新 UI。

代码语言:javascript复制
// 可以手动订阅更新,也可以事件绑定到视图层。
store.subscribe(() =>
  console.log(store.getState())
);
注册provider

provider是为组件提供上下文环境。在根组件中

代码语言:javascript复制
import { Provider } from "react-redux";
import store from "./store";
import ReduxTest from "./ReduxTest";
...
<Provider store={store}>
  <ReduxTest />
</Provider>

#### 使用状态映射(connect)

store的状态,如何正确反映到组件中,dispatch如何调用?这需要react-redux提供的另外一个函数:connect

代码语言:javascript复制
connect(state=>({
    fruits:state.list,
}))(原来的函数组件)

原来的函数组件,映射出来,自动带上了各种状态,包括dispatch方法。(接收的这两个参数)

代码语言:javascript复制
import React, { useState } from 'react';
import { useEffect } from "react";

import { connect } from 'react-redux'

// 水果列表
function FruitList({ fruits, setFruit }) {
    let result = fruits.map(f => <li onClick={() => { setFruit(f) }} key={f}>{f}</li>)
    return result;
}

// 添加水果
function AddFruit({ onAddFruit, fruits }) {
    return (
        <input type='text' onKeyUp={(e) => {
            if (e.keyCode == 13) {
                onAddFruit(e.target.value)
                e.target.value = '';
            }
            e.persist()
        }} />
    )
}


export default  connect(state=>({
    fruits:state.list,
}))(function HooksTest({fruits,dispatch}) {
    console.log(1,fruits)
    const [fruit, setFruit] = useState("草莓");
    // 全局属性
    useEffect(() => {
        setTimeout(() => {
            // 变更状态,提交
            dispatch({ type: "init", payload: ["香蕉", "西瓜"] });
        }, 1000);
    }, []);
    return (

        <div>
            <p>{fruit === "" ? "请选择喜爱的水果:" : `您的选择是:${fruit}`}</p>
            <AddFruit onAddFruit={pname => dispatch({ type: 'add', payload: pname })} fruits={fruits}></AddFruit>
            <FruitList setFruit={setFruit} fruits={fruits}></FruitList>
        </div>
    );
})

如果子组件也需要,就再创建一个connect。即可。

重构

当前代码很不友好,应该重构一下。

重点思考,connect的两个参数是什么含义?

在组件中dispatch操作(add,init)会造成很大的耦合。如果能结构到组件的参数中,就好了。

首先用一个语义化的函数名指代第一个参数:

代码语言:javascript复制
//给包装的组件传属性
const mapStateToProps=state=>({
    fruits:state.list,
})

第二个参数本质上是一个actionCreater。它是一个对象,声称了了你想定义的action操作。

代码语言:javascript复制
// store.js
// action-creater
export const init=(payload)=>({
    type:'init',
    payload
})

export const addFruit=(payload)=>({
    type:'add',
    payload
})

// 组件.js
import {addFruit,init} from '../store'
const mapDispatchToProps={
    init,addFruit
}

export default  connect(mapStateToProps,mapDispatchToProps)(function HooksTest({fruits,init,addFruit}) {
    const [fruit, setFruit] = useState("草莓");
    useEffect(() => {
        setTimeout(() => {
            init(["香蕉", "西瓜"])
        }, 1000);
    }, []);
    return (

        <div>
            <p>{fruit === "" ? "请选择喜爱的水果:" : `您的选择是:${fruit}`}</p>
            <AddFruit onAddFruit={(payload)=>addFruit(payload)} fruits={fruits}></AddFruit>
            <FruitList setFruit={setFruit} fruits={fruits}></FruitList>
        </div>
    );
})

改完之后的代码语意清晰,春风拂面,不用注视即可看懂,简直是redux操作中的一股清流

异步处理

redux是不支持异步的。如果需要异步,需装中间件。redux中间件概念:

派发的action本来是直接到中间件中的。但经过中间件(强化器)处理后,可以做异步操作,或者打日志

  1. 安装redux-thunk和redux-logger: npm i redux-thunk redux-logger-S
  2. 应用中间件,store.js 中
代码语言:javascript复制
import { createStore, applyMiddleware } from "redux";
import logger from "redux-logger";
import thunk from "redux-thunk";
// 对store应用中间件,注意有先后顺序
const store = createStore(fruitReducer, applyMiddleware(logger, thunk));
  1. 定义异步动作
代码语言:javascript复制
// store
// 在把异步请求的动作放到一个异步操作中。
export const asyncFetch = (payload) => {
  return dispatch => {
    setTimeout(() => {
                dispatch({type:'init', payload: ["草莓", "香蕉"]}); }, 1000);
  }; 
};
  1. 使用(hook.js)

那么原来的init都可以不要了。

代码语言:javascript复制
import {addFruit,asyncFetch} from '../store'
const mapDispatchToProps = {
    asyncFetch,
    addFruit
};

function HooksTest({fruits,addFruit,asyncFetch}) {
    const [fruit, setFruit] = useState("草莓");
    // 全局属性
    useEffect(() => {
        asyncFetch(["草莓", "香蕉"] )
    }, []);
    return (
        <div>
            <p>{fruit === "" ? "请选择喜爱的水果:" : `您的选择是:${fruit}`}</p>
            <AddFruit onAddFruit={(payload)=>addFruit(payload)} fruits={fruits}></AddFruit>
            <FruitList setFruit={setFruit} fruits={fruits}></FruitList>
        </div>
    );
}

export default  connect(mapStateToProps,mapDispatchToProps)(HooksTest)

你用类似同步的写法,轻易地写出了功能,异步操作的日志也被轻易地打印出来了。

模块化(combineReducers)

当前尽管hook.js已经非常好读,但是store还是一团糟。应该考虑把hook相关的逻辑(reducer)从是store中分离。

首先,在store文件夹下新建一个 fruitReducer.js,把无关store本身的业务逻辑

代码语言:javascript复制
// 把action和reducer移至fruit.redux.js
// 导出异步操作
export const asyncFetch = (payload) => {
    return dispatch => {
        setTimeout(() => {
            dispatch({ type: 'init', payload});
        }, 1000);
    };
};

// action-creater
export const init = (payload) => ({
    type: 'init',
    payload
})

export const addFruit = (payload) => ({
    type: 'add',
    payload
})

export default function(state = {
    list: []
}, action) {
    switch (action.type) {
        case "init": // 初始化fruits
            return { state, list: action.payload }
        case "add": // 新增
            return { ...state, list: [...state.list, action.payload] };
        default:
            return state;
    }
}

把combineReducers引入,并作为createStore的第一个参数

代码语言:javascript复制
// store/index.js
import { combineReducers } from "redux";
import fruitReducer from './fruitReducer';
const store = createStore(
  // 此处设置别名`fruit`
  combineReducers({ fruit: fruitReducer }),
  applyMiddleware(logger, thunk)
);

这时候,在组件Hook.js中,可以以 state.fruit调用状态:

代码语言:javascript复制
// ReduxTest.js
import {addFruit,asyncFetch} from "../store/fruitReducer";
const mapStateToProps = state => ({
  fruits: state.fruit.list
});

0 人点赞