react-native-easy-app 詳解與使用之(二) fetch

react-native-easy-app 是一款為React Native App快速開發(fā)提供基礎(chǔ)服務(wù)的純JS庫(支持 IOS & Android),特別是在從0到1的項(xiàng)目搭建初期,至少可以為開發(fā)者減少30%的工作量。

react-native-easy-app 主要做了這些工作:
1. 對AsyncStorage進(jìn)行封裝,開發(fā)者只需幾行代碼即可實(shí)現(xiàn)一個(gè)持久化數(shù)據(jù)管理器。
2. 對fetch進(jìn)行封裝,使得開發(fā)者只需關(guān)注當(dāng)前App的前后臺交互邏輯和協(xié)議,定義好參數(shù)設(shè)置及解析邏輯即可。
3. 重新封裝了RN的View、Text、Image、FlatList 使用得這些控件在適當(dāng)?shù)臅r(shí)候支持事件或支持icon與文本,能有效減少布局中的嵌套邏輯。
4. 通過設(shè)置一個(gè)屏幕參考尺寸,重置XView、XText、XImage的尺寸,實(shí)現(xiàn)自動多屏適配

可能有人覺得,不同的App對Http請求的要求各異,第三方庫怎么可能做到全面的封裝,就算做到了,那也必定會 封裝過度

一千個(gè)人心中,有一千個(gè)哈姆雷特,也許我的思路能給你帶來不一樣的啟發(fā)也未可知呢?

網(wǎng)絡(luò)請求(fetch)

我們先來看下React native中文網(wǎng)給出的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語句,其所在的函數(shù)必須有async關(guān)鍵字聲明
    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是不是也可以方便快捷的發(fā)送請求呢?

  • 異步請求(核心代碼) 示例 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)
   });

通過執(zhí)行上面三段示例代碼,發(fā)現(xiàn)輸出了一致的結(jié)果(電影列表數(shù)組):

movies.png

通過對比發(fā)現(xiàn) XHttp 的使用與React Native平臺提供的fetch很相似,其execute('get')方法返回的是一個(gè)promise對象,故也可以像fetch一樣,發(fā)送同步或異步請求。另外還可以通過[method]+回調(diào)的形式發(fā)送請求。

相比原生fetch請求,XHttp 卻返回了多個(gè)參數(shù),我們打印一下示例2中的response看看里面都有啥?輸出結(jié)果,格式化后如下:

response.png
  1. success => [true | false] 請求成功或失敗的標(biāo)識(默認(rèn)以Http的請求狀態(tài)碼:[ status >= 200 && status < 400 ] 作為判斷依據(jù))。
  2. json => [Json Object | originText] 默認(rèn)為請求返回的json對象,必要時(shí)可以指定返回純文本字符串(若請求結(jié)果為非標(biāo)準(zhǔn)Json,如XML結(jié)構(gòu)或其它)或通過自定義配置指定請求返回的數(shù)據(jù)結(jié)構(gòu)。
  3. message 默認(rèn)情況下,請求成功時(shí):為[code+url],失敗時(shí):則為錯誤信息[錯誤信息+code+url],若開發(fā)者指定了特定的解析方式,則由開發(fā)者制定。
  4. status 默認(rèn)情況下為Http請求的status code,可由開發(fā)者制定,返回自定義的業(yè)務(wù)邏輯請求狀態(tài)碼

通過上面的示例, react-native-easy-app 的 XHttp 可以像使用fetch一樣方便快捷的發(fā)送Http請求,而且還包含請求碼,錯誤信息,結(jié)果也被轉(zhuǎn)化為了json對象,使用我們發(fā)送請求更加方便了。

但在實(shí)際的App開發(fā)中,我們Http請求框架的要求不只是能發(fā)送簡單的Http請求就可以了,比如說,需要打印請求日志、設(shè)置header參數(shù)、統(tǒng)一處理解析邏輯,甚至可能處理返回的結(jié)構(gòu)不是標(biāo)準(zhǔn)的json數(shù)據(jù)等各種需求。

我們來看看 react-native-easy-app 的 XHttp 能滿足我們哪些需求:
注:上面三個(gè)示例的請求方式各有所長,下文發(fā)送請求示例的地方我都選擇使用請求 示例 3 的方式舉例。

  • 需求 1 :能支持get、post、put、delete等基本常用類型的請求
    • 框架會自動根據(jù)輸入的請求類型,自動會處理請求的body有無問題
    • 1、通過XHttp 的execute('method')方式發(fā)送請求自然是沒有問題
    • 2、通過method + 回調(diào)的形式(滿足90%的情況),我問下的情況怎么辦?不用擔(dān)心框架提供了另一種方式實(shí)現(xiàn),即:
XHttp().url('https://facebook.github.io/react-native/movies.json').request('HEAD', (success, json, message, status) => {
    console.log(json.movies);
})

  • 需求 2:能支持常用的contentType設(shè)置,如 application/json、multipart/form-data、application/x-www-form-urlencoded等
    • 當(dāng)然并不只是簡單的傳個(gè)參數(shù)而已,必須能根據(jù)請求contentType按正常的方式處理body,如果contentType若為multipart/form-data,則使用FormData去接收拼接開發(fā)者傳入的參數(shù)
    • 1、 XHttp 有三種方式設(shè)置contentType,三種常用的方式被提取了出來,如下分別是:直接設(shè)置;通過header設(shè)置;通過方法直接指定。開發(fā)者設(shè)置了相應(yīng)的方式之后,就可以放心的發(fā)送Http請求了,剩下的框架會處理(下面示例為:上傳圖片設(shè)置):
contentType.png

  • 需求 3:能支持超時(shí)設(shè)置;支持日志打??;支持返回非標(biāo)準(zhǔn)Json以及baseUrl的拼接
    • 請求超的原理是通過 Promise.race 實(shí)現(xiàn);
    • 1.由于超時(shí)請求并不完全屬于某個(gè)特定的請求,故引入了一個(gè)公共配置對象:XHttpConfig,開發(fā)者可以通過兩種試設(shè)置請求超時(shí)配置,如下:
import { XHttpConfig } from 'react-native-easy-app';

XHttpConfig().initTimeout(300000); //全局配置,設(shè)置所有Http請求的超時(shí)時(shí)間為30秒

XHttp().url('https://facebook.github.io/react-native/movies.json').timeout(15000) //設(shè)置當(dāng)前請求超時(shí)間為15秒
    .get((success, json, message, status) => {
    })
  • 2、日志打印也是通過 XHttpConfig().initHttpLogOn(true) 設(shè)置為 true 即可,設(shè)置完成后,我們發(fā)送請求,看看控制臺的輸出日志:
XHttpConfig().initHttpLogOn(true);
XHttp().url('https://facebook.github.io/react-native/movies.json').get((success, json, message, status) => {
})
httplog.png

可以看出控制臺打印出了詳細(xì)的日志,是不是很方便?

  • 3、現(xiàn)在的移動開發(fā)99%的情況下前后臺交互都是使用的json格式數(shù)據(jù),但很難保證一些特殊情況下,App不使用非標(biāo)準(zhǔn)json數(shù)據(jù)格式的Http請求。比如需要請求一些老網(wǎng)站或者使用一些第三方開放的老接口。這時(shí)候只需要指定返回純文件數(shù)據(jù)即可,下面找一個(gè)返回xml格式的接口,請求看看結(jié)果:
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)
})

控制臺輸出結(jié)果如下(通過XHttp的 pureText() 指定返回的數(shù)據(jù)以純文本返回):


httpXml.png
  • 4、至于baseUrl的拼接,則是為了在App開發(fā)中,減少不必要的baseUrl的重復(fù)使用(程序通過判斷傳入的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:能自由設(shè)置公共的params、headers;發(fā)送Http請求的時(shí)候,也能自由設(shè)定當(dāng)前請求的header及param數(shù)據(jù)。
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配置,我們設(shè)置了公共的heders、params,然后在通過XHttp發(fā)送請求時(shí),又設(shè)置了特定的header和param的值,同時(shí)了修改了contentType的類型,并改為post請求,執(zhí)行代碼我們看看控制臺日志內(nèi)容:

common_params.png

通過控制臺打印的日志,我們可以很清晰的看到,參數(shù)從001~008所有的參數(shù)(除了005)都能有效設(shè)置到請求當(dāng)中。但為什么公共參數(shù) params.testChannel = 'testChannel005'; 的設(shè)置沒有生效呢,其實(shí)是因?yàn)?,XHttp中的接口請求的私有參數(shù)中也設(shè)置了一個(gè):testChannel: 'testChannel008' 的參數(shù),兩者的Key相同,所以被接口私有參數(shù)給覆蓋了(細(xì)心的同學(xué)也可以發(fā)現(xiàn),日志中'Content-Type': 'application/json',contentType的類型也被覆蓋了),這說明了接口的私有參數(shù)具有更高的優(yōu)先級,這是合理的同時(shí)也使接口的請求更靈活方便。


  • 需求 5:能支持自定義數(shù)據(jù)解析,這也是最重要的。
    每個(gè)app都有一套前后臺數(shù)據(jù)交互方式,對于返回的數(shù)據(jù)都有統(tǒng)一固定的格式:方便前端解析處理,如 cryptonator.com 網(wǎng)站提供的比特幣查詢接口,接口url:https://api.cryptonator.com/api/ticker/btc-usd。我們先通過postman請求一下:
request_postman.png

返回的數(shù)據(jù)格式如下:

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

可以看出,接口返回的數(shù)據(jù)結(jié)構(gòu)中,有三個(gè)主要字段:

  1. success 接口邏輯成功與失敗的判斷依據(jù)。
  2. error 接口若失敗時(shí),包含錯誤信息。
  3. ticker 接口返回的主要數(shù)據(jù)的主體。

以前面XHttp發(fā)送請求,接口的成功與否的判斷依然是http的status來判斷,顯示達(dá)不到要求,請求cryptonator.com網(wǎng)站api數(shù)據(jù)統(tǒng)一解析的基本要求,那怎么自定義數(shù)據(jù)解析呢?我們試試看。

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個(gè)標(biāo)準(zhǔn)參數(shù)的內(nèi)容:

custom_parse_data_log.png
custom_parse_data.png

發(fā)現(xiàn)沒有,json對應(yīng)的值就是返回的數(shù)據(jù)結(jié)構(gòu)中:ticker對應(yīng)的數(shù)據(jù)。其它字段并不能反映出來,因?yàn)閿?shù)據(jù)剛好與默認(rèn)判斷條件吻合或?yàn)榭?。這是怎么實(shí)現(xiàn)的呢?

因?yàn)橥ㄟ^XHttpConfig的initParseDataFunc方法,我們重新定義了,接口請求返回的標(biāo)準(zhǔn)字段的值:

  1. success => success && json.success 只有當(dāng)接口請求與返回的成功標(biāo)記同時(shí)為true的時(shí)候才認(rèn)為是成功
  2. json => json.ticker 直接讀取json.ticker的值(若為空,則返回一個(gè)沒有任何屬性對象)
  3. message => json.error || message 優(yōu)先獲取接口返回的錯誤信息(若為空,則讀取Http請求的錯誤信息)
  4. status => status 由于些api并沒有code判斷標(biāo)記,故依然使用Http的status

這樣Http請求返回的參數(shù)自定義問題就解決了,這時(shí)候可能有人會說:我的app不只是請求一個(gè)后臺或者還要請求第三方接口,不同的后臺返回的數(shù)據(jù)結(jié)構(gòu)也完全不一樣,這種情況下么處理?不用擔(dān)心,這種情況也是有解的:

  • 辦法一(非標(biāo)準(zhǔn)接口較少的情況):
    比如說,我的請求以cryptonator.com網(wǎng)站的api為主,偶爾要請求域名查詢接口:
    https://api.domainsdb.info/v1/domains/search?domain=zhangsan&zone=com,這個(gè)時(shí)候,我可以依然保持前面的自定義解析方式不變,在請求域名查詢的時(shí)候,增加一個(gè)標(biāo)記:
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

請求依然成功,各參數(shù)也沒有問題,因?yàn)樵诎l(fā)送Http請求的時(shí)候增加了一個(gè)標(biāo)記rawData(),這個(gè)標(biāo)記就是用于特殊處理的,標(biāo)記當(dāng)前Http請求需要返回原始的,不做任何解析的數(shù)據(jù)(設(shè)置此標(biāo)記,會自動忽略用戶自定義的數(shù)據(jù)解析方式)

  • 辦法二(也有可能一個(gè)App要請求多個(gè)不同的平臺或者新老版本過渡,而且不同風(fēng)格的接口數(shù)量還不在少數(shù)),同時(shí)在這種情況下可能請求的參數(shù)風(fēng)格,公共參數(shù)也有不同的要求,這就更復(fù)雜了,這種情況能否處理?答案是肯定的:

假設(shè)當(dāng)前App要請求三個(gè)平臺:分別為SA,SB,SC,這三個(gè)平臺要求不同的公共參數(shù)(包括header),且返回的數(shù)據(jù)結(jié)構(gòu)也完全不一致,這時(shí)候我們可以這樣處理,配置與請求都可以完全獨(dú)立的實(shí)現(xiàn):

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('SA').url(url) .get((success, json, message, status) =>{
});

XHttp('SB').url(url) .get((success, json, message, status) =>{
});

XHttp('SC').url(url) .get((success, json, message, status) =>{
});

就是這么簡單,配置與請求可以通過serverTag來區(qū)別,默認(rèn)情況下使用同一個(gè)配置,但若指定了新的serverTag,發(fā)送Http請求時(shí)就可以通過serverTag來指定使用哪個(gè)Http請求的配置,這樣同一個(gè)app里面,請求不同的服務(wù)器,以及處理不同服務(wù)器返回的數(shù)據(jù)也完全沒有壓力。

通過上面的例子,我們可以看出,XHttpConfig的三個(gè)公共配置方法:initHeaderSetFunc、initParamSetFunc、initParseDataFunc 是一個(gè) 面向切面的編程模式 ,這些方法還有一個(gè)共同的參數(shù)request(第二個(gè)參數(shù))里面包含了請求的所有原始信息,因此可以有更多的想象空間,就等你去探索。


  • 可能部分同學(xué)覺得,框架的參數(shù)設(shè)置挺方便,但數(shù)據(jù)的解析我想完全自己實(shí)現(xiàn)可以么?當(dāng)然可以,通過fetch方法,返回的是原fetch請求的promise,框架不做任何處理:
parse_native.png
  • 也有同學(xué)想,框架的解析很方便,我想完全使用框架的解析,但有些參數(shù)是放在header里面,我怎么才能在解析數(shù)據(jù)的時(shí)候取到response的header數(shù)據(jù)呢?這個(gè)問題也不用擔(dān)心,在所有示例中,我列表的解析回調(diào)的參數(shù)都是4個(gè):(success, json, message, status),但實(shí)際上有5個(gè)參數(shù),第5就是response,它就是fetch返回的reponse,你可以從里取到任何想要的數(shù)據(jù),包括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))
  • 也有同學(xué)可能想到有一種應(yīng)用場景oauth2需要特別處理:
  1. 發(fā)送請求req1,因?yàn)閍ccessToken失效而請求失敗
  2. 程序通過refreshToken重新獲取到了新的accessToken
  3. 拿著新的accessToken重新請求req1

這種應(yīng)用場景怎么處理呢?

XHttpConfig()
    .initHttpLogOn(true)
    .initBaseUrl(ApiCredit.baseUrl)
    .initContentType(XHttpConst.CONTENT_TYPE_URLENCODED)
    .initHeaderSetFunc((headers, request) => {
        if (request.internal) {
            Object.assign(headers, AuthToken.baseHeaders());//添加基礎(chǔ)參數(shù)
            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過期標(biāo)記
        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;
        });
    }
};

在這里我就不做詳細(xì)說明了直接貼代碼,詳細(xì)的請大家可以直接閱讀源碼或者參考 react-native-easy-app 庫對應(yīng)的 示例項(xiàng)目,至于原理是:在請求的時(shí)候,將初請求的方法引用保存到了request中,并命名為resendRequest,若獲取到新的token之后,重新請求一遍resendRequest方法,傳入原來的參數(shù)即可。


可能有同學(xué)覺得react-native-easy-app封裝XHttp與XHttpConfig的方法與參數(shù)太多了,根本沒辦法記住,框架雖好卻不便于使用,這個(gè)目前可能需要大家參考示例項(xiàng)目來寫了(后面我會完善說明文檔)。

當(dāng)然大家有沒有發(fā)現(xiàn),在使用這些庫方法的時(shí)候,代碼有提示呢?那就對了。因?yàn)槲覟橹饕姆椒ㄔ黾恿薲ts描述文檔,所以在寫代碼過程中,如果不記得方法名參數(shù)直接通過代碼自動提示來寫就行了(自動提示在webStorm上的體驗(yàn)更好):

提示1.png
提示2.png
提示3.png

react-native-easy-app 詳解與使用之(三) View,Text,Image,F(xiàn)latlist

想進(jìn)一步了解,請移步至 npm 或github查看 react-native-easy-app,有源碼及使用示例,待大家一探究竟,歡迎朋友們 Star!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容