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

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

react-native-fast-app 主要做了這些工作:
1. 對AsyncStorage進行封裝,開發者只需幾行代碼即可實現一個持久化數據管理器。
2. 對fetch進行封裝,使得開發者只需關注當前App的前後臺交互邏輯和協議,定義好參數設置及解析邏輯即可。
3. 重新封裝了RN的View、Text、Image、FlatList 使用得這些控件在適當的時候支持事件或支持icon與文本,能有效減少佈局中的嵌套邏輯。
4. 可通過配置,當前庫重定義的組件支持[尺寸屬性] 按設定的參考屏幕尺寸進行等比縮放,去適配在不同屏幕分辨下的樣式。

可能有人覺得,不同的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-fast-app的請求 RFHttp是不是也可以方便快捷的發送請求呢?

  • 異步請求(核心代碼) 示例 1
import {  RFHttp } from 'react-native-fast-app';

 RFHttp().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 {  RFHttp } from 'react-native-fast-app';

  const response = await RFHttp().url('https://facebook.github.io/react-native/movies.json').execute('GET');
  const {success, json, message, status} = response;
  console.log(json.movies)
  • 異步請求2(核心代碼)示例 3
import {  RFHttp } from 'react-native-fast-app';

   RFHttp().url('https://facebook.github.io/react-native/movies.json').get((success, json, message, status)=>{
       console.log(json.movies)
   });

通過執行上面三段示例代碼,發現輸出了一致的結果(電影列表數組):

movies.png

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

相比原生fetch請求,RFHttp 卻返回了多個參數,我們打印一下示例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-fast-app 的 RFHttp 可以像使用fetch一樣方便快捷的發送Http請求,而且還包含請求碼,錯誤信息,結果也被轉化爲了json對象,使用我們發送請求更加方便了。

但在實際的App開發中,我們Http請求框架的要求不只是能發送簡單的Http請求就可以了,比如說,需要打印請求日誌、設置header參數、統一處理解析邏輯,甚至可能處理返回的結構不是標準的json數據等各種需求。

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

  • 需求 1 :能支持get、post、put、delete等基本常用類型的請求
    • 框架會自動根據輸入的請求類型,自動會處理請求的body有無問題
    • 1、通過RFHttp 的execute(‘method’)方式發送請求自然是沒有問題
    • 2、通過method + 回調的形式(滿足90%的情況),我問下的情況怎麼辦?不用擔心框架提供了另一種方式實現,即:
RFHttp().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、 RFHttp 有三種方式設置contentType,三種常用的方式被提取了出來,如下分別是:直接設置;通過header設置;通過方法直接指定。開發者設置了相應的方式之後,就可以放心的發送Http請求了,剩下的框架會處理:

contentType.png


  • 需求 3:能支持超時設置;支持日誌打印;支持返回非標準Json以及baseUrl的拼接
    • 請求超的原理是通過 Promise.race 實現;
    • 1.由於超時請求並不完全屬於某個特定的請求,故引入了一個公共配置對象:RFHttpConfig,開發者可以通過兩種試設置請求超時配置,如下:
import { RFHttpConfig } from 'react-native-fast-app';

RFHttpConfig.initTimeout(300000); //全局配置,設置所有Http請求的超時時間爲30秒

RFHttp().url('https://facebook.github.io/react-native/movies.json').timeout(15000) //設置當前請求超時間爲15秒
    .get((success, json, message, status) => {
    })
  • 2、日誌打印也是通過 RFHttpConfig.initHttpLogOn(true) 設置爲 true 即可,設置完成後,我們發送請求,看看控制檯的輸出日誌:
RFHttpConfig.initHttpLogOn(true);
RFHttp().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'
RFHttp().url(url).pureText().get((success, text, message, status) => {
    console.log('XML data', text)
})

控制檯輸出結果如下(通過RFHttp的 pureText() 指定返回的數據以純文本返回):
httpXml.png

  • 4、至於baseUrl的拼接,則是爲了在App開發中,減少不必要的baseUrl的重複使用(程序通過判斷傳入的url是否是完整按需拼接BaseUrl),使用方法如下:
import { RFHttpConfig, RFHttp } from 'react-native-fast-app';

RFHttpConfig.initBaseUrl('http://www.webxml.com.cn/WebServices/');
RFHttp().url('MobileCodeWS.asmx/getDatabaseInfo').get((success, text, message, status) => {
    console.log('XML data', text)
})

  • 需求 4:能自由設置公共的params、headers;發送Http請求的時候,也能自由設定當前請求的header及param數據。
import { RFHttpConfig, RFHttp } from 'react-native-fast-app';

RFHttpConfig.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';
    });

RFHttp().url('react-native/movies.json')
    .header({'Content-Type': 'application/json', header_type: 'header_type006'})
    .param({paramUserName: 'paramUserName007', testChannel: 'testChannel008'})
    .post((success, text, message, status) => {
    })

從代碼中可以看出通過RFHttpConfig配置,我們設置了公共的heders、params,然後在通過RFHttp發送請求時,又設置了特定的header和param的值,同時了修改了contentType的類型,並改爲post請求,執行代碼我們看看控制檯日誌內容:

common_params.png

通過控制檯打印的日誌,我們可以很清晰的看到,參數從001~008所有的參數(除了005)都能有效設置到請求當中。但爲什麼公共參數 params.testChannel = ‘testChannel005’; 的設置沒有生效呢,其實是因爲,RFHttp中的接口請求的私有參數中也設置了一個: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 接口返回的主要數據的主體。

以前面RFHttp發送請求,接口的成功與否的判斷依然是http的status來判斷,顯示達不到要求,請求cryptonator.com網站api數據統一解析的基本要求,那怎麼自定義數據解析呢?我們試試看。

import { RFHttpConfig, RFHttp } from 'react-native-fast-app';

RFHttpConfig.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);
    });

RFHttp().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對應的數據。其它字段並不能反映出來,因爲數據剛好與默認判斷條件吻合或爲空。這是怎麼實現的呢?

因爲通過RFHttpConfig的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不只是請求一個後臺或者還要請求第三方接口,不同的後臺返回的數據結構也完全不一樣,這種情況下麼處理?不用擔心,這種情況也是有解的:

  • 辦法一(非標準接口較少的情況):
    比如說,我的請求以cryptonator.com網站的api爲主,偶爾要請求域名查詢接口:
    https://api.domainsdb.info/v1/domains/search?domain=zhangsan&zone=com,這個時候,我可以依然保持前面的自定義解析方式不變,在請求域名查詢的時候,增加一個標記:
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 { RFHttpConfig, RFHttp } from 'react-native-fast-app';

RFHttpConfig.initHttpLogOn(true)
    .initHeaderSetFunc((headers, request) => {
        switch (request.extra.type) {
            case 'SA':
                headers.type = 'SA headers';
                break;
            case 'SB':
                headers.type = 'SB headers';
                break;
            case 'SC':
                headers.type = 'SC headers';
                break;
        }
    })
    .initParamSetFunc((params, request) => {
        switch (request.extra.type) {
            case 'SA':
                headers.type = 'SA params';
                break;
            case 'SB':
                headers.type = 'SB params';
                break;
            case 'SC':
                headers.type = 'SC params';
                break;
        }
    })
    .initParseDataFunc((result, request, callback) => {
        let {success, json, message, status} = result;
        switch (request.extra.type) {
            case 'SA':
                headers.type = 'SA params';
                callback(success && json.success, (json.tickerSA || {}), (json.errorA || message), status);
                break;
            case 'SB':
                headers.type = 'SB params';
                callback(success && json.success, (json.tickerSB || {}), (json.errorB || message), status);
                break;
            case 'SC':
                headers.type = 'SC params';
                callback(success && json.success, (json.tickerSC || {}), (json.errorC || message), status);
                break;
        }
    });

RFHttp().url('https://facebook.github.io/react-native/movies.json')
    .extra({type: 'SA'})
    .get((success, json, message, status) => {
    });
RFHttp().url('https://facebook.github.io/react-native/movies.json')
    .extra({type: 'SB'})
    .get((success, json, message, status) => {
    });
RFHttp().url('https://facebook.github.io/react-native/movies.json')
    .extra({type: 'SC'})
    .get((success, json, message, status) => {
    });

不過,這種方式太過異類,extra作爲RFHttp的擴展字段提供了更多的靈活性。當然,平常來說,也不一定得用extra,畢竟這種方式可讀性也不強,而且判斷不同類別可能使用baseUrl或者interface就夠了。在情況只有兩種的時候,可以使用RFHttp.internal(true),分別用於標記是否爲站內請求(App請求或者第三方請求),這樣有較強的可讀性。

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


可能部分同學覺得,框架的參數設置挺方便,但數據的解析我完全想完全自己實現可以麼?當然可以,通過fetch方法,返回的是原fetch請求的promise,框架不做任何處理:

parse_native.png

也有同學可能想到有一種應用場景oauth2需要特別處理:

  1. 發送請求req1,因爲accessToken失效而請求失敗
  2. 程序通過refreshToken重新獲取到了新的accessToken
  3. 拿着新的accessToken重新請求req1

這種應用場景怎麼處理呢?

RFHttpConfig.initParseDataFunc(async (result, request, callback) => {
        let {success, json, response, message, status} = result;
        AuthToken.parseTokenRes(response);//解析token
        if (status === 503) {//指定的Token過期標記
            if (isEmpty(RNStorage.refreshToken) || isEmpty(RNStorage.customerId)) {
                showToast('Token過期,退出登錄');
            }
            await AuthToken.getAccessToken().then(() => {//獲取到新Token後,retry失敗的接口
                request.resendRequest(request, callback);
            });
        } else {
            let {successful, msg, code} = json;
            callback(success && successful === 1, selfOr(json.data, {}), selfOr(msg, message), code);
        }
    });

在這裏我就不做詳細說明了直接貼代碼,詳細的請大家可以直接閱讀源碼或者參考 react-native-fast-app 庫對應的 示例項目,至於原理是:在請求的時候,將初請求的方法引用保存到了request中,並命名爲resendRequest,若獲取到新的token之後,重新請求一遍resendRequest方法,傳入原來的參數即可。


可能有同學覺得react-native-fast-app封裝RFHttp與RFHttpConfig的方法與參數太多了,根本沒辦法記住,框架雖好卻不便於使用,這個目前可能需要大家參考示例項目來寫了(後面我會完善說明文檔)。

當然大家有沒有發現,在使用這些庫方法的時候,代碼有提示呢?那就對了。因爲我爲主要的方法增加了dts描述文檔,所以在寫代碼過程中,如果不記得方法名參數直接通過代碼自動提示來寫就行了(自動提示在webStorm上的體驗更好):

提示1.png

提示2.png

提示3.png

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章