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

目录

一. 网络编程
二. 数据持久化
 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-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也必须是一个字符串。

  • 写入一对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);
        })
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容