经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » JS/JS库/框架 » JavaScript » 查看文章
《深入浅出React和Redux》(4) - 服务器通信、单元测试
来源:cnblogs  作者:zhixin9001  时间:2021/3/1 9:13:59  对本文有异议

与服务器通信

与服务器通信的时长不可控,需要采用异步的形式,可以使用js的fetch函数来调用api。

fetch函数

fetch函数的基本使用形式为:

  1. fetch(apiUrl).then((response) => {
  2. if (response.status !== 200) {
  3. throw new Error('Fail to get response with status ' + response.status);
  4. }
  5. response.json().then((responseJson) => {
  6. this.setState(...);
  7. }).catch((error) => {
  8. this.setState(...);
  9. });
  10. }).catch((error) => {
  11. this.setState(...);
  12. });

以纯react(没有引入redux)的代码为例,fetch函数执行时会立即返回一个Promise类型的对象,所以要接then和catch,只要服务器返回的是合法的HTTP响应(包括500、400),都会触发then调用,所以在then回调函数中还需要判断status是否为200。
此外,即使在response.status为200时,也不能直接读取response中的内容,因为fetch在接收到HTTP响应的报头部分就会调用then,而不是等到整个HTTP响应完成。所以为了获取到body,还需要继续调用json()并针对其返回的Promise提供回调函数。
在终于成功获取到服务器返回的内容后,通过触发状态的变化引发页面的重新渲染。

redux-thunk中间件

redux的单向数据流是同步操作,如何实现调用服务器这样的异步操作呢?可以使用redux-thunk中间件。

  1. npm install --save redux-thunk

在Redux架构下,一个action对象在通过store.dispatch派发,在调用reducer函数之前,会先经过一个中间件的环节,这就是产生异步操作的机会。

要产生异步操作要发送异步action对象,与普通的action对象不同,它并没有type字段,而且它是一个函数。而redux-thunk的工作是检查action对象是不是函数,如果不是函数就放行,完成普通action对象的生命周期,而如果发现action对象是函数,那就执行这个函数,并把Store的dispatch函数和getState函数作为参数传递到函数中去,处理过程到此为止,不会让这个异步action对象继续往前派发到reducer函数。
createStore时,将redux-thunk中间件作为storeEnhancer之一传入:

  1. const middlewares = [thunkMiddleware];
  2. const storeEnhancers = compose(
  3. applyMiddleware(...middlewares),
  4. (win && win.devToolsExtension) ? win.devToolsExtension() : (f) => f,
  5. );
  6. export default createStore(reducer, {}, storeEnhancers);

异步操作有固定的模式,首先定义三种action类型,分别表示异步操作开始、成功、失败:

  1. export const FETCH_STARTED = 'WEATHER/FENTCH_STARTED';
  2. export const FETCH_SUCCESS = 'WEATHER/FENTCH_SUCCESS';
  3. export const FETCH_FAILURE = 'WEATHER/FENTCH_FAILURE';

然后定义生成异步action的函数,这个函数会被redux-thunk截获并调用,调用时传递的参数是dispatch函数和getState函数:

  1. export const sampleAsyncAction = ()=>{
  2. return (dispatch, getState) => {
  3. // 在这里dispatch FETCH_STARTED action
  4. return fetch(apiUrl).then((response) => {
  5. if (response.status !== 200) {
  6. throw new Error('Fail to get response with status ' + response.status);
  7. }
  8. response.json().then((responseJson) => {
  9. // 在这里dispatch FETCH_SUCCESS action
  10. }).catch((error) => {
  11. });
  12. }).catch((error) => {
  13. // 在这里dispatch FETCH_FAILURE action
  14. });
  15. }
  16. }

终止异步操作

在页面与服务端交互过程中,往往会有一次请求还没结束就发出下一次请求的情况,比如在选择一个下拉项后调用API加载数据,如果数据还没加载完成,就切换到别的下拉项,会发生什么,这取决于两次请求的返回顺序,如果第一次请求先返回,那么页面会先显示第一次的响应结果,再刷新为第二次的,整体来说问题不大;但是如果第二次的请求先于第一次的返回,那么页面显示的最终结果就与下拉项不匹配了。
对于这种场景,简单点可以通过加载数据时禁用下拉框来解决,但这种做法的用户体验较差,如果服务端一直没有响应,下拉框就一直处于禁用状态;还有更合理的一种做法时,在发出下一次API请求时,终止上一次的API请求。

可惜ES6的标准中,Promise对象是无法中断的,为此可以通过应用层的修改来丢弃上一次的请求。以fetchWeather异步action举例:

  1. export const fetchWeather = (cityCode) => {
  2. return (dispatch) => {
  3. const seqId=++nextSeqId;
  4. const dispatchIfValid=(action)=>{
  5. if(seqId===nextSeqId){
  6. return dispatch(action);
  7. }
  8. }
  9. dispatchIfValid(fetchWeatherStarted());
  10. const apiUrl = `/data/cityinfo/${cityCode}.html`;
  11. // dispatch(fetchWeatherStarted());
  12. return fetch(apiUrl).then((response) => {
  13. if (response.status !== 200) {
  14. throw new Error('Fail to get response with status ' + response)
  15. }
  16. response.json().then((response) => {
  17. dispatchIfValid(fetchWeatherSuccess(response.weatherinfo));
  18. }).catch((error) => {
  19. throw new Error('Invalid js on response: ' + error);
  20. });
  21. }).catch((error) => {
  22. dispatchIfValid(fetchWeatherFailure(error));
  23. });
  24. }
  25. };

在action构造函数文件中定义一个文件模块级的nextSeqId变量,这是一个递增的整数数字,给每一个访问API的请求做序列编号。在fetchWeather返回的函数中,fetch开始一个异步请求之前,先给nextSeqId自增加一,然后自增的结果赋值给一个局部变量seqId,这个seqId的值就是这一次异步请求的编号,如果随后还有fetchWeather构造器被调用,那么nextSeqId也会自增,新的异步请求会分配为新的seqId。

然后,action构造函数中所有的dispatch函数都被替换为一个新定义的函数dispatchIfValid,这个dispatchIfValid函数会检查当前环境的seqId是否等同于全局的nextSeqId。如果相同,说明fetchWeather没有被再次调用,就继续使用dispatch函数。如果不相同,说明这期间有新的fetchWeather被调用,也就是有新的访问服务器的请求被发出去了,这时候当前seqId代表的请求就已经过时了,直接丢弃掉,不需要dispatch任何action。

单元测试

关于单元测试框架的选择,由于在create-react-app创建的应用中已经自带了Jest库,所以就直接使用Jest。
Jest会自动在当前目录下寻找文件名以.test.js为后缀的文件和存放在__test__目录下的代码文件,来执行单元测试。
单元测试代码的组织方式,通常有两种模式:

  • 把全部测试代码放在与src平行的test目录,在test目录下建立和src对应子目录结构,每个单元测试文件都加上test.js后缀,这种方法可以保持src目录的整洁,但缺点是单元测试中引用功能代码的路径会比较长;
  • 在src的子目录下创建__test__目录,用于存放对应这个目录的单元测试,这种方法的优缺点与第一种相反。

React & Redux 应用的测试对象主要有action构造函数、reducer、view,其中reducer、普通的action构造函数都是纯函数,非常便于测试。
但异步action的构造函数和view的测试相对比较复杂。

异步action的构造函数的测试

一个异步action对象就是一个函数,需要结合redux-thunk之类的中间件才能发挥作用,异步action被派发之后,会连续派发另外两个action对象代表fetch开始和fetch结束,单元测试要做的就是验证这样的行为。
中间件的应用和action的dispatch都涉及到Redux Store,但单元测试中并不需要创建一个完整功能的Store,也不应该进行真实的网络访问。所以需要一些测试辅助工具。
其中可以使用redux-mock-store来创建一个mock store:

  1. npm install -save-dev redux-mock-store

使用sinon来“篡改”fetch函数的行为,使其不会发出真实的网络请求:

  1. npm install -save-dev sinon

然后就可以开始测试了,首先需要做一些准备工作:

Create Mock Store

  1. import thunk from 'redux-thunk';
  2. import configureStore from 'redux-mock-store';
  3. const middlewares = [thunk];
  4. const createMockStore = configureStore(middlewares);
  5. ...
  6. const store = createMockStore();

Create Mock Store后,就可以像真实store一样在其上dispatch action了。

“篡改”fetch函数的行为

  1. import { stub } from 'sinon';
  2. ...
  3. let stubbedFetch;
  4. beforeEach(() => {
  5. stubbedFetch = stub(global, 'fetch');
  6. });
  7. afterEach(() => {
  8. stubbedFetch.restore();
  9. });
  10. const mockResponse = Promise.resolve({
  11. status: 200,
  12. json: () => Promise.resolve({
  13. weatherinfo: {}
  14. })
  15. });
  16. stubbedFetch.returns(mockResponse);

使用sinon的stub函数来覆盖fetch的返回结果,单元测试用例之间应该互不影响,所以stubbedFetch应该在beforeEach中执行,并在测试用例跑完时执行afterEach时恢复stub行为。

React组件的测试

测试React组件,测试的是渲染结果、事件处理。
但并不是所有的测试过程都需要把React组件的DOM树都渲染出来,尤其对于包含复杂子组件的React组件,如果深入渲染整个DOM树,那就要渲染所有子组件,可是子组件可能会有其他依赖关系,比如依赖于某个React Context值,为了渲染这样的子组件需要耗费很多精力准备测试环境,这种情况下针对目标组件的测试只要让它渲染顶层组件就好了,不需要测试子组件。
测试React组件可以借助Enzyme,它由AirBnb开源,enzyme依赖react-addons-test-utils,要一起安装:

  1. npm install -save-dev enzyme react-addons-test-utils

Enzyme支持三种渲染方法:

  • shallow,只渲染顶层React组件,不渲染子组件,适合只测试React组件的渲染行为;
  • mount,渲染完整的React组件包括子组件,借助模拟的浏览器环境完成事件处理功能;
  • render,渲染完整的React组件,但是只产生HTML,并不进行事件处理。

无状态React组件的测试,可以使用shallow方法只渲染一层,忽略子组件是为了简化测试过程。举例:

  1. const wrapper = shallow(<Filter />);
  2. expect(wrapper.contains(<Link filter={FilterTypes.ALL}> {FilterTypes.ALL} </Link>)).toBe(true);

被连接的React组件的测试,被连接的React组件是指状态保存在Redux的Store上,并通过connect函数产生的组件,这种组件使用时需要包裹在Provider中,测试的时候也一样,而且还会测试事件处理、action dispatch后引发视图的变化,所以这里需要使用真实的store。

  1. import { Provider } from 'react-redux';
  2. ...
  3. const subject = (
  4. <Provider store={store}>
  5. <待测组件 />
  6. </Provider>);

参考书籍

《深入浅出React和Redux》 程墨

原文链接:http://www.cnblogs.com/zhixin9001/p/14438208.html

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号