V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
rufeng008
V2EX  ›  程序员

二十分钟封装,一个 App 前后台 Http 交互的实现

  •  
  •   rufeng008 · 2020-05-11 10:28:19 +08:00 · 1475 次点击
    这是一个创建于 1664 天前的主题,其中的信息可能已经有所发展或是发生改变。

    在 React Native 开发过程中,几乎所有的 app 都需要使用到 Http 请求,所以 fetch 的封装必不可少,由于不同 app 的请求参数,解析规则,token 机制等完全不一样,所以在大多数 App 开发中,前后台 Http 请求的实现都是开发者自己封装的。

    封装一个前后台 Http 请求实现需要多久?

    可能有人回答是 1 小时,也有 3 、5 小时甚至更长时间的,或者也有说先这样封装个大概,等到需求不满足的时候再改。

    花费 1 小时的时间不一定短,花费 3 、5 的时间也不一定算长,具体要看前后台交互的复杂程度与开发者对交互实现的封装程度。

    那这里我们就引出了一个问题了,我们通常说的 app 的 Http 请求 [封装] ,到底封装的是什么,我们需要做哪些工作,能使用得 app 的接口请求更简单,易用且有较高的灵活性?在我看来这个“封装”主要分两个部分:

    1. 数据交换 层面的封装,即:

      • 实现前后台的互通,支持服务器要求的数据交换类型、格式等
      • 调用者可以自由设置请求的 header 、params 等参数,程序根据不同的设置也能保证请求能正确的发送给服务端并返回相应的结果
      • 支持超时、日志打印等一些基本功能
    2. 业务逻辑 层面的封装,即:

      • 入参:公共部分 header 、params 的参数处理,避免在具体接口请求是传入不必要与接口无关的参数
      • 出参:对后台返回的数据按约定好的规则做一层基础解析处理,避免在具体接口数据解析的时候做一些无意义的操作

    从投入的时间上来看: 第一部分基本上要花掉开发者 80%以上的时间来封装 第二部分需要消耗的时间可能不足 20% ** [以此推算,按上面 1 个小时的封装时间,用在逻辑封装部分的时间也就 12 分钟左右😝] **

    我们再回头看一下,第一部分的 [数据交换] 封装是否涉及到具体业务逻辑呢?答案是:没有。

    既然没有我们为什么不把第一部分的封装交给第三方框架呢,我们只需要做第二部分的封装多省事,有这样第三方框架么?

    答案是:有的,react-native-fast-app 就可以实现 [前后台数据交换] 层面的封装,通过这个开源库,我们就只需要实现涉及 [ App 业务逻辑] 层面的封装即可。


    为验证 react-native-fast-app 的实用性,在这里我们先来构想一个业务逻辑层面封装的需求:

    1. 请求接口的公共 headers 参数有:

      • version 、channelCode 、model 、platform (所有接口)
      • accessToken 、refreshToken 、customerId (登录后额外增加)
    2. 请求接口的公共 params 参数有:

      • customerId (登录后额外增加)
    3. 后台返回的数据结构示例如下: { data: {}, successful:1, msg: 'request msg', code: 'xxx'}

    4. 请求状态码为 503 的时候表示 accessToken 过期,accessToken 过期的情况下,需要重新获取新的 accessToken 并刷新因 accessToken 过期导致请求失败的接口

    5. accessToken 、refreshToken 在登录成功后的 response 的 headers 中返回。

    对于以上业务逻辑层面的需求,看看通过 react-native-fast-app 我们可以怎么做。

    RFHttpConfig().initHttpLogOn(true)
        .initHeaderSetFunc((headers) => {
            headers['model'] = 'xiao mi';
            headers['version'] = '1.0.0';
            headers['platform'] = Platform.OS;
            headers['channelCode'] = 'channelOfficial';
            if (isLogin()) {
                headers['customerId'] = RNStorage.customerId;
                headers['accessToken'] = RNStorage.accessToken;
                headers['refreshToken'] = RNStorage.refreshToken;
            }
        })
        .initParamSetFunc(params => {
            if (isLogin()) {
                params['customerId'] = RNStorage.customerId;
            }
        })
        .initParseDataFunc((result, request, callback) => {
            let {success, json, message, status, response} = result;
            if (status === 503) {// accessToken 过期标记
                this.refreshToken(request, callback);
            } else {
                let {data, successful, msg, code} = json;
                callback(success && successful === 1, data || {}, msg || message, code, response);
            }
        });
    

    accessToken 重新请求的实现及对失败接口的刷新:

    refreshToken = (request, callback) => {
        if (global.hasQueryToken) {
            global.tokenExpiredList.push({request, callback});
        } else {
            global.hasQueryToken = true;
            global.tokenExpiredList = [{request, callback}];
            const refreshUrl = `${RNStorage.baseUrl}api/refreshToken?refreshToken=${RNStorage.refreshToken}`;
            fetch(refreshUrl).then(resp => {
                resp.json().then(({successful, data: {accessToken}}) => {
                    if (successful === 1) {// 获取到新的 accessToken
                        RNStorage.accessToken = accessToken;
                        global.tokenExpiredList.map(({request, callback}) => {
                            request.resendRequest(request, callback);
                        });
                        global.tokenExpiredList = [];
                    } else {
                        console.log('Token 过期,退出登录');
                    }
                });
            }).catch(err => {
                console.log('Token 过期,退出登录');
            }).finally(() => {
                global.hasQueryToken = false;
            });
        }
    };
    

    就这样对当前构想的 app 的逻辑层面的封装就实现了**(实现上面的代码约 70 行,也许要超过 20 分钟 😆😝,但相较于以前从零开的封装,是不是节约了大量的时间呢?)**是不是清晰明了。当然,这只是代码片段,没有实际操作,就没办法证明上面的代码实现是实际有效的。


    为了演示,先用 react native init HttpTestDemo 创建一个 RN 项目:示例项目:HttpTestDemo 修改并删除不必要的布局或资源,结果如下:

    image.png

    假定有三个接口,分别为 api/login 、api/userInfo 、api/refreshToken (为了省事,接口都以 json 文件替代

    • api/login 有两个必传参数:[userName 、userPass];请求内容类型为:application/x-www-form-urlencoded ; post 请求
    • api/userInfo 无参数;请求内容类型为:application/json ; get 请求
    • api/refreshToken 必须参数 refreshToken ;请求内容类型为:application/json ; get 请求

    https://react-native-fast-app.oss-cn-beijing.aliyuncs.com/api/login https://react-native-fast-app.oss-cn-beijing.aliyuncs.com/api/userInfo https://react-native-fast-app.oss-cn-beijing.aliyuncs.com/api/refreshToken

    1. react-native-fast-app 的说明文档,安装库:npm install react-native-fast-app --save

    2. 定义一个持久化对象,用于保存 accessToken,customerId 等参数:

    export const RNStorage = {// 持久化数据列表
        customerId: undefined,//客户 ID
        accessToken: undefined,//OAuth2.0 accessToken
        refreshToken: undefined,//OAuth2.0 refreshToken
        baseUrl: undefined,
        userInfo: undefined,
        hasLogin: false,
    };
    

    3.在页面的构造方法时调用 RNStorage 的初始化操作;初始化完成之后,调用 Http 请求 RFHttpConfig 的 [业务逻辑] 层初始化方法,这样就完成了,现在就可以调用接口了。

    • 调用登录接口:(由于使用 json 文件的形式只能使用 get 请求)
    import { RFHttp } from 'react-native-fast-app';
    
    login = () => {
        let params = {userName: 'zhangsan', userPass: '123456a'};
        RFHttp().url('api/login').param(params).formEncoded().get((success, json, message, status, resonse) => {
            if (success) {
                if (resonse.headers && resonse.headers.map) {
                    RNStorage.accessToken = resonse.headers.map['x-oss-meta-accesstoken'];
                    RNStorage.refreshToken = resonse.headers.map['x-oss-meta-refreshtoken'];
                }
                RNStorage.customerId = json.customerId;
                RNStorage.hasLogin = true;
                this.setState({data: JSON.stringify(json)});
            } else {
                console.log('失败', message);
            }
        });
    };
    

    调用接口,通过框架自带的日志功能,可以看到,该拼的参数都拼接了,从 header 中也获取到了 token

    image.png

    • 调用获取用户个人信息接口:
    import { RFHttp } from 'react-native-fast-app';
    
    queryUserInfo = () => {
        RFHttp().url('api/userInfo').formJson().get((success, json, message) => {
            if (success) {
                RNStorage.userInfo = json;
                this.setState({data: JSON.stringify(json)});
            } else {
                console.log('失败', message);
            }
        });
    };
    

    调用接口,通过框架自带的日志功能,可以看到 accessToken 、refreshToken 也正确的拼接了。

    image.png

    由于没有合适的服务器,token 过期的情况就不演示了,只要请求 refreshToken 的接口正常请求就不会有问题。

    至此一个完整的 App [业务逻辑] 层面的封装就完全实现了,从 Http 请求的配置到,refreshToken 的重新请求到刷新失败接口,一共大概只用了 70 行代码左右,是不是相较于之前从零开始的 fetch 封装简单容易多了,节约了大量的封装时间呢?

    担心框架的灵活性?请参考 react-native-fast-app 详解与使用之(二) fetch 并且 react-native-fast-app 开源库并不只有 Http 请求的封装,还有更多功能,有兴趣的同学可以查看此栏目的其它文章,你肯定会有更多收获。

    当前示例项目链接:HttpTestDemo

    如果有任何疑问,欢迎扫码加入 RN 技术 QQ 交流群

    1531585724192_.pic_hd.jpg

    2 条回复    2020-05-11 15:29:30 +08:00
    wudalang123
        1
    wudalang123  
       2020-05-11 15:02:14 +08:00
    说的时候应该举出一个具体应用实例
    rufeng008
        2
    rufeng008  
    OP
       2020-05-11 15:29:30 +08:00
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2639 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 20ms · UTC 10:59 · PVG 18:59 · LAX 02:59 · JFK 05:59
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.