V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
rufeng008
V2EX  ›  程序员

react-native-easy-app 详解与使用之(二)fetch

  •  
  •   rufeng008 · 2020-05-14 12:27:20 +08:00 · 804 次点击
    这是一个创建于 1444 天前的主题,其中的信息可能已经有所发展或是发生改变。

    react-native-easy-app 是一款为 React Native App 快速开发提供基础服务的纯 JS 库(支持 IOS & Android),特别是在从 0 到 1 的项目搭建初期,至少可以为开发者减少 30%的工作量。

    react-native-easy-app 主要做了这些工作:

    1. 对 AsyncStorage 进行封装,开发者只需几行代码即可实现一个持久化数据管理器。

    2. 对 fetch 进行封装,使得开发者只需关注当前 App 的前后台交互逻辑和协议,定义好参数设置及解析逻辑即可。

    3. 重新封装了 RN 的 View 、Text 、Image 、FlatList 使用得这些控件在适当的时候支持事件或支持 icon 与文本,能有效减少布局中的嵌套逻辑。

    4. 通过设置一个屏幕参考尺寸,重置 XView 、XText 、XImage 的尺寸,实现自动多屏适配

    可能有人觉得,不同的 App 对 Http 请求的要求各异,第三方库怎么可能做到全面的封装,就算做到了,那也必定会 封装过度

    一千个人心中,有一千个哈姆雷特,也许我的思路能给你带来不一样的启发也未可知呢?

    网络请求( fetch )

    我们先来看下 React native 中文网给出的 fetch 使用示例:

    • 异步请求(核心代码)
    fetch('https://facebook.github.io/react-native/movies.json')
        .then((response) => response.json())
        .then((responseJson) => {
          return responseJson.movies;
        })
        .catch((error) => {
          console.error(error);
        });
    
    • 同步请求(核心代码)
    try {
        // 注意这里的 await 语句,其所在的函数必须有 async 关键字声明
        let response = await fetch('https://facebook.github.io/react-native/movies.json');
        let responseJson = await response.json();
        return responseJson.movies;
      } catch (error) {
        console.error(error);
      }
    

    RN 平台的 fetch 请求很简洁,那我们再看看react-native-easy-app的请求 XHttp是不是也可以方便快捷的发送请求呢?

    • 异步请求(核心代码) 示例 1
     import { XHttp } from 'react-native-easy-app';
    
     XHttp().url('https://facebook.github.io/react-native/movies.json').execute('GET')
       .then(({success, json, message, status}) => {
          console.log(json.movies)
        })
        .catch(({message}) => {
            showToast(message);
        })
    
    • 同步请求(核心代码)示例 2
      import { XHttp } from 'react-native-easy-app';
    
      const response = await XHttp().url('https://facebook.github.io/react-native/movies.json').execute('GET');
      const {success, json, message, status} = response;
      console.log(json.movies)
    
    • 异步请求 2 (核心代码)示例 3
       import { XHttp } from 'react-native-easy-app';
    
       XHttp().url('https://facebook.github.io/react-native/movies.json').get((success, json, message, status)=>{
           console.log(json.movies)
       });
    

    通过执行上面三段示例代码,发现输出了一致的结果(电影列表数组):

    movies.png

    通过对比发现 XHttp 的使用与 React Native 平台提供的 fetch 很相似,其 execute('get')方法返回的是一个 promise 对象,故也可以像 fetch 一样,发送同步或异步请求。另外还可以通过[method]+回调的形式发送请求。

    相比原生 fetch 请求,XHttp 却返回了多个参数,我们打印一下示例 2 中的 response 看看里面都有啥?输出结果,格式化后如下:

    response.png

    1. success => [true | false] 请求成功或失败的标识(默认以 Http 的请求状态码:[ status >= 200 && status < 400 ] 作为判断依据)。
    2. json => [Json Object | originText] 默认为请求返回的 json 对象,必要时可以指定返回纯文本字符串(若请求结果为非标准 Json,如 XML 结构或其它)或通过自定义配置指定请求返回的数据结构。
    3. message 默认情况下,请求成功时:为[code+url],失败时:则为错误信息[错误信息+code+url],若开发者指定了特定的解析方式,则由开发者制定。
    4. status 默认情况下为 Http 请求的 status code,可由开发者制定,返回自定义的业务逻辑请求状态码

    通过上面的示例,react-native-easy-app 的 XHttp 可以像使用 fetch 一样方便快捷的发送 Http 请求,而且还包含请求码,错误信息,结果也被转化为了 json 对象,使用我们发送请求更加方便了。

    但在实际的 App 开发中,我们 Http 请求框架的要求不只是能发送简单的 Http 请求就可以了,比如说,需要打印请求日志、设置 header 参数、统一处理解析逻辑,甚至可能处理返回的结构不是标准的 json 数据等各种需求。

    我们来看看 react-native-easy-app 的 XHttp 能满足我们哪些需求: 注:上面三个示例的请求方式各有所长,下文发送请求示例的地方我都选择使用请求 示例 3 的方式举例

    • 需求 1:能支持 get 、post 、put 、delete 等基本常用类型的请求
      • 框架会自动根据输入的请求类型,自动会处理请求的 body 有无问题
      • 1 、通过 XHttp 的 execute('method')方式发送请求自然是没有问题
      • 2 、通过 method + 回调的形式(满足 90%的情况),我问下的情况怎么办?不用担心框架提供了另一种方式实现,即:
    XHttp().url('https://facebook.github.io/react-native/movies.json').request('HEAD', (success, json, message, status) => {
        console.log(json.movies);
    })
    

    • 需求 2:能支持常用的 contentType 设置,如 application/json 、multipart/form-data 、application/x-www-form-urlencoded 等
      • 当然并不只是简单的传个参数而已,必须能根据请求 contentType 按正常的方式处理 body,如果 contentType 若为 multipart/form-data,则使用 FormData 去接收拼接开发者传入的参数
      • 1 、XHttp 有三种方式设置 contentType,三种常用的方式被提取了出来,如下分别是:直接设置;通过 header 设置;通过方法直接指定。开发者设置了相应的方式之后,就可以放心的发送 Http 请求了,剩下的框架会处理(下面示例为:上传图片设置):

    contentType.png


    • 需求 3:能支持超时设置;支持日志打印;支持返回非标准 Json 以及 baseUrl 的拼接
      • 请求超的原理是通过 Promise.race 实现;
      • 1.由于超时请求并不完全属于某个特定的请求,故引入了一个公共配置对象:XHttpConfig,开发者可以通过两种试设置请求超时配置,如下:
    import { XHttpConfig } from 'react-native-easy-app';
    
    XHttpConfig().initTimeout(300000); //全局配置,设置所有 Http 请求的超时时间为 30 秒
    
    XHttp().url('https://facebook.github.io/react-native/movies.json').timeout(15000) //设置当前请求超时间为 15 秒
        .get((success, json, message, status) => {
        })
    
    • 2 、日志打印也是通过 XHttpConfig().initHttpLogOn(true) 设置为 true 即可,设置完成后,我们发送请求,看看控制台的输出日志:
    XHttpConfig().initHttpLogOn(true);
    XHttp().url('https://facebook.github.io/react-native/movies.json').get((success, json, message, status) => {
    })
    

    httplog.png

    可以看出控制台打印出了详细的日志,是不是很方便?

    • 3 、现在的移动开发 99%的情况下前后台交互都是使用的 json 格式数据,但很难保证一些特殊情况下,App 不使用非标准 json 数据格式的 Http 请求。比如需要请求一些老网站或者使用一些第三方开放的老接口。这时候只需要指定返回纯文件数据即可,下面找一个返回 xml 格式的接口,请求看看结果:
    let url = 'http://www.webxml.com.cn/WebServices/MobileCodeWS.asmx/getDatabaseInfo'
    XHttp().url(url).pureText().get((success, text, message, status) => {
        console.log('XML data', text)
    })
    

    控制台输出结果如下(通过 XHttp 的 pureText() 指定返回的数据以纯文本返回): httpXml.png

    • 4 、至于 baseUrl 的拼接,则是为了在 App 开发中,减少不必要的 baseUrl 的重复使用(程序通过判断传入的 url 是否是完整按需拼接 BaseUrl ),使用方法如下:
    import { XHttpConfig, XHttp } from 'react-native-easy-app';
    
    XHttpConfig().initBaseUrl('http://www.webxml.com.cn/WebServices/');
    XHttp().url('MobileCodeWS.asmx/getDatabaseInfo').get((success, text, message, status) => {
        console.log('XML data', text)
    })
    

    • 需求 4:能自由设置公共的 params 、headers ;发送 Http 请求的时候,也能自由设定当前请求的 header 及 param 数据。
    import { XHttpConfig, XHttp } from 'react-native-easy-app';
    
    XHttpConfig().initHttpLogOn(true)
        .initBaseUrl('https://facebook.github.io/')
        .initContentType('multipart/form-data')
        .initHeaderSetFunc((headers, request) => {
            headers.headers_customerId = 'headers_CustomerId001';
            headers.headers_refreshToken = 'headers_RefreshToken002';
        })
        .initParamSetFunc((params, request) => {
            params.params_version = 'params_version003';
            params.params_channel_code = 'params_channel_code004';
            params.testChannel = 'testChannel005';
        });
    
    XHttp().url('react-native/movies.json')
        .header({'Content-Type': 'application/json', header_type: 'header_type006'})
        .param({paramUserName: 'paramUserName007', testChannel: 'testChannel008'})
        .post((success, text, message, status) => {
        })
    

    从代码中可以看出通过 XHttpConfig 配置,我们设置了公共的 heders 、params,然后在通过 XHttp 发送请求时,又设置了特定的 header 和 param 的值,同时了修改了 contentType 的类型,并改为 post 请求,执行代码我们看看控制台日志内容:

    common_params.png

    通过控制台打印的日志,我们可以很清晰的看到,参数从 001~008 所有的参数(除了 005 )都能有效设置到请求当中。但为什么公共参数 params.testChannel = 'testChannel005'; 的设置没有生效呢,其实是因为,XHttp 中的接口请求的私有参数中也设置了一个:testChannel: 'testChannel008' 的参数,两者的 Key 相同,所以被接口私有参数给覆盖了(细心的同学也可以发现,日志中'Content-Type': 'application/json',contentType 的类型也被覆盖了),这说明了接口的私有参数具有更高的优先级,这是合理的同时也使接口的请求更灵活方便。


    • 需求 5:能支持自定义数据解析,这也是最重要的。 每个 app 都有一套前后台数据交互方式,对于返回的数据都有统一固定的格式:方便前端解析处理,如 cryptonator.com 网站提供的比特币查询接口,接口 url: https://api.cryptonator.com/api/ticker/btc-usd 。我们先通过 postman 请求一下:

    request_postman.png

    返回的数据格式如下:

    {
      "ticker": {
        "base": "BTC",
        "target": "USD",
        "price": "5301.78924881",
        "volume": "179358.70555921",
        "change": "-21.18183054"
      },
      "timestamp": 1584291183,
      "success": true,
      "error": ""
    }
    

    可以看出,接口返回的数据结构中,有三个主要字段:

    1. success 接口逻辑成功与失败的判断依据。
    2. error 接口若失败时,包含错误信息。
    3. ticker 接口返回的主要数据的主体。

    以前面 XHttp 发送请求,接口的成功与否的判断依然是 http 的 status 来判断,显示达不到要求,请求 cryptonator.com 网站 api 数据统一解析的基本要求,那怎么自定义数据解析呢?我们试试看。

    import { XHttpConfig, XHttp } from 'react-native-easy-app';
    
    XHttpConfig().initHttpLogOn(true)
        .initBaseUrl('https://www.cryptonator.com/api/')
        .initParseDataFunc((result, request, callback) => {
            let {success, json, message, status} = result;
            callback(success && json.success, json.ticker || {}, json.error || message, status);
        });
    
    XHttp().url('ticker/btc-usd').get((success, json, message, status) => {
        console.log('success = ' + success);
        console.log('json    = ' + JSON.stringify(json));
        console.log('message = ' + message);
        console.log('status  = ' + status);
    });
    

    我们再看下控制台输出的请求日志与 Http 请求打印的4 个标准参数的内容:

    custom_parse_data_log.png

    custom_parse_data.png

    发现没有,json 对应的值就是返回的数据结构中:ticker 对应的数据。其它字段并不能反映出来,因为数据刚好与默认判断条件吻合或为空。这是怎么实现的呢?

    因为通过 XHttpConfig 的 initParseDataFunc 方法,我们重新定义了,接口请求返回的标准字段的值:

    1. success => success && json.success 只有当接口请求与返回的成功标记同时为 true 的时候才认为是成功
    2. json => json.ticker 直接读取 json.ticker 的值(若为空,则返回一个没有任何属性对象)
    3. message => json.error || message 优先获取接口返回的错误信息(若为空,则读取 Http 请求的错误信息)
    4. status => status 由于些 api 并没有 code 判断标记,故依然使用 Http 的 status

    这样 Http 请求返回的参数自定义问题就解决了,这时候可能有人会说:我的 app 不只是请求一个后台或者还要请求第三方接口,不同的后台返回的数据结构也完全不一样,这种情况下么处理?不用担心,这种情况也是有解的:

    FHttp().url('https://api.domainsdb.info/v1/domains/search')
       .param({domain: 'zhangsan', zone: 'com'})
       .contentType('text/plain')
       .rawData()
       .get((success, json, message, status) => {
           if (success) {
               console.log('rawData', JSON.stringify(json))
           } else {
               console.log(message)
           }
       })
    

    接口请求打印的日志为: rawData.png

    请求依然成功,各参数也没有问题,因为在发送 Http 请求的时候增加了一个标记 rawData(),这个标记就是用于特殊处理的,标记当前 Http 请求需要返回原始的,不做任何解析的数据(设置此标记,会自动忽略用户自定义的数据解析方式)

    • 办法二(也有可能一个 App 要请求多个不同的平台或者新老版本过渡,而且不同风格的接口数量还不在少数),同时在这种情况下可能请求的参数风格,公共参数也有不同的要求,这就更复杂了,这种情况能否处理?答案是肯定的:

    假设当前 App 要请求三个平台:分别为 SA,SB,SC,这三个平台要求不同的公共参数(包括 header ),且返回的数据结构也完全不一致,这时候我们可以这样处理,配置与请求都可以完全独立的实现:

    import { XHttpConfig, XHttp } from 'react-native-easy-app';
    
    XHttpConfig('SA').initHttpLogOn(true) ...
    
    XHttpConfig('SB').initHttpLogOn(true) ...
    
    XHttpConfig('SC').initHttpLogOn(true) ...
    
    const url = 'https://facebook.github.io/react-native/movies.json';
    
    XHttp().serverTag('SA').url(url) .get((success, json, message, status) =>{
    });
    
    XHttp().serverTag('SB').url(url) .get((success, json, message, status) =>{
    });
    
    XHttp().serverTag('SC').url(url) .get((success, json, message, status) =>{
    });
    

    就是这么简单,配置与请求可以通过 serverTag 来区别,默认情况下使用同一个配置,但若指定了新的 serverTag,发送 Http 请求时就可以通过 serverTag 来指定使用哪个 Http 请求的配置,这样同一个 app 里面,请求不同的服务器,以及处理不同服务器返回的数据也完全没有压力。

    通过上面的例子,我们可以看出,XHttpConfig 的三个公共配置方法:initHeaderSetFunc 、initParamSetFunc 、initParseDataFunc 是一个 面向切面的编程模式 ,这些方法还有一个共同的参数 request(第二个参数)里面包含了请求的所有原始信息,因此可以有更多的想象空间,就等你去探索。


    • 可能部分同学觉得,框架的参数设置挺方便,但数据的解析我想完全自己实现可以么?当然可以,通过 fetch 方法,返回的是原 fetch 请求的 promise,框架不做任何处理:

    parse_native.png

    • 也有同学想,框架的解析很方便,我想完全使用框架的解析,但有些参数是放在 header 里面,我怎么才能在解析数据的时候取到 response 的 header 数据呢?这个问题也不用担心,在所有示例中,我列表的解析回调的参数都是 4 个:(success, json, message, status),但实际上有 5 个参数,第 5 就是 response,它就是 fetch 返回的 reponse,你可以从里取到任何想要的数据,包括 headers
    const url = 'https://facebook.github.io/react-native/movies.json';
    
    XHttp().url(url).get((success, json, message, status, response) => {
        console.log(JSON.stringify(response.headers))
    });
    
    const {success, json, message, status, response} = await XHttp().url(url).execute('GET');
    console.log(JSON.stringify(response.headers))
    
    • 也有同学可能想到有一种应用场景 oauth2 需要特别处理:
    1. 发送请求 req1,因为 accessToken 失效而请求失败
    2. 程序通过 refreshToken 重新获取到了新的 accessToken
    3. 拿着新的 accessToken 重新请求 req1

    这种应用场景怎么处理呢?

    XHttpConfig()
        .initHttpLogOn(true)
        .initBaseUrl(ApiCredit.baseUrl)
        .initContentType(XHttpConst.CONTENT_TYPE_URLENCODED)
        .initHeaderSetFunc((headers, request) => {
            if (request.internal) {
                Object.assign(headers, AuthToken.baseHeaders());//添加基础参数
                headers.customerId = RNStorage.customerId;
                if (RNStorage.refreshToken) {//若 refreshToken 不为空,则拼接
                    headers['access-token'] = RNStorage.accessToken;
                    headers['refresh-token'] = RNStorage.refreshToken;
                }
            }
        })
        .initParamSetFunc((params, request) => {
            if (request.internal && RNStorage.customerId) {
                params.CUSTOMER_ID = RNStorage.customerId;
            }
        }).initParseDataFunc((result, request, callback) => {
        let {success, json, response, message, status} = result;
        AuthToken.parseTokenRes(response);//解析 token
        if (status === 503) {//指定的 Token 过期标记
            this.refreshToken(request, callback)
        } else {
            let {successful, msg, code} = json;
            callback(success && successful === 1, selfOr(json.data, {}), selfOr(msg, message), code);
        }
    });
    
    static refreshToken(request, callback) {
        if (global.hasQueryToken) {
            global.tokenExpiredList.push({request, callback});
        } else {
            global.hasQueryToken = true;
            global.tokenExpiredList = [{request, callback}];
            const refreshUrl = `${RNStorage.baseUrl}api/refreshToken?refreshToken=${RNStorage.refreshToken}`;
            fetch(refreshUrl).then(resp => {
                resp.json().then(({successful, data: {accessToken}}) => {
                    if (successful === 1) {// 获取到新的 accessToken
                        RNStorage.accessToken = accessToken;
                        global.tokenExpiredList.map(({request, callback}) => {
                            request.resendRequest(request, callback);
                        });
                        global.tokenExpiredList = [];
                    } else {
                        console.log('Token 过期,退出登录');
                    }
                });
            }).catch(err => {
                console.log('Token 过期,退出登录');
            }).finally(() => {
                global.hasQueryToken = false;
            });
        }
    };
    

    在这里我就不做详细说明了直接贴代码,详细的请大家可以直接阅读源码或者参考 react-native-easy-app 库对应的 示例项目,至于原理是:在请求的时候,将初请求的方法引用保存到了 request 中,并命名为 resendRequest,若获取到新的 token 之后,重新请求一遍 resendRequest 方法,传入原来的参数即可。


    可能有同学觉得react-native-easy-app封装 XHttp 与 XHttpConfig 的方法与参数太多了,根本没办法记住,框架虽好却不便于使用,这个目前可能需要大家参考示例项目来写了(后面我会完善说明文档)。

    当然大家有没有发现,在使用这些库方法的时候,代码有提示呢?那就对了。因为我为主要的方法增加了 dts 描述文档,所以在写代码过程中,如果不记得方法名参数直接通过代码自动提示来写就行了(自动提示在 webStorm 上的体验更好):

    提示 1.png

    提示 2.png

    提示 3.png

    ###react-native-easy-app 详解与使用之(三) View,Text,Image,Flatlist

    想进一步了解,请移步至 npm 或 github 查看 react-native-easy-app,有源码及使用示例,待大家一探究竟,欢迎朋友们 Star !

    如果有任何疑问,欢迎扫码加入 RN 技术 QQ 交流群

    qq_qrCode.jpg

    目前尚无回复
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   1470 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 37ms · UTC 17:03 · PVG 01:03 · LAX 10:03 · JFK 13:03
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.