• 《深入浅出React和Redux》(4)


    与服务器通信

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

    fetch函数

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

    fetch(apiUrl).then((response) => {
      if (response.status !== 200) {
        throw new Error('Fail to get response with status ' + response.status);
      }
    
      response.json().then((responseJson) => {
        this.setState(...);
      }).catch((error) => {
        this.setState(...);
      });
    }).catch((error) => {
      this.setState(...);
    });
    

    以纯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中间件。

    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之一传入:

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

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

    export const FETCH_STARTED = 'WEATHER/FENTCH_STARTED';
    export const FETCH_SUCCESS = 'WEATHER/FENTCH_SUCCESS';
    export const FETCH_FAILURE = 'WEATHER/FENTCH_FAILURE';
    

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

    export const sampleAsyncAction = ()=>{
      return (dispatch, getState) => {
        
        // 在这里dispatch FETCH_STARTED action
    
        return fetch(apiUrl).then((response) => {
          if (response.status !== 200) {
            throw new Error('Fail to get response with status ' + response.status);
          }
    
          response.json().then((responseJson) => {
            // 在这里dispatch FETCH_SUCCESS action
          }).catch((error) => {
            
          });
        }).catch((error) => {
            // 在这里dispatch FETCH_FAILURE action
        });
      }
    }
    

    终止异步操作

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

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

    export const fetchWeather = (cityCode) => {
        return (dispatch) => {
            const seqId=++nextSeqId;
    
            const dispatchIfValid=(action)=>{
                if(seqId===nextSeqId){
                    return dispatch(action);
                }
            }
    
            dispatchIfValid(fetchWeatherStarted());
    
            const apiUrl = `/data/cityinfo/${cityCode}.html`;
    
            // dispatch(fetchWeatherStarted());
    
            return fetch(apiUrl).then((response) => {
                if (response.status !== 200) {
                    throw new Error('Fail to get response with status ' + response)
                }
                response.json().then((response) => {
                    dispatchIfValid(fetchWeatherSuccess(response.weatherinfo));
                }).catch((error) => {
                    throw new Error('Invalid js on response: ' + error);
                });
            }).catch((error) => {
                dispatchIfValid(fetchWeatherFailure(error));
            });
        }
    };
    

    在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:

    npm install -save-dev redux-mock-store
    

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

    npm install -save-dev sinon
    

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

    Create Mock Store

    import thunk from 'redux-thunk';
    import configureStore from 'redux-mock-store';
    const middlewares = [thunk];
    const createMockStore = configureStore(middlewares);
    ...
    const store = createMockStore();
    

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

    “篡改”fetch函数的行为

    import { stub } from 'sinon';
    ...
    let stubbedFetch;
    
    beforeEach(() => {
      stubbedFetch = stub(global, 'fetch');
    });
    
    afterEach(() => {
      stubbedFetch.restore();
    });
    
    const mockResponse = Promise.resolve({
      status: 200,
      json: () => Promise.resolve({
        weatherinfo: {}
      })
    });
    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,要一起安装:

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

    Enzyme支持三种渲染方法:

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

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

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

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

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

    参考书籍

    《深入浅出React和Redux》 程墨

  • 相关阅读:
    dedecms 织梦本地调试 后台反映非常慢的处理办法
    phpcms前端模板目录与文件结构分析图【templates】
    phpcms 思维导图
    Linux下文件的复制、移动与删除
    动态加载dll中的函数
    ava中关于String的split(String regex, int limit) 方法
    java.io.File中的 pathSeparator 与separator 的区别
    如何删除输入法记忆的词汇
    zip4j -- Java处理zip压缩文件的完整解决方案
    file.separator 和 / 区别
  • 原文地址:https://www.cnblogs.com/zhixin9001/p/14438208.html
Copyright © 2020-2023  润新知