目录
一. 网络编程
二. 数据持久化
1.
AsyncStorage
是什么2. 怎么使用
AsyncStorage
三. 离线缓存的实现
1. 为什么要做离线缓存
2. 几种离线缓存策略
3. “优先读取缓存型”策略的实现
一. 网络编程
RN的网络编程除了普通的网络请求,还支持WebSocket,这种协议可以在单个TCP连接上提供全双工的通信信道。我们可以在用到时,去做详细地学习。本篇只学习RN网络编程的普通网络请求。
-
fetch
方法发起请求
RN里,我们用fetch
方法从服务端请求数据或者给服务端上传数据,它的API很简单。
Promise fetch(url, configs);
它可以接收两个参数:第一个参数必填,字符串,是请求的url
。第二个参数可选,JS对象,是请求的一些配置,如你可以指定请求的方式、请求头、请求体(即你要上传给服务端的数据)。它天然是一个异步操作,所以执行完后返回一个Promise。
举个例子。
Promise promise = fetch('https://mywebsite.com/endpoint/', {
// 请求的方式
method: 'POST',
// 请求头
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
// 请求体
body: JSON.stringify({
firstParam: 'yourValue',
secondParam: 'yourOtherValue',
}),
});
注意:
我们上传给服务端数据的格式取决于
headers
中的Content-Type
。Content-Type
有很多种,因此对应body
的格式也有区别。到底应该采用什么样的Content-Type
取决于服务器端,所以请和服务器端的开发人员沟通确定清楚。
RN默认的请求方式为GET
,用GET
请求时,我们可以不用写请求方式和请求体,请求头按需配置,因此一个最简单的GET
请求如下。
Promise promise = fetch('https://mywebsite.com/mydata.json?firstParam=yourValue&secondParam=yourOtherValue');
- 处理服务端返回的数据
上面我们说到fetch
方法是个天然的异步操作,它执行后会返回一个Promise,我们正是用该Promise对象来处理服务端返回的数据。
举个例子。
fetch('https://facebook.github.io/react-native/movies.json')
.then(response => {
if (response.ok) {
// 请求到的response其实是一个Response对象,它是一个很原始的数据格式,我们不能直接使用,先获取它的JSON字符串文本格式
return response.text();
}
throw new Error('网络请求失败!');
})
.then(responseText => {
// 然后把JSON字符串序列化为JS对象
const responseJSObj = JSON.parse(responseText);
console.log(responseJSObj.movies);
})
.catch(error => {
console.error(error);
});
上面代码用Promise对象的then
方法处理服务端响应成功时的数据,用catch
方法处理服务端响应失败时错误信息。但是要注意只有请求被阻止或者网络故障时才属于触发catch
这种情况,即便是当服务端返回的响应是404或500,也不会走catch
方法,而是走then
方法,只不过此时response
的ok
属性值为false,所以我们需要在then
方法里处理服务端响应成功数据时做一下判断。
- 我们可以简单封装一个网络请求的工具类。
// ProjectRequest.js
export default class ProjectRequest {
static get(url) {
return new Promise((resolve, reject) => {
fetch(url)
.then(response => {
if (response.ok) {
return response.text();
} else {
throw new Error('网络请求失败!');
}
})
.then(responseText => {
const responseJSObj = JSON.parse(responseText);
resolve(responseJSObj);
})
.catch((error) => {
reject(error);
})
})
}
/**
* RN提供的fetch方法,是异步的,它本身就会返回一个Promise对象。但因为这里我们对它进行了封装,所以外面又包了一层Promise,来给fetch这个异步任务提供回调,这样外界才能拿到fetch的结果。
*
* @param url
* @param params
* @returns {Promise<any> | Promise}
*/
static post(url, params) {
return new Promise((resolve, reject) => {
fetch(url, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify(params)
})
.then(response => {
if (response.ok) {
// 请求到的response其实是一个Response对象,它是一个很原始的数据格式,我们不能直接使用,先获取它的JSON字符串文本格式
return response.text();
} else {
throw new Error('网络请求失败!');
}
})
.then(responseText => {
// 然后把JSON字符串序列化为JS对象
const responseJSObj = JSON.parse(responseText);
// 把请求成功的数据传递出去
resolve(responseJSObj);
})
.catch((error) => {
// 把请求失败的信息传递出去
reject(error);
})
})
}
}
二. 数据持久化
1. AsyncStorage是什么
AsyncStorage
是RN里的一种数据持久化方案,它是一个简单的、异步的、Key-Value
存储系统,它对于App来说是全局的,非常类似于我们iOS里的NSUserDefault
。
下面介绍一下它的常用API。
先声明两点:
AsyncStorage
是一个天然的异步操作,所以下面它所有的API执行后,都会返回一个Promise对象。正如JS对象的
key
必须是一个字符串,AsyncStorage
的key
也必须是一个字符串。
- 写入一对
key-value
static setItem(key: string, value: string, callback:(error))
写入一对key-value
,注意写入时value
必须是字符串,非字符串数据必须先转换为字符串后再写入(例如我们要写入的数据是一个数组或JS对象,那就必须先用JSON.stringtify()
方法把它们转换成JSON字符串后再写入)。写入期间如果发生任何错误,会有一个error
作为callback
的第一个参数带出。
- 读取某个
key
对应的value
static getItem(key: string, callback:(error, result))
读取某个key
对应的value
,并将读取到的结果作为callback
的第二个参数带出(自然如果读取的数据是数组或JS对象,因为我们存进去的时候存的是JSON字符串嘛,所以读取的时候就要用JSON.parse()
把它们再转换回数组或JS对象)。读取期间如果发生任何错误,会有一个error
作为callback
的第一个参数带出。
- 删除一对
key-value
static removeItem(key: string, callback:(error))
- 写入多对
key-value
,其中keyValuePairs
是字符串的二维数组,如[['key1', 'value1'], ['key2', 'value2']]
static multiSet(keyValuePairs, callback:(errors))
- 读取多个
key
对应的value
,其中keys
是字符串数组,如['k1', 'k2']
static multiGet(keys, callback:(errors, result))
- 删除多对
key-value
,其中keys
是字符串数组,如['k1', 'k2']
static multiRemove(keys, callback:(errors))
- 获取所有的
key
static getAllKeys(callback:(error, keys))
- 清空
AsyncStorage
static clear(callback:(error))
2. 怎么使用AsyncStorage
原先的导入方法是:
import {AsyncStorage} from "react-native";
但是RN准备从react-native
库中移除AsyncStorage
,推荐我们如下的使用方法:
- 导入
@react-native-community/async-storage
yarn add @react-native-community/async-storage
-
link
一下
react-native link @react-native-community/async-storage
- 导入
AsyncStorage
import AsyncStorage from '@react-native-community/async-storage';
- 写入数据
_whiteData() {
AsyncStorage.setItem('key', 'value', error => {
if (error) {
console.log('写入数据出错:', error);
} else {
console.log('写入数据成功');
}
});
}
- 读取数据
_readData() {
AsyncStorage.getItem('key', (error, value) => {
if (error) {
console.log('读取数据出错:', error);
} else {
console.log('读取数据成功:', value);
}
});
}
- 删除数据
_deleteDate() {
AsyncStorage.removeItem('key', error => {
if (error) {
console.log('删除数据出错:', error);
} else {
console.log('删除数据成功');
}
});
}
三. 离线缓存的实现
离线缓存就是基于网络请求和数据持久化实现的,将请求url
作为key
,将请求到的数据作为value
存在本地。
1. 为什么要做离线缓存
- 提升用户体验
我们无法保证每个用户的网络状况都非常好,能够快速地加载出界面并使用,所以我们可以做离线缓存来提升用户体验。
- 节省流量
节省服务器的流量,比如我们做了一个视频播放的App,所有视频的视频源都存放在我们的服务器上,如果用户看过的视频还要去服务端请求的话,这样对服务器造成的压力是比较大的,特别是访问量大的时候,再者服务器的带宽和流量都是花钱的,所以如果能做个离线缓存的话可以节省服务器的流量。
节省用户手机的流量,用户看多的视频没必要再让人家去请求花一遍流量了。
2. 几种离线缓存策略
常见的离线缓存策略有如下三种。
优先读取缓存型:客户端发起网络请求,我们拿到这个请求,优先读取缓存数据,如果缓存数据存在并且未过期则直接加载,否则从服务端请求最新数据并更新缓存。
优先读取网络型:客户端发起网络请求,总是优先从服务端请求数据,请求到数据后缓存到本地,只有当网络出现故障时才从本地读取缓存数据。
同时读取缓存和网络型:客户端发起网络请求,同时加载缓存和服务端数据,一般来说缓存肯定比服务端数据加载的快,所以其实就是先加载缓存数据,然后等网络数据请求回来后刷新页面的数据并更新本地的缓存数据。(感觉刷的那一下怪怪的)
各种离线缓存策略都有自己的优劣和适用场景,也就是说具体适用哪种离线缓存策略要靠我们来衡量,如果页面数据对实时性要求不是那么高,就使用优先读取缓存数据型,否则可以使用优先读取服务端数据型。
3. “优先读取缓存型”策略的实现
这里编写了一个工具类,针对App中所有的网络请求,实现了“优先读取缓存型”策略,于是以后但凡某个请求需要使用到离线缓存,就可以拿这个类去请求数据了,不用离线缓存的请求可以直接用网络请求的工具类。具体的思路和注释都在代码里。
// ProjectRequestWithCache.js
import AsyncStorage from '@react-native-community/async-storage';
export default class ProjectRequestWithCache {
/**
* 请求数据,优先读取缓存数据型策略的思想就在这个方法里实现
*
* 这里再写一下该策略的思想,方便对比代码理解
* 1、客户端发起网络请求,我们拿到这个请求的url
* 2、优先读取缓存数据
* 读取缓存数据成功,如果缓存数据存在并且未过期,则直接加载,
* 读取缓存数据失败、缓存数据不存在或已过期,则请求网络数据并更新缓存。
*/
static fetch(url) {
// 因为我们是对离线缓存做封装,
// 所以这里就用Promise对象包装了一下异步操作,这样就可以把异步操作成功的数据或失败的信息传递出去,外界就可以通过.then或者.catch来做回调了
return new Promise((resolve, reject) => {
// 优先读取缓存数据
this._fetchLocalData(url)
.then(wrapData => {
// 读取缓存数据成功
if (wrapData && this._checkTimestampValid(wrapData.timestamp)) {// 缓存数据存在并且未过期
// 直接返回并加载
resolve(wrapData.data);
} else {// 缓存数据不存在或已过期
this._fetchNetworkData(url)
.then(data => {
// 网络请求成功
resolve(data);
// 更新缓存
this._writeData(url, data);
})
.catch((error) => {
// 网络请求失败
reject(error);
})
}
})
.catch(error => {
// 读取缓存数据失败
this._fetchNetworkData(url)
.then(data => {
// 网络请求成功
resolve(data);
// 更新缓存
this._writeData(url, data);
})
.catch((error) => {
// 网络请求失败
reject(error);
})
})
});
}
/**
* 读取缓存数据
*/
static _fetchLocalData(url) {
return new Promise((resolve, reject) => {
AsyncStorage.getItem(url, (error, result) => {
if (!error) {
try {
// 因为result是个JSON字符串,所以要序列化为JS对象
// 把读取成功的结果传递出去
resolve(JSON.parse(result));
} catch (e) {
// 此处代表序列化失败
// 把读取失败的信息传递出去
reject(e);
}
} else {
// 把读取失败的信息传递出去
reject(error);
}
})
})
}
/**
* 请求网络数据
*/
static _fetchNetworkData(url) {
return new Promise((resolve, reject) => {
fetch(url)
.then(response => {
if (response.ok) {
// 请求到的response其实是一个Response对象,它是一个很原始的数据格式,我们不能直接使用,先获取它的JSON字符串文本格式
return response.text();
}
throw new Error('网络请求失败!');
})
.then(responseText => {
// 然后把JSON字符串序列化为JS对象
const responseJSObj = JSON.parse(responseText);
// 把请求成功的数据传递出去
resolve(responseJSObj);
})
.catch((error) => {
// 把请求失败的信息传递出去
reject(error);
})
})
}
/**
* 存储数据,作为缓存数据
*
* 请求的url作为key,请求到的数据作为value
*/
static _writeData(url, data, callBack) {
if (!url || !data) return;
const wrapData = this._wrapData(data);
// JSON.stringify为字符串
AsyncStorage.setItem(url, JSON.stringify(wrapData), callBack);
}
/**
* 给原数据包裹一个时间戳,以便用来检查缓存数据是否过期
*/
static _wrapData(data) {
return {data: data, timestamp: new Date().getTime()};
}
/**
* 检查缓存数据是否过期,缓存有效期为4个小时
*/
static _checkTimestampValid(timestamp) {
const currentData = new Date();
const targettData = new Date();
targettData.setTime(timestamp);
// 月
if (currentData.getMonth() !== targettData.getMonth()) return false;
// 日
if (currentData.getDate() !== targettData.getDate()) return false;
// 时
if (currentData.getHours() - targettData.getHours() > 4) return false;
return true;
}
}
使用举例。
constructor(props) {
super(props);
this.state = {
data: {},
};
}
_loadData() {
const url = `https://api.github.com/search/repositories?q=${this.searchText}`;
ProjectRequestWithCache.fetch(url)
.then(data => {
this.setState({
data: data,
});
})
.catch(error => {
alert(error);
})
}