RN:网络编程、数据持久化与离线缓存的实现

2019-08-28  本文已影响0人  意一ineyee

目录

一. 网络编程
二. 数据持久化
 1. AsyncStorage是什么
 2. 怎么使用AsyncStorage
三. 离线缓存的实现
 1. 为什么要做离线缓存
 2. 几种离线缓存策略
 3. “优先读取缓存型”策略的实现

一. 网络编程


RN的网络编程除了普通的网络请求,还支持WebSocket,这种协议可以在单个TCP连接上提供全双工的通信信道。我们可以在用到时,去做详细地学习。本篇只学习RN网络编程的普通网络请求。

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-TypeContent-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方法,只不过此时responseok属性值为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必须是一个字符串,AsyncStoragekey也必须是一个字符串。

static setItem(key: string, value: string, callback:(error))

写入一对key-value,注意写入时value必须是字符串,非字符串数据必须先转换为字符串后再写入(例如我们要写入的数据是一个数组或JS对象,那就必须先用JSON.stringtify()方法把它们转换成JSON字符串后再写入)。写入期间如果发生任何错误,会有一个error作为callback的第一个参数带出。

static getItem(key: string, callback:(error, result))

读取某个key对应的value,并将读取到的结果作为callback的第二个参数带出(自然如果读取的数据是数组或JS对象,因为我们存进去的时候存的是JSON字符串嘛,所以读取的时候就要用JSON.parse()把它们再转换回数组或JS对象)。读取期间如果发生任何错误,会有一个error作为callback的第一个参数带出。

static removeItem(key: string, callback:(error))
static multiSet(keyValuePairs, callback:(errors))
static multiGet(keys, callback:(errors, result))
static multiRemove(keys, callback:(errors))
static getAllKeys(callback:(error, keys))
static clear(callback:(error))

2. 怎么使用AsyncStorage

原先的导入方法是:

import {AsyncStorage} from "react-native";

但是RN准备从react-native库中移除AsyncStorage,推荐我们如下的使用方法:

yarn add @react-native-community/async-storage
react-native link @react-native-community/async-storage
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);
        })
}
上一篇 下一篇

猜你喜欢

热点阅读