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
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
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
RFHttp().url(‘https://facebook.github.io/react-native/movies.json‘).get((success, json, message, status)=>{ console.log(json.movies) });
通过执行上面三段示例代码,发现输出了一致的结果(电影列表数组):
通过对比发现 RFHttp 的使用与React Native平台提供的fetch很相似,其execute(‘get‘)方法返回的是一个promise对象,故也可以像fetch一样,发送同步或异步请求。另外还可以通过[method]+回调的形式发送请求。
相比原生fetch请求,RFHttp 却返回了多个参数,我们打印一下示例2中的response看看里面都有啥?输出结果,格式化后如下:
- success => [true | false] 请求成功或失败的标识(默认以Http的请求状态码:[ status >= 200 && status < 400 ] 作为判断依据)。
- json => [Json Object | originText] 默认为请求返回的json对象,必要时可以指定返回纯文本字符串(若请求结果为非标准Json,如XML结构或其它)或通过自定义配置指定请求返回的数据结构。
- message 默认情况下,请求成功时:为[code+url],失败时:则为错误信息[错误信息+code+url],若开发者指定了特定的解析方式,则由开发者制定。
- 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请求了,剩下的框架会处理:
- 需求 3:能支持超时设置;支持日志打印;支持返回非标准Json以及baseUrl的拼接
- 请求超的原理是通过 Promise.race 实现;
- 1.由于超时请求并不完全属于某个特定的请求,故引入了一个公共配置对象:RFHttpConfig,开发者可以通过两种试设置请求超时配置,如下:
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) => { })
可以看出控制台打印出了详细的日志,是不是很方便?
- 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() 指定返回的数据以纯文本返回):
- 4、至于baseUrl的拼接,则是为了在App开发中,减少不必要的baseUrl的重复使用(程序通过判断传入的url是否是完整按需拼接BaseUrl),使用方法如下:
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数据。
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请求,执行代码我们看看控制台日志内容:
通过控制台打印的日志,我们可以很清晰的看到,参数从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请求一下:
返回的数据格式如下:
{ "ticker": { "base": "BTC", "target": "USD", "price": "5301.78924881", "volume": "179358.70555921", "change": "-21.18183054" }, "timestamp": 1584291183, "success": true, "error": "" }
可以看出,接口返回的数据结构中,有三个主要字段:
- success 接口逻辑成功与失败的判断依据。
- error 接口若失败时,包含错误信息。
- ticker 接口返回的主要数据的主体。
以前面RFHttp发送请求,接口的成功与否的判断依然是http的status来判断,显示达不到要求,请求cryptonator.com网站api数据统一解析的基本要求,那怎么自定义数据解析呢?我们试试看。
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个标准参数的内容:
发现没有,json对应的值就是返回的数据结构中:ticker对应的数据。其它字段并不能反映出来,因为数据刚好与默认判断条件吻合或为空。这是怎么实现的呢?
因为通过RFHttpConfig的initParseDataFunc方法,我们重新定义了,接口请求返回的标准字段的值:
- success => success && json.success 只有当接口请求与返回的成功标记同时为true的时候才认为是成功
- json => json.ticker 直接读取json.ticker的值(若为空,则返回一个没有任何属性对象)
- message => json.error || message 优先获取接口返回的错误信息(若为空,则读取Http请求的错误信息)
- 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) } })
接口请求打印的日志为:
请求依然成功,各参数也没有问题,因为在发送Http请求的时候增加了一个标记rawData(),这个标记就是用于特殊处理的,标记当前Http请求需要返回原始的,不做任何解析的数据(设置此标记,会自动忽略用户自定义的数据解析方式)
- 办法二(也有可能一个App要请求多个不同的平台或者新老版本过渡,而且不同风格的接口数量还不在少数),同时在这种情况下可能请求的参数风格,公共参数也有不同的要求,这就更复杂了,这种情况能否处理?答案是肯定的:
假设当前App要请求三个平台:分别为SA,SB,SC,这三个平台要求不同的公共参数(包括header),且返回的数据结构也完全不一致,这时候我们可以这样处理:
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