Redux 中间件和异步操作
Posted samweb
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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的时候调用。为了更好使用中间件,纯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 instail redux redux-thunk, redux-logger -S. redux-thunk 用于异步请求,redux-logger 记录日志。
redux-thunk 作为异步请求的中间件,使用方式非常简单,当dispatch 一个函数的时候,它就执行这个函数,而不传递给reducer, 只有当dispatch 一个对象的时候,它才会传递到reducer 中,进行state的更改。也就是说,当进行异步请求的时候,首先要dispatch 一个函数(action creator 要返回一个函数),这个函数有一个dispatch 作为参数, 然后在函数的内部,比如获取到数据了或报错了, 再dispatch 一个对象,把获取的到数据或错误信息传递到reducer 中,进而改变state,完成数据的更新。
现在做一个简单的实例,就是点击按钮,随机获取一个用户的信息,网上有这么一个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 中间件和异步操作的主要内容,如果未能解决你的问题,请参考以下文章
Redux 错误操作必须是普通对象。使用自定义中间件进行异步操作
使用 typescript 将异步操作发送到 redux 调度
redux-thunk:错误:动作必须是普通对象。使用自定义中间件进行异步操作