Redux 中间件和异步操作

  回顾一下Redux的数据流转,用户点击按钮发送了一个action,  reducer 就根据action 和以前的state 计算出了新的state, store.subscribe 方法的回调函数中 store.getState() 获取新的state, 把state 注入到页面元素中,实现页面状态的更新。你发现根本就没有机会去做一个异步的操作,但现实世界中又有大量的异步操作,那怎么办? 这就用到了Redux的中间件。

  中间件,说白了,就是中间的部分,正常的流程中,先执行函数a,再执行 b 函数,但如果在a 和b之间插入了中间件,那么流程就变成了,先执行a 函数,再执行中间件,最后执行b函数, 如果中间件在执行的过程中出错了,那b 函数就没有机会执行了,可以看到中间件的出现,阻碍a 到b 的顺序执行,那么这时候,我们就可以在中间件中作一系的事情,比如日志的记录,异步处理。具体到Redux中就是,在正常的Redux流程 中,发送一个action, 它立刻会到reducer中,那么我们就要在发送action 和进入reducer 之间插入中间件,阻止这种顺序的操作,我们在这些中间件中作异步操作等等。怎样插入中间件呢? Redux 提供了一个applyMiddleware 函数,把要使用的中间件作为参数传递给它就好了,  applyMiddleware 函数在createStore的时候调用,使用到的中间件是redux-thunk, 用于发送异步请求。为了更好使用中间件,纯html 方式不太好演示,这里简配置一个webpack,  只使用webapck-dev-server 就可以了。

  在任意的文件夹中打开命令行工具(git bash),  mkdir redux-middleware && cd redux-middleware && npm init -y  快速创建项目。npm i webpack webpack-dev-server webpack-cli -D 安装webpack-dev-server. webpack4 提供了零配置,默认的入口文件是src目录下的index.js, 要创建src目录和它下面的index.js。 由于webpack-dev-server 是把文件打包到内存中,打包后的文件放到根目录下面的main.js, 所以main.js 不用建,不过要建一个html 文件引入它们, 最后npm install redux redux-thunk -S.

  redux-thunk 作为异步请求的中间件,使用方式非常简单,当dispatch 一个函数的时候,它就执行这个函数,而不传递给reducer, 只有当dispatch 一个对象的时候,它才会传递到reducer 中,进行state的更改。也就是说,当进行异步请求的时候,首先要dispatch一个函数,也就是说action creator 要返回一个函数(异步action creator),这个函数有一个dispatch 作为参数,函数体内就可以发送异步请求, 然后在函数的内部,比如获取到数据了或报错了, 再dispatch 一个对象,把获取的到数据或错误信息传递到reducer 中,进而改变state,完成数据的更新。模板如下

function asyncActionCreator() {
  return dispacth => {
    fetchData(url)
      .then(() => {
        dispacth({
          type: 'success'
        })
      })
      .catch(err => {
        dispacth({
          type: 'failure'
        })
      })
  }
}

store.dispacth(asyncActionCreator())

   现在做一个简单的实例,就是点击按钮,随机获取一个用户的信息,网上有这么一个api接口https://randomuser.me/api/?results=1 , 后面的result=1 表示一次只获取一个,是一个get 请求,返回的结果(json 格式)如下,进行了删减, 只显示name,geneder, email 和img

{
    "results": [
        {
            "gender": "female",
            "name": {
                "title": "mrs",
                "first": "minttu",
                "last": "murto"
            },
            "email": "minttu.murto@example.com",
            "picture": {
                "large": "https://randomuser.me/api/portraits/women/40.jpg",
                "medium": "https://randomuser.me/api/portraits/med/women/40.jpg",
                "thumbnail": "https://randomuser.me/api/portraits/thumb/women/40.jpg"
            }
        }
    ]
}

  准备工作完毕,开始写代码,但在写之前,还要考虑一下程序的state, 到底存储什么信息,页面上要展示什么?看一下操作,点击按钮,发起异步的数据请求,这里是一个异步action, 异步请求开始,这时就要给用户提示,需要一个状态status,提示正在加载,因此也需要一个action; 请求数据成功,获取到user,除了status 需要更新之外,它还需要一个状态 user 来存储获取到的数据,需要一个action. 如果失败了,通常更新status 表示加载失败,一个action. 只要涉及到状态的改变,都需要一个action, 在Redux 中只有action 才能改变state. 此外最好再加两个state,statusClass 和sendingRequest. statusClass 对提示的status 进行样式处理,比如加载失败用红色.  sendingRequest 它的取值为true or false, 表示有没有在请求,可以通过它,来决定页面上显示什么内容,比如在加载的时候,不显示用户信息。这两个状态随着status的变化而变化,所以不需要action.那我们的state 就如下所示, 在index.js 中书写

// 初始状态, 定义state的形态
const initialState = {
    sendingRequest: false, // 是否正在请求
    status: '',    // 加载提示
    statusClass: '',  // 加载提示样式
    user: {  // 用户信息
        name: '',
        gender: '',
        email: '',
        img: ''
    }
}

  那页面上显示信息呢?status 加载提示和user 信息,status 这里用bootstrap 的alert 组件 ,user 信息用Card组件.  当然status 还要加上样式stautsClass

<body style="text-align: center; margin-top: 50px">
    <button type="button" class="btn btn-primary" id="getUser">获取用户信息</button>
    <!-- 加载提示 -->
    <div style="width: 18rem; margin: 20px auto; display: none" id="status"></div>
    <!-- 用户信息 -->
    <div class="card" style="width: 20rem; margin: 20px auto; display: none" id="userInfo">
        <img class="card-img-top" src="" id="img">
        <div class="card-body">
            <h5 class="card-title" id="name"></h5>
            <h5 class="card-title" id="gender"></h5>
            <h5 class="card-title" id="email"></h5>
        </div>
    </div>

    <script src="main.js"></script>
</body>

   npx webpack-dev-server 启动服务器, 浏览器中输入localhost:8080 看一下效果, 可以看到在初始状态,加载提示和用户信息都是display 为none,不显示,整个页面只显示一个button. 

  那么现在要做的就是当用户点击的时候,动态显示提示内容,

  和最终的用户信息

 

  好了,现在开始写js 代码来实现这个功能。通过分析state的时候,一个异步action,就是点击按钮发送请求,它需要写一个异步的action creator. 三个同步action  因为它们改变状态,请求发送type: 'USER_REQUEST';  请求成功 type: 'USER_RECEIVED',  还要带有一个请求成功回来的user;数据请求失败type:  'USER_FAIL'; 写一下这四个action creator

// 三个同步action 都是返回的对象,用来改变state.
function userRequest() {  //获取数据时
    return { type: 'USER_REQUEST' }
}

function userReceived(userData) { // 获取成功
    return {
        type: 'USER_RECEIVED',
        payload: userData
    }
}

function userFail() { // 获取失败
    return {
        type: 'USER_FAIL'
    }
}

// getUser的异步action, 注意,它一定返回的是一个函数, 该函数有一个dispatch 作为参数,
// 该函数内部根据不同的情况发送不同的 同步action 来改变state
function getUser() {
    return dispatch => {
        dispatch(userRequest()); // 正在请求action,'USER_REQUEST', 整个应用的state 可以设为status为‘正在加载’

        return fetch('https://randomuser.me/api/?results=1')
            .then(
                response => {
                    if (response.ok) {
                        return response.json()
                    } else {
                        return undefined;
                    }
                },
                error => {
                    dispatch(userFail(error));  // 请求失败的action, 'USER_FAIL',status为‘加载失败’
                }
            )
            .then(json => {
                console.log(json)
                if (!json) {
                    dispatch(userFail()); 
                    return;
                }
                dispatch(userReceived(json.results)) // 请求成功的action 'USER_RECEIVED', 直接更改user
            })
    }
}

  现在有了action 就再写reducer 了,改变的状态的action只有3个,type: 'USER_REQUEST';   type: 'USER_RECEIVED',   type: 'USER_FAIL'

  type: 'USER_REQUEST', 表示正在发送请求,那么 sendingRequest 肯定设为true, status 就设为'正在加载', 正在加载 使用样式statusClass 为 'alert alert-info';

  type: 'USER_RECEIVED', 请求成功了,sendingRequest 设为false, user的信息也获取到了,status 也就不用显示了,还是初始化的'' ,statusClass 也是一样,不过user 状态的处理有点麻烦,因为在intialState中 user 是一个对象,所以在这里要重新创建一个对象user 来替换以前的user, 又因为获取回来的数据不能直接使用,所以进行拼接,然后给新创建的user 对象的属性一一进行赋值。

  type: 'USER_FAIL': 请求失败了,虽然失败了,但请求还是发送成功了, 所以sendingRequest 设为false, 由于没有获取到数据,user 信息不用更新,还是初始化的状态,但status 信息肯定要更新,status 设为 '加载数据失败' ,statusClass 为红色 'alert alert-danger'

  最后不要忘记default 分支,返回默认的初始值。

function userState(state = initialState, action) {
    switch (action.type) {
        case 'USER_REQUEST':
            return {
                ...state,
                sendingRequest: true,
                status: '正在加载',
                statusClass: 'alert alert-info'
            }
        case 'USER_RECEIVED': {
            const user = {
                name: '',
                email: '',
                gender: '',
                img: ''
            }
            user.name = `${action.payload[0].name.first} ${action.payload[0].name.last}`
            user.email = action.payload[0].email;
            user.gender = action.payload[0].gender;
            user.img = action.payload[0].picture.large;
            return {
                ...state,
                user,
                sendingRequest: false
            }
        }
        case 'USER_FAIL':
            return {
                ...state,
                sendingRequest: false,
                status: '获取数据失败',
                statusClass: 'alert alert-danger'
            }
        default:
            return state;
    }
}

  有了reducer , 终于可以创建store了,由于使用es6 模块化,所以要进行引用,由于使用了中间件redux-thunk, 所以要引入createStore, applyMiddleware,  thunk, index.js 顶部

import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';

  创建store

// 创建store, 要把中间件传进去
const store = createStore(userState, applyMiddleware(thunk))

  获取状态渲染页面了,使用document.getElementById 获取元素比较简单,稍微有点麻烦的是页面元素的切换,首先是 sendingRequest 的判断,为true时,正在请求,只能显示加载提示,为false的时候,表示请求完成,但又有两种情况,成功或失败,所以渲染函数如下

const statusEl = document.getElementById('status'); // 加载提示部分
const userInfoEl = document.getElementById('userInfo'); // 用户信息展示部分
const nameEl = document.getElementById('name');
const genderEl = document.getElementById('gender');
const emailEl = document.getElementById('email');
const imgEl = document.getElementById('img');

function render() {
    const state = store.getState();
    if (state.sendingRequest) { // 正在请求的时候,显示提示信息status, 用户信息不显示
        userInfoEl.style.display = 'none';
        statusEl.style.display = 'block';
        statusEl.innerHTML = state.status;
        statusEl.className = state.statusClass;
    } else { // 请求完成后它又分两种情况,
        console.log(state)
        if (state.user.name !== '') { // 请求成功,获取到user 显示user 
            userInfoEl.style.display = 'block';
            statusEl.style.display = 'none';
            nameEl.innerHTML = state.user.name;
            emailEl.innerHTML = state.user.email;
            genderEl.innerHTML = state.user.gender;
            imgEl.src = state.user.img;
        } else { // 请求失败,这里按理说应该写一个error 显示的,为了简单,直接使用了提示status 
            userInfoEl.style.display = 'none';
            statusEl.style.display = 'block';
            statusEl.innerHTML = state.status;
            statusEl.className = state.statusClass;
        }
    }
}

  使用subscribe 监听状态的变化,然后调用render 函数。

store.subscribe(() => {
    render()
})

  最后就是获取到button, 添加点击事件,发送异步action getUser

// 点击按钮发送请求
document.getElementById('getUser').addEventListener('click', () => {
    store.dispatch(getUser());
})

  整个功能就完成了。这时随着功能的复杂,程序开发过程中随时都有可能出现错误,就需要进行调试,对于redux 来说,最主要的就发送的action 和 改变的state, 如果能记录下来,就可以加快调试,有一个中间件,就是redux-logger, 干这个活, npm install redux-logger --save,  安装完成后,使用就简单了,在js 中引入,并放入到applyMiddleware

import { createLogger } from 'redux-logger';
const store = createStore(userState, applyMiddleware(thunk, createLogger()));

   总结一下在redux-thunk 中间件下的异步请求,整个请求过程放到一个接受dispatch 作为参数的函数体( dispatch => { 异步请求})中,只有dispatch 一个函数,redux-thunk 才不会把action 发送给reducer.  而在异步请求的过程中,至少要发送三个同步action, 请求中,请求成功,请求失败,它们要传递给reducer 去改变state, 因为这些信息是要给用户看的,相应的,我们的 reducer 也要至少处理 请求中,请求成功,请求失败 三个action, 返回各个对应的状态。   

  使用Redux DevTools 进行debug

  Redux DevTools 有两个,一个是 Chrome 浏览器插件,一个是npm包,所以使用Redux DevTool 进行Debug时,要先装浏览器插件,然后再在redux store 中引入。安装插件比较简单就不说了。安装完之后,可以不用任何依赖,就可以配置进store。但redux-devtools-extension非常好用,说一下redux store 中怎么引入?npm  install -D redux-devtools-extension

import { devToolsEnhancer } from 'redux-devtools-extension';
const store = createStore(reducer, devToolsEnhancer());

  如果是异步

import { composeWithDevTools } from ‘redux-devtools-extension’;
const store = createStore(
    reducer,
    composeWithDevTools(applyMiddleware(thunk))
);

  热模块替换

  热替换,应用程序状态进行更新,页面不会刷新。在开发模式下,webpack(webpack-dev-server)会暴露module.hot 对象,module.hot对象有一个accect方法,accept方法接受两个参数,一个或多个依赖和回调函数。你想任意的组件更新都会触发热替换,幸运的是,你不必列出所有的react组件作为依赖,子组件进行更新,父组件都能获取到,所以只要把最外层的App组件作第一个参数就可以了。只要模块成功替换,回调函数就会被执行,这时就要渲染App和其它的更新的组件到DOM。总之,组件的每一次更新都会引起组件被替换,这些改变重新渲染到DOM 不会引起页面的渲染

if (module.hot) {
    module.hot.accept('./App', () => {
        const NextApp = require('./App').default;
        ReactDOM.render(
            <Provider store={store}><NextApp /></Provider>,
            document.getElementById('root')
        );
    });
}

  webpack不会在生产模式下暴露module.hot. 在redux下,热替换还可以应用于reducer。监听reducer,模块更新完在怕,替换reducer。

if (module.hot) {
    …
    module.hot.accept('./reducers', () => {
        const nextRootReducer = require('./reducers').default;
        store.replaceReducer(nextRootReducer);
    });
}

  热更替有一定的局限性,比如,它不能保存组件的内部状态(local state),更新非组件文件也会触发页面刷新,要想更进一步,就要使用 React Hot Loader

  中间件,就是把许多action 共有的逻辑抽取出来。Ajax请求就有许多共有的逻辑,比如发送请求之前要dispatch一个action表示要发送请求,发送请求,请求成功的要dispatch 一个action,请求失败后要dispatch一个action,每一个Ajax请求都要经过这四个部分。为此创建三个不同的action。能不能把这个逻辑抽取成中间件?中间件拦截的是action,所以要对Ajax请求的action 作一个规定,有一个type,表示是Ajax请求,然后就是Ajax 请求的三个action,最后就Ajax求本身的内容,比如endpoint, method等。

// 表明这个action是一个特殊的API call
const CALL_API = 'CALL_API';
// 三个请求action
export const FETCH_TASKS_STARTED = 'FETCH_TASKS_STARTED';
export const FETCH_TASKS_SUCCEEDED = 'FETCH_TASKS_SUCCEEDED';
export const FETCH_TASKS_FAILED = 'FETCH_TASKS_FAILED';
export function fetchTasks() {
    return {
        [CALL_API]: {
            types: [FETCH_TASKS_STARTED, FETCH_TASKS_SUCCEEDED,
                FETCH_TASKS_FAILED],
            endpoint: '/tasks',
        },
    };
}

  创建一个文件,比如api.js来写中间件,中间件的模板如下

const CALL_API = 'CALL_API';
const apiMiddleware = store => next => action => {
    const callApi = action[CALL_API];
    if (typeof callApi === 'undefined') {
        return next(action);
    }
}
export default apiMiddleware;    

  如果一个action,不符合中间件的要求,不用处理这个action,next(action), 让下一个中间件处理。如果符合要求,那就要处理,在这里就是发送请求了,先创建一个文件,比如request.js, 写一个发送api的请求

import axios from 'axios';
const API_BASE_URL = 'http://localhost:3001';

export function makeCall(endpoint) {
    const url = `${API_BASE_URL}${endpoint}`;
    return axios
        .get(url)
        .then(resp => {
            return resp;
        })
        .catch(err => {
            return err;
        });
}

  然后在中间件中调用它,

const apiMiddleware = store => next => action => {
    const callApi = action[CALL_API];
    if (typeof callApi === 'undefined') {
        return next(action);
    }
    // 从action中获取到发送请求的不同阶段的action
    const [requestStartedType, successType, failureType] = callApi.types;
    
    // 请求发送之前
    next({ type: requestStartedType });

    return makeCall(callApi.endpoint).then(
        response =>
            next({ // 请求成功
                type: successType,
                payload: response.data,
            }),
        error =>
            next({ // 请求失败
                type: failureType,
                error: error.message,
            }),
    );
};
export default apiMiddleware;

  现在这个中间件,只能处理get请求,不能处理post请求,实际上也简单,就是dispatch action,定制的action传递的参数多了,比如createTask action creator 

export const CREATE_TASK_STARTED = 'CREATE_TASK_STARTED';
export const CREATE_TASK_SUCCEEDED = 'CREATE_TASK_SUCCEEDED';
export const CREATE_TASK_FAILED = 'CREATE_TASK_FAILED';
export function createTask({ title, description, status = 'Unstarted' }) {
    return {
        [CALL_API]: {
            types: [CREATE_TASK_STARTED, CREATE_TASK_SUCCEEDED,
                CREATE_TASK_FAILED],
            endpoint: '/tasks',
            method: 'POST',
            body: {
                title,
                description,
                status,
            },
        },
    };
}

  中间件中,从action中读取的参数,也要多,那mackCall 函数也要支持post,

function makeCall({ endpoint, method = 'GET', body }) {
    const url = `${API_BASE_URL}${endpoint}`;
    const params = {
        method: method,
        url,
        data: body,
        headers: {
            'Content-Type': 'application/json',
        },
    };
    return axios(params).then(resp => resp).catch(err => err);
}


const apiMiddleware = store => next => action => {
    ...
        return makeCall({
            method: callApi.method,
            body: callApi.body,
            endpoint: callApi.endpoint,
        }).then()

  创建完中间件,就要在createStore中的applyMiddleware中用了,不过要注意中间件的顺序,因为在我们创建的中间件中,我们定制了一个action 对象,所以最好把它放到其它中间件的前面。

 

  

posted @ 2019-08-11 15:36  SamWeb  阅读(1604)  评论(4编辑  收藏  举报