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

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

作者: 意一ineyee | 来源:发表于2019-08-28 11:10 被阅读0次

    目录

    一. 网络编程
    二. 数据持久化
     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);
            })
    }
    

    相关文章

      网友评论

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

          本文链接:https://www.haomeiwen.com/subject/acrpectx.html