统一前后端接口调用规范

在平时的前后端接口对接的过程中,前后端总是因为接口的实现方式不同需要花很长时间去对接,针对这种情况,我花了点时间把常用的几种调用方式总结成具体的代码,供大家参考,提高大家的工作效率。

后台代码采用java编写,使用了spring boot快速构建项目,前端网络层就用了最近比较流行的axios。也特别欢迎其他小伙伴针对这套接口编写其他语言的具体实现。这套代码我会上传到github:launcher

后台依赖:

//Guava,因为没有使用数据库存储数据,所以用了Guava缓存
<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>27.1-jre</version>
</dependency>

//lombok
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.6</version>
    <scope>provided</scope>
</dependency>

java后台具体实现

import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.util.Collection;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

@Slf4j
@RequestMapping("/user")
@RestController
public class UserController {

    /**
     * 创建一个空对象,避免找不到缓存抛异常
     */
    private static final User EMPTY = new User();

    /**
     * 构建一个本地缓存
     */
    private static final LoadingCache<Integer, User> CACHE = CacheBuilder.newBuilder()
            //初始化100个
            .initialCapacity(100)
            //最大10000
            .maximumSize(10000)
            //30分钟没有读写操作数据就过期
            .expireAfterAccess(30, TimeUnit.MINUTES)
            .build(new CacheLoader<Integer, User>() {
                //如果get()没有拿到缓存,直接点用load()加载缓存
                @Override
                public User load(Integer key) {
                    log.info("key:" + key);
                    return EMPTY;
                }

                /**
                 * 在调用getAll()的时候,如果没有找到缓存,就会调用loadAll()加载缓存
                 * @param keys
                 * @return
                 * @throws Exception
                 */
                @Override
                public Map<Integer, User> loadAll(Iterable<? extends Integer> keys) throws Exception {
                    log.info(String.valueOf(keys));
                    return super.loadAll(keys);
                }
            });


    @Data
    static class User {
        /**
         * id
         */
        private Integer id;
        /**
         * 名字
         */
        private String name;
        /**
         * 性别
         */
        private Boolean isMan;
        /**
         * 头像名称
         */
        private String imageName;
    }

    /**
     * 获取所有的users
     *
     * @return
     */
    @GetMapping("/list")
    @ResponseBody
    public Collection<User> users() {
        Map<Integer, User> map = CACHE.asMap();
        return map.values();
    }

    /**
     * 添加user(设置头像)
     *
     * @param user
     * @param image
     * @return
     */
    @PostMapping("/addWithImage")
    @ResponseBody
    public User add(@RequestPart("user") User user, @RequestPart("image") MultipartFile image) {
        //拿到头像进行处理
        user.setImageName(image.getName());
        //缓存
        CACHE.put(user.getId(), user);
        return user;
    }

    /**
     * 第一种方式(推荐)
     * 添加user(不设置头像)
     *
     * @param user
     * @return
     */
    @PostMapping("/add")
    @ResponseBody
    public User add(@RequestPart("user") User user) {
        //缓存
        CACHE.put(user.getId(), user);
        return user;
    }

    /**
     * 第二种方式
     * 添加user(不设置头像)
     *
     * @param user
     * @return
     */
    @PostMapping("/v2/add")
    @ResponseBody
    public User add2(@RequestBody User user) {
        //缓存
        CACHE.put(user.getId(), user);
        return user;
    }

    /**
     * 修改user
     *
     * @param user
     * @return
     * @throws ExecutionException
     */
    @PutMapping("/update")
    @ResponseBody
    public User update(@RequestBody User user) throws ExecutionException {
        User u = CACHE.get(user.getId());
        u.setImageName(user.getImageName());
        u.setName(user.getName());
        u.setIsMan(user.getIsMan());
        //缓存
        CACHE.put(u.getId(), u);
        return user;
    }

    /**
     *
     * @param id
     * @return
     * @throws ExecutionException
     */
    @DeleteMapping("/{id}")
    @ResponseBody
    public User deleteUser(@PathVariable("id") Integer id) throws ExecutionException {
        User user = CACHE.get(id);
        CACHE.invalidate(id);
        return user;
    }
}

js前端依赖

//axios
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>

针对axios做了一套个性化封装,最终得到产物:launcher.js

var LAUNCHER = function(){
    //请求方式
    var METHOD = {
        GET: 'GET',
        POST: 'POST',
        PUT: 'PUT',
        DELETE: 'DELETE',
        HEAD: 'HEAD',
        PATCH: 'PATCH',
        OPTIONS: 'OPTIONS'
    }

    //content-type
    var CONTENT_TYPE = {
        NAME: 'Content-Type',
        JSON: 'application/json;charset=UTF-8',
        FORM_DATA: 'multipart/form-data'
    }

    var BASE = {
        send: function(obj){
            //判断响应码是否正常,默认的
            function isOkResponse(status){
                return status >= 200 && status < 300;
            }

            axios.request({
                url: obj.url,
                //默认是get
                method: obj.method||METHOD.GET,
                baseURL: obj.baseURL,
                headers: obj.headers,
                params: obj.params,
                data: obj.data,
                //默认1000
                timeout: obj.timeout||1000,
                //默认json
                responseType: obj.responseType||'json',
                //上传事件
                onUploadProgress: obj.onUploadProgress||function(progressEvent){
                    //默认上传进度事件,可定义全局进度条
                    console.log('上传进度事件');
                },
                //定义可获得的http响应状态码
                validateStatus: obj.validateStatus||function(status){
                    return isOkResponse(status);
                }
            }).then(function(response){
                //处理响应头和响应数据
                function handleResponse(res,obj){
                    if(obj.handleResponse){
                        obj.handleResponse(res);
                    }
                    if(obj.handleResponseHeaders){
                        obj.handleResponseHeaders(res.headers);
                    }
                    if(obj.handleResponseData){
                        obj.handleResponseData(res.data);
                    }
                }
                //验证响应数据是否有效,全局验证
                function validateResponse(data){
                    return true;
                }

                //处理非正常业务数据
                function handleErrorResponse(res,obj){
                    if(obj.handleErrorResponse){
                        obj.handleErrorResponse(res);
                    }
                    if(obj.handleResponseHeaders){
                        obj.handleResponseHeaders(res.headers);
                    }
                    if(obj.handleErrorResponseData){
                        obj.handleErrorResponseData(res);
                    }
                }

                //处理非正常业务数据
                function handleErrorData(data){
                    //可以自行定义弹框或提醒
                    console.log(data);
                }
                //开始处理响应数据
                if(validateResponse(response)){
                    //自定义响应数据验证器
                    if(obj.validateResponse){
                        if(obj.validateResponse(response)){
                            //能调用这个方法,说明业务数据一定正确
                            handleResponse(response,obj);
                        }else{
                            //说明是非正常业务数据,比如响应code返回非正常状态
                            //如果有自定义处理非正常业务数据,就调用自定义处理器,否则调用全局处理器
                            if(obj.handleErrorResponse || obj.handleErrorResponseData){
                                handleErrorResponse(response,obj);
                            }else{
                                //调用全局处理非正常业务数据
                                handleErrorData(response.data);
                            }
                        }
                    }else{
                        //能调用这个方法,说明业务数据一定正确
                        handleResponse(response,obj);
                    }
                }
            }).catch(function(error){
                //默认全局处理error的方式
                function handleError(error){
                    //可以自行定义弹框或提醒
                    console.log(error);
                }
                //如果有自定义异常处理器,就调用自定义的处理器,否则调用全局处理器
                if (obj.handleError) {
                    obj.handleError(error);
                }else{
                    handleError(error);
                }
            });
        }
    }

    //抽离公共的参数
    function defaultHandleConfig(obj){
        return {
            url:obj.url,
            baseURL:obj.baseURL,
            headers:obj.headers,
            responseType:obj.responseType,
            //响应状态验证器
            validateStatus:obj.validateStatus,
            //响应数据验证器
            validateResponse:obj.validateResponse,
            handleResponse:obj.handleResponse,
            //处理响应头(正常业务数据)
            handleResponseHeaders:obj.handleResponseHeaders,
            //处理响应数据(正常业务数据)
            handleResponseData:obj.handleResponseData,
            //处理非正常响应数据(不符合validateResponseData的数据)
            handleErrorResponse:obj.handleErrorResponse,
            //处理非正常响应数据(不符合validateResponseData的数据)
            handleErrorResponseData:obj.handleErrorResponseData,
            //处理异常情况,不定义会调用全局处理器
            handleError:obj.handleError
        }
    }

    //设置headers
    function addHeader(headers,key,value){
        if(!headers){
            headers = {};
        }
        headers[key] = value;
        return headers;
    }

    //将data转换为json包装
    function handleData2Json(data){
        if(!!data){
            return JSON.stringify(data);
        }
        return data;
    }

    //将data转换为FormData包装
    function handleData2FormData(data){
        //判断是不是文件
        function isFile(file){
            return file instanceof File;
        }
        //创建一个FormData,用来包装数据
        var formData = new FormData();
        if(!!data){
            //抽出data的所有属性和值,放进FormData中
            for(var attr in data){
                var value = data[attr];
                if(isFile(value)){
                    formData.append(attr,value);
                }else if(typeof value === 'object'){
                    //针对{},[]类型,要使用Blob包装
                    formData.append(attr, new Blob([JSON.stringify(value)],{type:CONTENT_TYPE.JSON}));
                }else{
                    formData.append(attr,value);
                }
            }
        }
        return formData;
    }

    return {
        //对应所有get请求
        get: function(obj) {
            var config = defaultHandleConfig(obj);
            config.params = obj.params;
            BASE.send(config);
        },

        //对应spring mvc中的@RequestPart,支持文件上传
        postFormData: function(obj){
            var config = defaultHandleConfig(obj);
            config.method = METHOD.POST;
            config.headers = addHeader(obj.headers,CONTENT_TYPE.NAME,CONTENT_TYPE.FORM_DATA);
            config.data = handleData2FormData(obj.data);
            config.onUploadProgress = obj.onUploadProgress;
            BASE.send(config);
        },

        //对应spring mvc中的@RequestBody
        post: function(obj){
            var config = defaultHandleConfig(obj);
            config.method = METHOD.POST;
            config.headers = addHeader(obj.headers,CONTENT_TYPE.NAME,CONTENT_TYPE.JSON);
            config.data = handleData2Json(obj.data);
            BASE.send(config);

        },

        //和post一样,不过要保持幂等
        put: function(obj){
            var config = defaultHandleConfig(obj);
            config.method = METHOD.PUT
            config.headers = addHeader(obj.headers,CONTENT_TYPE.NAME,CONTENT_TYPE.JSON);
            config.data = handleData2Json(obj.data);
            BASE.send(config);
        },

        //删除
        delete: function(obj){
            var config = defaultHandleConfig(obj);
            config.method = METHOD.DELETE;
            config.params = obj.params;
            BASE.send(config);
        }
    };
}();

针对后台接口的测试页面

<!DOCTYPE html>
<html>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script src="launcher.js"></script>
<head>
    <title>hah</title>
</head>
<body>
<h1>Hello World</h1>

<input type="file" name="file" id="fileId">
<input type="button" value="添加user(设置头像)" id="btn" onclick="submit()">

<br>
<input type="button" value="添加user(不设置头像)" id="btn" onclick="add()">

<br>
<input type="button" value="添加user v2(不设置头像)" id="btn" onclick="addv2()">

<br>
<input type="button" value="查询user" id="btn" onclick="users()">

<br>
<input type="button" value="修改user" id="btn" onclick="update()">

<br>
<input type="button" value="删除user" id="btn" onclick="deleteUser()">
</body>
<script type="text/javascript">

//小伙伴需要针对自己的环境修改URL_PREFIX
var URL_PREFIX = 'https://localhost';

//查询所有的user
function users(){
    LAUNCHER.get({
        url:'/user/list',
        baseURL:URL_PREFIX,
        handleResponseData:function(data){
            console.log(data);
        }
    });
}

//添加user(设置头像)
function submit(){
     var objFile = document.getElementById("fileId");
     LAUNCHER.postFormData({
        url:"/user/addWithImage",
        baseURL:URL_PREFIX,
        data:{
            "image":objFile.files[0],
            "user":{
                'id':1,
                "name":"Zz",
                "isMan": true
            }
        },
        onUploadProgress: function(progressEvent){
            console.log(progressEvent);
        },
        handleResponseData:function(data){
            console.log(data);
        }
    });
}

//推荐
//添加user(不设置头像)
function add(){
     LAUNCHER.postFormData({
        url:"/user/add",
        baseURL:URL_PREFIX,
        data:{
            "user":{
                'id':2,
                "name":"Zz_2",
                "isMan": true
            }
        },
        handleResponseData:function(data){
            console.log(data);
        }
    });
}

//添加user v2(不设置头像)
function addv2(){
     LAUNCHER.post({
        url:"/user/v2/add",
        baseURL:URL_PREFIX,
        data:{
            'id':3,
            "name":"Zz_3",
            "isMan": true
        },
        handleResponseData:function(data){
            console.log(data);
        }
    });
}

//更新user
function update(){
     LAUNCHER.put({
        url:"/user/update",
        baseURL:URL_PREFIX,
        data:{
            'id':3,
            "name":"hello",
            "isMan": true
        },
        handleResponseData:function(data){
            console.log(data);
        }
    });
}

//删除指定的user
function deleteUser(){
    LAUNCHER.delete({
        url:"user/1",
        baseURL:URL_PREFIX,
        handleResponseData:function(data){
            console.log(data);
        }
    });
}
</script>
</html>

调试过程中,为了避免跨域问题,利用了nginx的动静分离分离功能,小伙伴在代码下载后需要针对自己的环境修改

//小伙伴需要针对自己的环境修改URL_PREFIX
var URL_PREFIX = 'https://localhost';

以便能成功地测试。

以上代码是我对于前后端接口调试的一个简单理解,避免前后端开发在接口对接上反复地不必要的沟通。有不同理解的小伙伴欢迎留言,大家一起讨论。

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