×

教你更优雅地写 API 之「规范响应数据」

Falcon 2021-01-07 views:
自动摘要

正在生成中……

图片
 

 

 

前言

在推出 lumen-api-starter 以后,收到了不少的关注和反馈,先在此感谢各位朋友萌 ?

关于 lumen-api-starter 的介绍可以参考上一篇是时候使用 Lumen 8 + API Resource 开发项目了!。本篇是在前一篇的基础上,重新整理并独立出了 package,可以同时支持最新版本 Laravel 和 Lumen 项目。

Package 地址:laravel-response
Laravel 版本 Api 开发初始化项目:laravel-api-starter
Lumen 版本 Api 开发初始化项目:lumen-api-starter

回到正题, 在用 Laravel 或 Lumen 写 API 项目前,通常需要先定义一些项目规范,来让后续的开发体验更舒适,包含有:

  • 规范统一响应数据结构:成功操作、失败操作以及异常操作响应
  • 使用枚举来管理项目中的常量,减少 bug,提高扩展性
  • 更有效地记录日志,来提高线上排查问题效率
  • 其他。..(规划中)
 

更新记录

 

实现过程

RESTful 服务最佳实践 :如何去设计 Http 状态码以及数据返回格式。

 

思路

  • 尽可能地遵循 Laravel 思维进行扩展,符合一定规范
  • 尽量少的依赖安装,最好是 0 依赖,不额外增加负担
  • 尽量完善的单元测试,保证代码质量(关于使用示例可以跳过下面的介绍直接查看 github.com/Jiannei/laravel-respons... 测试用例)
  • 实现需要简洁,使用需要优雅
 

功能

  • 统一的数据响应格式,固定包含:codestatusdatamessageerror
  • 内置 Http 标准状态码支持,同时支持扩展 ResponseCodeEnum 来根据不同业务模块定义响应码
  • 响应码 code 对应描述信息 message 支持本地化,支持配置多语言
  • 合理地返回 Http 状态码
  • 根据 debug 开关,合理返回异常信息、验证异常信息等
  • 支持格式化 Laravel 的 Api ResourceApi Resource CollectionPaginator(简单分页)、LengthAwarePaginator(普通分页)、Eloquent\ModelEloquent\Collection,以及简单的 array 和 string 等格式数据返回
  • 分页数据格式化后的结果与使用 league/fractal (DingoApi 使用该扩展进行数据转换)的 transformer 转换后的格式保持一致,也就是说,可以顺滑地从 Laravel Api Resource 切换到 league/fractal
 

规范

  • 合适的 Http 状态码,可以让客户端 / 浏览器更好地理解 Http 响应

 

教你更优雅地写 API 之规范响应数据
 

 

  • 格式固定
{
    "status": "success",// 描述 HTTP 响应结果:HTTP 状态响应码在 500-599 之间为”fail”,在 400-499 之间为”error”,其它均为”success”
    "code": 200,// 包含一个整数类型的 HTTP 响应状态码,也可以是业务描述操作码,比如 200001 表示注册成功
    "message": "操作成功",// 多语言的响应描述
    "data": {// 实际的响应数据
        "nickname": "Joaquin Ondricka",
        "email": "lowe.chaim@example.org"
    },
    "error": {}// 异常时的调试信息
}
 

需求

在不使用任何 package 的情况下,在 Laravel 中响应 API Json 格式数据,通常是下面这个样子:

return response()->json($data, $status, $headers, $options);

但实际开发场景,会有很多种数据返回需求:

  • 更多时候只是简单的成功和失败响应,所以需要有快捷的 success 和 fail 格式化方法
  • 成功响应可能包含有:User::all()User::first()UserResourceUserCollectionUser::paginate()User::simplePaginate() 、 Collection 和普通的 Array 等,希望这些不同类型的数据都能格式化成统一的结构
  • 失败的响应,通常就是根据不同的业务场景,返回不同的错误码和错误描述
  • 异常响应,对于表单验证、Http 等异常情况,能够针对是否开启 debug 有不同响应,并且格式与前面统一
 

定义业务操作码

<?php
namespace App\Repositories\Enums;

use Jiannei\Enum\Laravel\Repositories\Enums\ResponseCodeEnum as BaseResponseCodeEnum;

class ResponseCodeEnum extends BaseResponseCodeEnum
{
    // 业务操作正确码:1xx、2xx、3xx 开头,后拼接 3 位
    // 200 + 001 => 200001,也就是有 001 ~ 999 个编号可以用来表示业务成功的情况,当然你可以根据实际需求继续增加位数,但必须要求是 200 开头
    // 举个栗子:你可以定义 001 ~ 099 表示系统状态;100 ~ 199 表示授权业务;200 ~ 299 表示用户业务。..
    const SERVICE_REGISTER_SUCCESS = 200101;
    const SERVICE_LOGIN_SUCCESS = 200102;

    // 客户端错误码:400 ~ 499 开头,后拼接 3 位
    const CLIENT_PARAMETER_ERROR = 400001;
    const CLIENT_CREATED_ERROR = 400002;
    const CLIENT_DELETED_ERROR = 400003;

    const CLIENT_VALIDATION_ERROR = 422001; // 表单验证错误

    // 服务端操作错误码:500 ~ 599 开头,后拼接 3 位
    const SYSTEM_ERROR = 500001;
    const SYSTEM_UNAVAILABLE = 500002;
    const SYSTEM_CACHE_CONFIG_ERROR = 500003;
    const SYSTEM_CACHE_MISSED_ERROR = 500004;
    const SYSTEM_CONFIG_ERROR = 500005;

    // 业务操作错误码(外部服务或内部服务调用。..)
    const SERVICE_REGISTER_ERROR = 500101;
    const SERVICE_LOGIN_ERROR = 500102;
}
 

本地化操作码描述

<?php
// resources/lang/zh-CN/enums.php
use App\Repositories\Enums\ResponseCodeEnum;

return [
    // 响应状态码
    ResponseCodeEnum::class => [
        // 成功
        ResponseCodeEnum::HTTP_OK => '操作成功', // 自定义 HTTP 状态码返回消息
        ResponseCodeEnum::HTTP_INTERNAL_SERVER_ERROR => '操作失败', // 自定义 HTTP 状态码返回消息
        ResponseCodeEnum::HTTP_UNAUTHORIZED => '授权失败',

        // 业务操作成功
        ResponseCodeEnum::SERVICE_REGISTER_SUCCESS => '注册成功',
        ResponseCodeEnum::SERVICE_LOGIN_SUCCESS => '登录成功',

        // 客户端错误
        ResponseCodeEnum::CLIENT_PARAMETER_ERROR => '参数错误',
        ResponseCodeEnum::CLIENT_CREATED_ERROR => '数据已存在',
        ResponseCodeEnum::CLIENT_DELETED_ERROR => '数据不存在',
        ResponseCodeEnum::CLIENT_VALIDATION_ERROR => '表单验证错误',

        // 服务端错误
        ResponseCodeEnum::SYSTEM_ERROR => '服务器错误',
        ResponseCodeEnum::SYSTEM_UNAVAILABLE => '服务器正在维护,暂不可用',
        ResponseCodeEnum::SYSTEM_CACHE_CONFIG_ERROR => '缓存配置错误',
        ResponseCodeEnum::SYSTEM_CACHE_MISSED_ERROR => '缓存未命中',
        ResponseCodeEnum::SYSTEM_CONFIG_ERROR => '系统配置错误',

        // 业务操作失败:授权业务
        ResponseCodeEnum::SERVICE_REGISTER_ERROR => '注册失败',
        ResponseCodeEnum::SERVICE_LOGIN_ERROR => '登录失败',
    ],
];
 

使用示例

 

成功响应

  • 示例代码
<?php
public function index()
{
    $users = User::all();

    return Response::success(new UserCollection($users));
}

public function paginate()
{
    $users = User::paginate(5);

    return Response::success(new UserCollection($users));
}

public function simplePaginate()
{
    $users = User::simplePaginate(5);

    return Response::success(new UserCollection($users));
}

public function item()
{
    $user = User::first();

    return Response::success(new UserResource($user));
}

public function array()
{
    return Response::success([
        'name' => 'Jiannel',
        'email' => 'longjian.huang@foxmail.com'
    ],'', ResponseCodeEnum::SERVICE_REGISTER_SUCCESS);
}
  • 返回全部数据
{
    "status": "success",
    "code": 200,
    "message": "操作成功",
    "data": [
        {
            "nickname": "Joaquin Ondricka",
            "email": "lowe.chaim@example.org"
        },
        {
            "nickname": "Jermain D'Amore",
            "email": "reanna.marks@example.com"
        },
        {
            "nickname": "Erich Moore",
            "email": "ernestine.koch@example.org"
        }
    ],
    "error": {}
}
  • 分页数据
{
    "status": "success",
    "code": 200,
    "message": "操作成功",
    "data": {
        "data": [
            {
                "nickname": "Joaquin Ondricka",
                "email": "lowe.chaim@example.org"
            },
            {
                "nickname": "Jermain D'Amore",
                "email": "reanna.marks@example.com"
            },
            {
                "nickname": "Erich Moore",
                "email": "ernestine.koch@example.org"
            },
            {
                "nickname": "Eva Quitzon",
                "email": "rgottlieb@example.net"
            },
            {
                "nickname": "Miss Gail Mitchell",
                "email": "kassandra.lueilwitz@example.net"
            }
        ],
        "meta": {
            "pagination": {
                "count": 5,
                "per_page": 5,
                "current_page": 1,
                "total": 12,
                "total_pages": 3,
                "links": {
                    "previous": null,
                    "next": "http://laravel-api.test/api/users/paginate?page=2"
                }
            }
        }
    },
    "error": {}
}
  • 返回简单分页数据
{
    "status": "success",
    "code": 200,
    "message": "操作成功",
    "data": {
        "data": [
            {
                "nickname": "Joaquin Ondricka",
                "email": "lowe.chaim@example.org"
            },
            {
                "nickname": "Jermain D'Amore",
                "email": "reanna.marks@example.com"
            },
            {
                "nickname": "Erich Moore",
                "email": "ernestine.koch@example.org"
            },
            {
                "nickname": "Eva Quitzon",
                "email": "rgottlieb@example.net"
            },
            {
                "nickname": "Miss Gail Mitchell",
                "email": "kassandra.lueilwitz@example.net"
            }
        ],
        "meta": {
            "pagination": {
                "count": 5,
                "per_page": 5,
                "current_page": 1,
                "links": {
                    "previous": null,
                    "next": "http://laravel-api.test/api/users/simple-paginate?page=2"
                }
            }
        }
    },
    "error": {}
}
  • 返回单条数据
{
    "status": "success",
    "code": 200,
    "message": "操作成功",
    "data": {
        "nickname": "Joaquin Ondricka",
        "email": "lowe.chaim@example.org"
    },
    "error": {}
}

其他快捷方法

Response::accepted();
Response::created();
Response::noContent();
 

失败响应

不指定 meesage

public function fail()
{
    Response::fail();// 不需要加 return
}
  • 未配置多语言响应描述,返回数据
{
    "status": "fail",
    "code": 500,
    "message": "Http internal server error",
    "data": {},
    "error": {}
}
  • 配置多语言描述后,返回数据
{
    "status": "fail",
    "code": 500,
    "message": "操作失败",
    "data": {},
    "error": {}
}

指定 message

public function fail()
{
    Response::fail('error');// 不需要加 return
}

返回数据

{
    "status": "fail",
    "code": 500,
    "message": "error",
    "data": {},
    "error": {}
}

指定 code

public function fail()
{
    Response::fail('',ResponseCodeEnum::SERVICE_LOGIN_ERROR);
}

返回数据

{
    "status": "fail",
    "code": 500102,
    "message": "登录失败",
    "data": {},
    "error": {}
}

其他快捷方法

Response::errorBadRequest();
Response::errorUnauthorized();
Response::errorForbidden();
Response::errorNotFound();
Response::errorMethodNotAllowed();
Response::errorInternal();
 

异常响应

对于异常的数据格式化,需额外在 app/Exceptions/Handler.php 中 引入 use Jiannei\Response\Laravel\Support\Traits\ExceptionTrait; 引入以后,对于 ajax 请求产生的异常都会进行格式化数据返回。

(Lumen 中为达到同样效果,还需在 app/Http/Controllers/Controller.php 中引入 ExceptionTrait

  • 表单验证异常
{
    "status": "error",
    "code": 422,
    "message": "验证失败",
    "data": {},
    "error": {
        "email": [
            "The email field is required."
        ]
    }
}
  • Controller 以外抛出异常返回

可以直接使用 abort 辅助函数直接抛出 HttpException 异常

abort(ResponseCodeEnum::SERVICE_LOGIN_ERROR);

// 返回数据

{
    "status": "fail",
    "code": 500102,
    "message": "登录失败",
    "data": {},
    "error": {}
}
  • 其他异常

开启 debug

{
    "status": "error",
    "code": 404,
    "message": "Http not found",
    "data": {},
    "error": {
        "message": "",
        "exception": "Symfony\\Component\\HttpKernel\\Exception\\NotFoundHttpException",
        "file": "/home/vagrant/code/laravel-api-starter/vendor/laravel/framework/src/Illuminate/Routing/AbstractRouteCollection.php",
        "line": 43,
        "trace": [
            {
                "file": "/home/vagrant/code/laravel-api-starter/vendor/laravel/framework/src/Illuminate/Routing/RouteCollection.php",
                "line": 162,
                "function": "handleMatchedRoute",
                "class": "Illuminate\\Routing\\AbstractRouteCollection",
                "type": "->"
            },
            {
                "file": "/home/vagrant/code/laravel-api-starter/vendor/laravel/framework/src/Illuminate/Routing/Router.php",
                "line": 646,
                "function": "match",
                "class": "Illuminate\\Routing\\RouteCollection",
                "type": "->"
            },
            ...
        ]
    }
}

关闭 debug

{
    "status": "error",
    "code": 404,
    "message": "Http not found",
    "data": {},
    "error": {}
}
 

One more thing?

回顾一下,这些封装全部都是基于 response()->json() ,即返回的是 JsonResponse 对象,所以我们依旧可以继续链式调用该对象上的方法。

// 设置 HTTP 响应码
return Response::success(new UserResource($user))->setStatusCode(ResponseCodeEnum::HTTP_CREATED);
 

其他

依照惯例,如果对您的日常工作有所帮助或启发,欢迎三连 star + fork + follow

如果有任何批评建议,通过邮箱(longjian.huang@foxmail.com)的方式可以联系到我。

总之,欢迎各路英雄好汉。

QQ 群:1105120693