x-http-wrapper: 如何解决每次发版时,修改 http 相关代码造成的错误!(Android、iOS、h5)

2017-01-16 20:45:17 +08:00
 xuyt

其实是我做了个开源工具(^__^),拿出来给大家鉴赏下,欢迎大家提意见
项目: https://github.com/xuyt11/x-http-wrapper 欢迎关注和 star 。
功能:这是一个 http 相关代码的创建工具。

现在我们每一次发版,基本上都会涉及到 http 相关的修改,以此来满足发版的业务需求。
而在其中需要添加或修改的有 http request 、 http request param 、 http response entity 等其他相关的 http 代码。
而在多次的修改中,若前后端没有协调好,就有可能会造成之后的返工、重复修改与线上 bug 量的增加等问题。

现在的痛点

如何解决每次发版时,都需要新增、修改 http 相关代码!
如何解决每次发版时,修改 http 相关代码造成的错误!

解决思路: 规范

  1. 其实很简单,就是一个词“规范”,任何事情,只要我们有了一定的规范,就会有一定的流程、可追踪并且降低难度。
    我相信 99.99%的公司,都会有相关的 http 接口文档提供给前端同学,而且也会自己的一套规范(不论是我现在依赖的 apidocjs ,还是上家公司的 doc 文件)。
    当然,肯定也有口头约定的情况,但这需要在之后,立即将约定转化为文档,提供给前端的同学。 git 、 http 都可以作为提供的形式。

  2. 我们依赖这个 http 的规范,就可以将 http 接口文档去解析转义为 x-http-wrapper 内部的 API 数据。

  3. 再来就是依赖一定的规范(x-http-wrapper 的模板文件规范),将内部 API 数据转化为 http 相关文件。这样,每次只要接口文档更新过后,我们就可以根据文档生成各个程序内部可以运行的代码。
    这个功能与现在 IDE 中的 getter 、 setter 方法生成器功能其实是相同的原理!

x-http-wrapper 介绍

  1. 这是一个 http 相关代码的创建工具。
  2. 现在能创建的 http 相关的文件类型有: http 请求分类, http 请求,请求方法参数,响应实体,响应实体中状态码列表和基础响应实体类。
    • HttpApi( http 请求分类): 所有 API 请求的统一调用入口,统合所有的请求类别的接口,防止 API 接口分散。
    public class HttpApi {
    
       private static Account account;
       private static Data data;
       private static Message message;
    
       public static Account account() {
           if (null == account) {
               account = Account.getInstance();
           }
           return account;
       }
    
       public static Data data() {
           if (null == data) {
               data = Data.getInstance();
           }
           return data;
       }
    
       public static Message message() {
           if (null == message) {
               message = Message.getInstance();
           }
           return message;
       }
    }
    
    • Request( http 请求): 单个请求分组中,所有的请求方法。
    public class Account extends BaseApi {
    
        public static Account getInstance() {
            return Helper.instance;
        }
    
        private static class Helper {
            public static final Account instance = new Account();
        }
    
        private Account() {
            super();
        }
    
        /**
         * @version 2.0.0
         * @requestUrl 
         * @title 初始化账号信息
         *
         */
        public RequestHandle init(Context cxt001,
            ResponseHandlerInterface response) {
            // hide implementation
        }
    
        /**
         * @version 2.0.0
         * @title 扫二维码到 web 端进行操作
         *
         * @param context String desc
         * @param project_id isOptional Integer desc
         * @param scene isOptional String desc
         * @param uuid_rand String desc
         */
        public RequestHandle qrcodeConfirm(Context cxt001,
            String context, Integer project_id, String scene, String uuid_rand, 
            ResponseHandlerInterface response) {
            // hide implementation
        }
    
        /**
         * 缩略请求方法
         */
        public RequestHandle qrcodeConfirm(Context cxt001,
            QrcodeConfirmRP.Parameter parameter, 
            ResponseHandlerInterface response) {
            return qrcodeConfirm(cxt001,
            parameter.context, parameter.project_id, parameter.scene, parameter.uuid_rand, 
            response);
        }
    
    }
    
    • RequestParam(请求方法参数): 请求参数分组归类,对应单个请求,用于请求参数较多的情况,生成请求参数的分类实体类(请求参数也肯能有多个分类),减少请求方法的输入参数。
    /**
     * 请求方法参数
     */
    public class QrcodeConfirmRP implements Serializable {
    
       public static final class Parameter implements Serializable {
           /**
            * type: String<br>
            * isOptional : false<br>
            * desc: <p>扫码场景,枚举值</p>
            */
           public String context;
           /**
            * type: Integer<br>
            * isOptional : true<br>
            * desc: <p>业务参数: 根据 context 的不同而不同</p>
            */
           public Integer project_id;
           /**
            * type: String<br>
            * isOptional : true<br>
            * desc: <p>身份信息: 服务端会优先使用客户端传入的身份信息,当为”投资人“的时候必传</p>
            */
           public String scene;
           /**
            * type: String<br>
            * isOptional : false<br>
            * desc: <p>从二维码扫描得到的唯一码</p>
            */
           public String uuid_rand;
       }
    
    }
    
    • Response(响应实体): 请求的相应数据 model
    public class Init {
    
        private long member_id;
        private long member_role;
        private long member_status;
        private String ry_token;
        private long step;
    
        public long getMemberId() {return member_id;}
        public long getMemberRole() {return member_role;}
        public long getMemberStatus() {return member_status;}
        public String getRyToken() {return ry_token;}
        public long getStep() {return step;}
        public void setMemberId(long member_id) {this.member_id = member_id;}
        public void setMemberRole(long member_role) {this.member_role = member_role;}
        public void setMemberStatus(long member_status) {this.member_status = member_status;}
        public void setRyToken(String ry_token) {this.ry_token = ry_token;}
        public void setStep(long step) {this.step = step;}
    
    }
    
    • StatusCode(响应实体中状态码列表): 响应中所有状态码的枚举类
    public class StatusCode {
    
        /** '') */
        public static final int OK = 0;
    
        /** '登录状态已过期,请重新登入') */
        public static final int UNAUTHORIZED = 101;
    
        /** '您没有权限查看') */
        public static final int FORBIDDEN = 102;
    
        /** '资源未找到') */
        public static final int NOT_FOUND = 103;
    
        /** '客户端请求错误') # 4XX 客户端错误 */
        public static final int CLIENT_ERROR = 228;
    
        /** '服务器错误') # 5XX 服务器错误 */
        public static final int SERVER_ERROR = 229;
    
        /** '参数错误') */
        public static final int PARAM_ERROR = 230;
    
        /** '登录失败,请检查您的邮箱地址是否正确') */
        public static final int LOGIN_FAIL_EMAIL_NOT_EXIST = 332;
    
        /** '登录失败,请确认您的手机号是否正确') */
        public static final int LOGIN_FAIL_MOBILE_NOT_EXIST = 333;
    
        /** '登录失败,请检查密码是否正确') */
        public static final int LOGIN_FAIL_PASSWORD_ERROR = 334;
    
    }
    
    • BaseResponse(基础响应实体类): 基础的响应实体类
    public class ResponseEntity<T> {
    
        private int status_code;
        private String message;
        private Error error;
        private T data;
    
        public int getStatusCode() {return status_code;}
        public void setStatusCode(int status_code) {this.status_code = status_code;}
        public String getMessage() {return message;}
        public void setMessage(String message) {this.message = message;}
        public Error getError() {return error;}
        public void setError(Error error) {this.error = error;}
        public T getData() {return data;}
        public void setData(T data) {this.data = data;}
    
        public static class Error {
            private String detail;
            private List<String> device_token;
            private List<String> content;
            private List<String> followed_id;
    
            public String getDetail() {return detail;}
            public void setDetail(String detail) {this.detail = detail;}
            public List<String> getDeviceToken() {return device_token;}
            public void setDeviceToken(List<String> device_token) {this.device_token = device_token;}
            public List<String> getContent() {return content;}
            public void setContent(List<String> content) {this.content = content;}
            public List<String> getFollowedId() {return followed_id;}
            public void setFollowedId(List<String> followed_id) {this.followed_id = followed_id;}
        }
    
    }
    
  3. http 的数据来源,现阶段只有 apidocjs 这一个
    • 若有其他数据来源,可以配置 api_data.source 属性,然后添加对应的解析器,解析为 xhw 的 model 。

工具环境与依赖

快速使用入门

  1. 下载项目的 Zip 包,解压缩,从 xhwt 文件夹下,选取其中的一个包装器模板文件夹,作为目标包装器的配置,该文件夹在下面都叫做target dir
    • 例如: xhwt/asynchttp/non_version(这是 android-async-http 库的一个模板与配置);
  2. 获取接口数据文件(api_data.json:存储 apidocjs 生成的 API 文档的数据)的路径;
    • 例如: guide 文件夹中的 api_data.json 的绝对路径
  3. 修改 target dir 下配置文件(x-http-wrapper.json)中 api_data.file_path_infos 的配置信息,将 api_data.json 的绝对路径添加上去;
      "api_data": {
        "source": "apidocjs",
        "file_path_type": "file",
        "file_path_infos": [
          {
            "os_name": "Mac OS X",
            "path": "api_data.json 的绝对路径"
          },
          {
            "os_name": "Windows",
            "path": "api_data.json 的绝对路径"
          }
        ],
        "file_charset": "UTF-8"
      }
    
  4. 修改 target dir 中, API 的模板文件中<t:header></t:header>标签内,生成文件的目标路径;
    • API 的模板文件是以.xhwt 为后缀的文件,是生成各个 http 相关文件的模板;
    • <t:header></t:header>标签内,保存的是模板文件生成文件的文件名称与文件地址;
      • 例如:
      {
          "file_name":"HttpApi.swift",
          "file_dirs":[
              {
                  "os_name":"Windows",
                  "path":"生成文件的目标路径(绝对路径)"
              },
              {
                  "os_name":"Mac OS X",
                  "path":"生成文件的目标路径(绝对路径)"
              }
          ]
      }
      
  5. 修改 target dir 下配置文件(x-http-wrapper.json)中 template_file_infos 中的 need_generate 属性,用于开启、关闭生成文件的功能;
    • 例如:若你想生成 HttpApi 类型的文件,就需要将 template_file_infos.HttpApi.need_generate 设置为 true ,并要修改了 xxx-httpapi.xhwt 文件中 header 标签内的地址;
      "template_file_infos": {
        "HttpApi": {
          "need_generate": true,
          "path": "ncm_ios_n-httpapi.xhwt"
        },
        ...
      }
    
  6. 命令行生成相关 http 文件
    • 命令行运行: java -jar (jar 文件的路径) (配置文件的绝对路径)
    • jar 文件的路径:在 guide 文件夹下有最新的 jar(x-http-wrapper.jar)
    • 配置文件的绝对路径:配置文件(x-http-wrapper.json)的绝对路径
    java -jar x-http-wrapper.jar xxxx/x-http-wrapper.json
    

api 的数据源:apidocjs

工作流程

  1. 解析 x-http-wrapper.json 这个配置文件;
  2. 在配置文件中,有 API 数据文件(在 api_data 中),再根据配置数据,将 API 数据解析为 x-http-wrapper 中的 model 数据;
  3. 在配置文件中,有所有的 x-http-wrapper 的 template 文件(在 template_file_infos 中),根据 template 文件中的内容与 model datas 和配置一起,生成目标文件;

最新的 jar

  1. 使用方式:
java -jar x-http-wrapper.jar xxx/x-http-wrapper.json
  1. x-http-wrapper.json 文件,必须是绝对路径,该文件是整个 wrapper 的配置文件;
  2. 若有多个 json 文件,也可以(如:有多个程序(ios,android)需要生成代码);

wrapper 的配置文件:

wrapper 内部 api 数据模型

  1. BaseModel:
    • 所有的 model 都需要继承 BaseModel
    • BaseModel 中有一个泛型用于存储更高一级的 BaseModel
    • 在 template engine 中,反射只认 BaseModel ,不是 BaseModel 的 model 不能反射
    • template engine 在反射调用时,若没有在反射的对象中找到方法,会从 higherLevel 中去找,直到没有 higherLevel 为止;
  2. model 的结构:
    • VersionModel-->StatusCodeGroup, RequestGroup
    • StatusCodeGroup-->StatusCode
    • RequestGroup-->Request-->Url,Header,Input,Response
    • Response-->Response File,Response Message

wrapper 模板文件的类型

  1. 所有的类别都在 XHWTFileType 枚举中,现阶段共有 6 个类别;
    • HttpApi, Request, RequestParam, Response, StatusCode, BaseResponse
  2. 且在该枚举中也有该模板类别所需数据的获取过滤功能(getReflectiveDatas 方法);

wrapper 模板标签

  1. 生成的文件内容由该文件类型获取到的 API 数据与标签两者来驱动
  2. 头部标签<t:header></t:header>: 用于标示该模板文件,生成的目标文件路径和名称;
    • file_dirs:目标文件路径
    • file_name:目标文件名称
  3. 现阶段只有 7 个标签类型:使用反射来进行数据的加工
    • text, foreach, retain, list_single_line, if_else, list_replace, list_attach
    • 标签内部的匹配都为反射的方法名称;
      • 例如:在 foreach 标签中
      <t:foreach each="request_groups">
      </t:foreach>
      
      匹配的 request_groups 即为反射后去 request_groups 方法的数据,然后利用该数据去遍历;
2085 次点击
所在节点    程序员
0 条回复

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/335002

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX