在社区常常看到前端的兄弟萌吐槽后端的年轻人不讲码德,来!骗!来!糊弄!乱改接口,动不动格式就变了,我大意了,字符串没有判空,控制台一片红。要么就是返回的数据嵌套太深,一层包一层,你搁这俄罗斯套娃呢?而若是按照 RESTful 架构来设计接口,就不会存在这种相似的问题。php
众所周知,RESTful API 是一套成熟的 API 设计理论,它不只有结构清晰、易于理解、方便扩展等诸多优势,并且它的做者 Roy Thomas Fielding 是位巨佬,他是 HTTP 规范的主要做者、Apache 服务器的共同创始人并在 Adobe 担任首席科学家,跟随巨佬的脚步,能够少走不少弯路。css
本文我将记录在视频网站项目中实践 RESTful 架构的经验与心得。例如,设计 Laravel 的接口、在 Vue 中作相应的对接工做等,这样妈妈就不再用担忧个人接口问题了,针不戳!html
服务端使用 HTTPS 做为通讯协议,不只比 HTTP 更加安全,并且现代浏览器对 HTTP 2 的支持已经逐渐成熟,性能方面也有很大提升。因此即使用户以 HTTP 协议访问接口,咱们也直接将访问重定向至 HTTPS 协议,非常省心!前端
在 nginx.conf
中添加以下配置完成重定向的配置:vue
server {
listen 80;
server_name www.lcgod.com lcgod.com;
access_log off;
rewrite ^/(.*)$ https://www.lcgod.com/$1 permanent;
}
复制代码
以上是我博客的配置,用户不论访问 http://www.lcgod.com/*
仍是 http://lcgod.com/*
,都将被 Nginx 重定向至 https://www.lcgod.com/*
,兄弟萌能够随意访问进行测试。ios
接着添加以下代码便可配置 HTTPS 并开启 HTTP 2:nginx
server {
listen 443 ssl http2;
server_name www.lcgod.com lcgod.com;
# 301 重定向
if ($host = lcgod.com) {
rewrite ^/(.*)$ https://www.lcgod.com/$1 permanent;
}
ssl_certificate /etc/nginx/ssl/www.pem;
ssl_certificate_key /etc/nginx/ssl/www.key;
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/nginx/ssl/www.pem;
resolver 8.8.8.8 114.114.114.114 valid=300s;
resolver_timeout 5s;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers 'EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+AES128'
':RSA+AES128:EECDH+AES256:RSA+AES256'
':EECDH+3DES:RSA+3DES:!aNULL:!MD5:!RC4:!DHE:!kEDH';
add_header Strict-Transport-Security "max-age=15768001; preload";
add_header X-Content-Type-Options nosniff;
# 设置前端项目根目录
root /home/nginx/spa/web;
index index.html;
# 省略了一些网站配置……
}
复制代码
以上代码中 ssl_certificate /etc/nginx/ssl/www.pem
及 ssl_certificate_key /etc/nginx/ssl/www.key
是配置 HTTPS 所须要的 SSL 证书,直接使用 阿里云免费证书 就好,话提及来,我已经白嫖好几年了,嘤嘤嘤~web
大型项目通常都会将接口部署在专用域名之下。例如,掘金的接口项目部署在 api.juejin.cn
下,前端 Vue 项目部署在 juejin.cn
下。这样作的优势是方便扩展,缺点是存在跨域问题,浏览器每次发送复杂请求时(例如掘金的点赞接口),都会先发送一个 OPTIONS
预检请求,探测服务端的跨域规则,若服务端容许跨域才会继续发送真正的异步请求。以下图所示:正则表达式
能够从上图中发现掘金服务端设置的一些跨域规则,有一条 access-control-max-age: 86400
,意为浏览器对点赞接口发送了一次 OPTIONS
预检请求后,会缓存一天的时间,一天内对点赞接口的后续访问都不会再次发送预检请求。此规则很好地避免了浏览器发送过多的预检请求,浪费服务器资源。sql
其实跨域还会存在一些例如 Cookie 设置之类的坑,跨域相关的坑是很是多的,只有亲自踩坑才会明白其中的痛苦,并在痛苦中成长,因此我就再也不赘述。
对于像我独立开发的一个街舞视频网站 惟舞 这种小项目,业务逻辑简单,我将前端 Vue 与接口 Laravel 都部署在同一域名中,接口项目使用 api
前缀进行区分便可。我就比较喜欢使用这种简单的作法,毕竟我不跨域我就永远不会踩坑 (=・ω・=)
在 nginx.conf
的中添加以下规则,便可完成前缀设置:
server {
listen 443 ssl http2;
server_name www.vhiphop.com vhiphop.com;
# 省略了一些网站配置……
# 设置前端项目根目录
root /home/nginx/spa/web;
index index.html;
location /api {
try_files $uri $uri/ /index.php?$query_string;
}
location / {
try_files $uri /index.html;
}
location ~ \.php(.*)$ {
# 设置 PHP 项目根目录
root /home/nginx/api/web/public;
index index.php;
fastcgi_pass unix:/var/run/php-fpm/php-fpm.sock;
fastcgi_index index.php;
fastcgi_split_path_info ^((?U).+\.php)(/?.+)$;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
}
复制代码
其中的 location /api {}
配置项表明用户访问以 www.vhiphop.com/api
开头的 URL,优先交给 PHP 的接口项目处理。
其中的 location / {}
配置项则表明非 www.vhiphop.com/api
开头的 URL 都返回 Vue 的单页面项目。
适当地利用缓存策略能够在减缓服务器压力、优化用户体验的同时不影响项目的版本更新。
我在 nginx.conf
中进行了以下设置:
server {
listen 443 ssl http2;
server_name www.vhiphop.com vhiphop.com;
# 省略了一些网站配置……
#设置 css、js 和图片等静态资源的缓存时间
location ~ .*\.(css|js|ico|png|gif|jpg|json|mp3|mp4|flv|swf)(.*) {
expires 60d;
}
location /index.html {
add_header cache-control max-age=30;
}
}
复制代码
例如,用户第一次访问了咱们的 Vue 项目后,浏览器将项目的静态资源(html、css、js、图片等)下载至本地,并缓存。
假设用户在 30 秒内重新窗口中打开本网站,或点击收藏的网站书签刷新本网站,浏览器都不会从新请求服务器下载最新资源,而是直接对 index.html
返回 200 (form disk cache)
状态码,意为直接从硬盘中读取该文件;而其余资源例如图片,则会返回 200 (form memory cache)
状态码,意为直接从内存中读取该图片,以下图:
假设咱们开发人员在距离用户第一次访问 30 秒内在服务端对 Vue 项目的代码进行了更新,用户在 30 秒后使用以上方式再次刷新页面,浏览器则从新请求服务器,根据请求头中的 last-modified
、etag
或 expires
等规则,判断是否须要下载最新资源,若是资源发生改变,则下载最新资源、更新缓存。
若是用户点击刷新按钮或点击地址栏并按回车键,浏览器每次都会从新访问服务器,若服务器资源已发生改变,则从新下载资源,若未改变,则从缓存、硬盘中读取。兄弟萌能够用 Chrome 试试,若是返回 304 状态码,就表明从新访问了服务器,但资源未发生改变,再次从缓存、硬盘中读取资源,以下图:
RESTful 架构提倡每一个 URI 都表明一种资源,HTTP 的 URL 是对 URI 的一种实现,这种关系相似 JavaScript 是对 ECMAScript 的一种实现。
目前国内大厂的接口设计基本都是将版本号做为固定前缀放入 URL 中,例如掘金的沸点推荐接口 api.juejin.cn/recommend_api/v1
,这种作法的优势是清晰、直观,若是想让不一样的版本部署在不一样的服务器,Nginx 只须要设置简单的 location
规则便可完成转发。
将版本号放入 Header 中其实更符合 RESTful 架构的设计,毕竟资源自己是没有版本概念的,不一样版本的接口实际上返回的是同一种资源的不一样表现形式。
因此 URL 中应尽可能避免出现与资源无关的字符。而且这种作法也很适合中、小型系统,接口开发完成后,版本更新迭代不会很频繁,每次更新版本时只须要修改 Header 中的版本号便可。
我使用 Flutter 开发的 惟舞 APP 的接口就使用了以上作法,相关代码以下:
_dio = Dio()
..options.baseUrl = baseUrl
..options.headers.addAll({
HttpHeaders.acceptHeader: 'application/'
'vnd.vhiphop.v${Constants.apiVersion}+json',
})
复制代码
假设当前 APP 接口版本号为 1.0
,那么 Dio 每次发送请求时,都会设置 accept
的值为 vnd.vhiphop.v1.0+json
。
其实这个问题就像问世界上最好的语言是什么同样(别问,问就是 PHP)。一千个开发者,有一千个哈姆雷特,本质上对于咱们的区别也就是改一两行代码的事,更有甚者,淘宝、百度的不少接口都是用 JSONP 来发送异步请求,你能说他们架构设计的不够好吗?因此选择一个适合本身系统的就好,Any colour you like~
路径即接口 URL 的后缀部分,例如掘金的热门文章接口 api.juejin.cn/recommend_api/v1/article/recommend_all_feed
其中的 /recommend_api/v1/article/recommend_all_feed
即是路径,但掘金的接口确定不是按 RESTful 架构设计的,以下图所示:
仍是那句话, RESTful 架构提倡每一个 URI 都表明一种资源,由于资源是一种实体,因此应该使用名词,正常状况下资源都能与数据库中的表名对应,而且接口返回的数据都是集合的形式(例如数组、对象),因此 URL 应该使用名词的复数形式。
例如,数据库中有文章表 article
与用户表 user
,相关接口的 path 部分设计为以下:
# 获取文章列表
/articles
# 获取用户列表
/users
复制代码
一、直观
你有一个袋子,里面有好多个苹果,你会说这是个苹果袋。但不管里面有 0、1 仍是 1000 个苹果,它依然是个袋子。表也是如此,表名须要描述清楚它所包含的对象,而非有多少个数据。
二、便利
单数形式更简单。有一些单词,它的复数形式可能不是常规的,或者就没有复数形式,可是单数不同,单数形式则没那么多讲究。有些单词的复数,可能会让你想到头大,可能得好好谷歌才能找到。
三、优雅
特别是一些 master_detail
形式的资源名称,统一用单数,读起来更方便,对齐更整齐,从顺序上更有逻辑性。例如:
// 单数:
order
// 复数:
orders
// 单数:
order_detail
// 复数:
order_details
复制代码
四、简单朴素
设想下,不管是表名、主键、关系仍是实例,你均可以统一用单数,看上去很是统一,也不用费心地各类复数单数中转换你的思惟。例如:
# 表名
customer
# 主键
customer.customer_id
# 关联表
customer_address
# 方法名
public function getCustomer { }
# 查询语句
SELECT FROM customer WHERE customer_id = 100
复制代码
一旦你肯定将这个对象名称定为 customer
,那么全部和数据库相关的交互、编程就均可以使用这个单词。
五、全球化
假设你身处一个全球化的团队,成员中有些人的母语不是英文(说的就是我),对于他们来讲,辨认和书写一个单词的复数形式更加困难,会给他们带来麻烦,也给团队合做带来麻烦。
六、效率
能够节省你的拼写时间与硬盘空间,甚至让你的键盘更“长寿”。
综上所述,我推荐在数据库中使用单数表名,而在 URL 中使名词复数。
URL 的基本结构为 协议、域名 与 路径,因为协议 与 域名 都是不区分大小写的,因此为了保持统一,路径 也要采用小写形式,不要使用驼峰命名法,例如,获取用户隐私协议的接口:
// 错误作法
/userPrivacyPolicies
// 正确作法
/user_privacy_policies
// 更好的作法
/user-privacy-policies
复制代码
为何不推荐使用下划线分隔单词?
了解正则表达式的兄弟萌都懂,在正则表达式中 /w
表示单词字符,其范围包括 a-z
、A-Z
、0-9
和下划线。例如,hello_world
将被视为一个单词字符,而 hello-world
将被视为两个单词。大部分状况下,前端的路由名称与接口的路径名称保持统一,不只规范而且利于搜索引擎的关键词收录。
使用分隔符 -
分隔单词,比下划线 _
看起来更加容易分辨,键盘上也能够少按一个 Shift
键。
综上所述,我推荐使用分隔符 -
对名词进行分隔。
查询字符串是 URL 的最后一部分,通常用于对结果返回结果的过滤。例如,获取文章列表第一页的 20 条记录:
/articles?page=1&size=20
复制代码
只获取 user_id
为 233
的用户的文章:
/articles?user_id=233
复制代码
还有一种更好的作法,就是对资源进行分层,下面这种写法更加清晰、直观:
/users/233/articles
复制代码
若是只获取发布状态为已发布
的文章,你可能会这么作:
/users/233/articles/published
复制代码
我是不推荐使用以上作法的,当层数过多时,URL 已经没有那么直观了,改成如下写法要更好:
/users/233/published-articles
// 更好的写法
/users/233/articles?publish_state=1
复制代码
实际上讲,使用 JSON 做为数据格式进行交互,早已成为主流,毕竟它轻量、易于阅读,最重要的是它是 ECMAScript 的子集,浏览器对它的支持有着自然的优点。
若是使用 axios
进行 HTTP 请求,默认的 Content-Type
就是 application/json
,无需进行任何设置。
若是使用 fetch
进行 HTTP 请求,则默认的 Content-Type
是 text/plain
,咱们须要进行以下修改:
const response = await fetch(
'https://www.lcgod.com/api',
{ headers: { 'Content-Type': 'application/json; charset=utf-8' }},
);
复制代码
Laravel 从 5.4
版本开始,再也不支持在配置文件中定制 PDO 的 fetch mode
,取而代之的 PDO::FETCH_OBJ
。也就是说,经过查询构造器或模型从数据库中取出的数据不是单纯的数组形式,而是数组与 stdClass Object
的结合体,直接返回给前端,根本没法解析为数组,那还用个 🔨
因此须要将 app/Providers/EventServiceProvier.php
文件中的 boot
方法替换为以下,便可将 fetchMode
改成正常:
public function boot() {
parent::boot();
Event::listen(\Illuminate\Database\Events\StatementPrepared::class, function ($event) {
$event->statement->setFetchMode(\PDO::FETCH_ASSOC);
});
}
复制代码
从数据库取出传统的数组后,在控制器中直接返回 response
全局函数便可输出 JSON 数据,有如下两种用法:
# 手动设置 Content-Type
return response([], 200)->header('Content-Type', 'application/json');
# 框架自动设置 Content-Type
return response()->json([], 200);
复制代码
客户端使用不一样的 HTTP 动词请求服务端,服务端根据动词对资源作出不一样类型的操做:
名称 | 动做 | 数据库操做 |
---|---|---|
GET | 获取资源 | SELECT |
POST | 新增资源 | INSERT |
PUT | 更新总体资源 | UPDATE |
PATCH | 更新部分资源 | UPDATE |
DELETE | 删除资源 | DELETE |
HEAD | 获取资源元数据 | - |
OPTIONS | 获取客户端能够改变的资源信息 | - |
服务端返回不一样的状态码表示资源的不一样状态:
状态码 | 状态信息 |
---|---|
200 | 成功返回数据(返回 JSON 数组或 JSON 对象) |
201 | 成功建立或更新数据(返回 JSON 对象) |
204 | 成功删除数据(无返回数据) |
401 | 用户登陆后才能访问(返回 JSON 对象) |
403 | 提交的参数不合法(返回 JSON 对象) |
404 | 未找到相关的服务(返回 JSON 对象) |
405 | 使用了不支持的 HTTP 动词(例如只支持 GET,而你发送 POST) |
500 | 服务器内部发生错误(返回 JSON 对象) |
客户端发送的请求只要失败了,服务端统一返回如下格式的 JSON 字符串,例如,某个请求地址不正确,服务端没有相关的接口,则返回 404
状态码:
{
"message": "未找到相关的服务",
"error_code": 1001
}
复制代码
手机号格式错误,返回 403
状态码:
{
"message": "请输入正确的手机号",
"error_code": 1001
}
复制代码
短信验证码错误,返回 403
状态码,并给出不一样的 error_code
:
{
"message": "请输入正确的验证码",
"error_code": 1002
}
复制代码
其中的 error_code
由后端决定相关的错误状态,客户端根据 error_code
作出不一样的动做。例如,惟舞网的注册组件就是这样作的:
下面列举我在项目中使用 HTTP 动词的一些例子。
获取用户列表:
/users
复制代码
服务端返回 200
状态码:
{
"count": 123456,
"users": [
{
"id": 233,
"token": "abc123",
"nickname": "聪聪",
"avatar": "avatar.jpg",
"phone": "181****9876"
},
{
"id": 234,
"token": "abc123",
"nickname": "聪聪2",
"avatar": "avatar.jpg",
"phone": "181****9876"
},
{
"id": 235,
"token": "abc123",
"nickname": "聪聪3",
"avatar": "avatar.jpg",
"phone": "181****9876"
}
]
}
复制代码
获取 user_id
为 233
的用户的我的资料:
/users/233
复制代码
服务端返回 200
状态码:
{
"id": 233,
"token": "abc123",
"nickname": "聪聪",
"avatar": "avatar.jpg",
"phone": "181****9876"
}
复制代码
注册一个新用户:
/users
复制代码
假设经过手机验证码注册,则提交的数据以下:
{
"sign_mode": 1,
"phone": 12345678910,
"code": 123456,
"nickname": "聪聪",
"psw": "abc123456"
}
复制代码
服务端返回 201
状态码:
{
"id": 233,
"token": "abc123",
"nickname": "聪聪",
"avatar": "avatar.jpg",
"phone": "181****9876"
}
复制代码
修改 user_id
为 233
的用户我的资料:
/users/233
复制代码
假设 user
表有如下 4 个字段储存用户我的资料,则将这 4 个字段所有提交:
{
"nickname": "聪聪",
"avatar": "avatar.jpg",
"phone": 12345678910,
"psw": "abc123456"
}
复制代码
服务端返回 201
状态码:
{
"message": "ok",
"error_code": 0
}
复制代码
修改 user_id
为 233
的用户手机号:
/users/233
复制代码
提交的数据中只须要包含手机号与验证码便可,后端将不会对其余信息进行更改:
{
"phone": 12345678910,
"code": "123456"
}
复制代码
服务端返回 201
状态码:
{
"message": "ok",
"error_code": 0
}
复制代码
注销 user_id
为 233
的用户:
/users/233
复制代码
服务端返回 204 No Content
状态码
其实所谓的删除,实际项目中都是软删除,例如将字段 is_del
的值从 0
更新为 1
,后端不可能使用 DELETE
操做真正对数据进行物理删除,以便用户误操做后找回数据。
视频播放页须要先获取视频的大小,作一些初始化操做。获取 video_id
为 233
的视频元数据:
/videos/233
复制代码
前面提过该动词,但我在实际项目中也不多主动使用,都是浏览器用于探测跨域规则自动发送的。
在生产环境中,服务端必定要关闭 debug 信息提示,避免暴露错误信息给客户端,保证接口的安全性。
Laravel 8 的错误由 app/Exceptions/Handler.php
处理,将该文件中的 register
方法替换为以下,便可拦截框架运行出错时的 debug 提示:
public function register() : void {
$this->renderable(function (\Throwable $e) {
$isDebug = (bool) env('APP_DEBUG', false);
$errorMessage = $isDebug ? $e->getTrace() : ['error_message' => '服务器繁忙', 'error_code' => 1001];
$statusCode = $isDebug ? $e->getStatusCode() : 500;
return response()->json($errorMessage, $statusCode);
});
}
复制代码
在生产环境中,修改 .env
文件的 debug 配置为 false
:
APP_DEBUG=false
复制代码
假设框架运行时发生错误,此时只会返回客户端简单的提示:
{
"message": "服务器繁忙",
"error_code": 1001
}
复制代码
新建一个 app/Helpers/ApiResponse.php
,用于处理接口主动返回数据:
<?php
namespace App\Helpers;
use Illuminate\Http\JsonResponse;
trait ApiResponse {
protected static function ok(array $data = [], int $statusCode = 200) : JsonResponse {
!$data && $data = ['message' => 'ok', 'error_code' => 0];
return response()->json($data, $statusCode);
}
protected static function created(array $data = []) : JsonResponse {
return self::ok($data, 201);
}
protected static function noContent() : void {
abort(204);
}
protected static function error( $message = '身份已失效, 请尝试从新登陆', $errorCode = 1001, $statusCode = 403, ) : JsonResponse {
return self::ok(
[
'message' => $message,
'error_code' => $errorCode,
],
$statusCode
);
}
protected static function notFound($message = '未找到相关数据') : JsonResponse {
return self::error($message, 404);
}
}
复制代码
在 app/Http/Controller.php
中使用 ApiResponse
:
<?php
namespace App\Http\Controllers;
use App\Helpers\ApiResponse;
use Illuminate\Routing\Controller as BaseController;
class Controller extends BaseController {
use ApiResponse;
}
复制代码
在 app/Http/UserController.php
中调用 ApiResponse
的方法,直接返回数据给客户端:
<?php
namespace App\Http\Controllers;
use App\Services\UserService;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
class UserController extends Controller {
private UserService $service;
public function __construct() {
$this->service = new UserService();
}
// GET 获取用户列表
public function index() : JsonResponse {
$response = $this->service->index();
return self::ok($response);
}
// GET 获取某个用户的我的资料
public function show(int $id) : JsonResponse {
$response = $this->service->show($id);
return self::ok($response);
}
// POST 注册一个新用户
public function store(Request $request) : JsonResponse {
// 作一些验证参数之类的操做……
$response = $this->service->store($data);
return self::created($response);
}
// PUT 修改某个用户的我的资料
public function update(int $id) : JsonResponse {
// 作一些验证参数之类的操做……
$response = $this->service->update($id, $data);
return self::created($response);
}
// DELETE 注销某个用户
public function destroy(int $id) : JsonResponse {
$this->service->destroy($id);
return self::noContent();
}
}
复制代码
在 /src
目录下新建 utils
文件夹,存放项目中全部的工具文件,便于后期的扩展与维护。
在 utils
文件夹中新建 request.js
,用于封装 axios
,发送异步请求。
在 request.js
中初始化 axios
实例,设置接口地址,直接使用项目的 .env
文件里的配置:
import axios from 'axios';
import { Message } from 'element-ui';
import store from '@/store';
const service = axios.create({
baseURL: process.env.VUE_APP_BASE_API,
timeout: 5000,
});
复制代码
设置一些自定义的请求头,并对实际 URL 进行处理,若是项目须要访问第三方的接口,将 baseURL
设置为空便可:
service.interceptors.request.use(
(config) => {
if (config.url.includes('http')) {
config.baseURL = '';
return config;
}
const { getters } = store;
config.headers['x-user-id'] = getters.userId;
config.headers['x-user-token'] = getters.userToken;
return config;
},
(error) => Promise.reject(error),
);
复制代码
根据 HTTP 状态码进行相关的一些操做,例如 401
状态码须要清空用户信息,退出登陆:
service.interceptors.response.use(
(response) => response.data,
(error) => {
let { data } = error.response;
if (typeof data !== 'object') data = {};
if (!data.error_code) data.error_code = 1001;
switch (error.response.status) {
case 403:
if (!data.message) data.message = '参数错误';
break;
case 404:
if (!data.message) data.message = '未找到相关服务';
break;
case 401:
if (!data.message) data.message = '登陆已失效,请从新登陆!';
store.dispatch('user/logout').catch(() => {});
break;
default:
if (!data.message) data.message = '网络繁忙';
}
return Promise.reject(data);
},
);
复制代码
request
方法用于对异常的处理,根据参数判断是否自动提示错误信息:
async function request({ url, method, params, isAutoShowErrorTip, }) {
let isError = false;
const data = await service({ url, method, params })
.catch((error) => { isError = true; return error; });
if (isError && isAutoShowErrorTip) {
Message({
message: data.message,
type: 'error',
duration: 5000,
});
}
return { data, isError };
}
复制代码
将 HTTP 动词对应的请求方法分别导出,便于项目的 API 文件调用。
export function get({ url, params, isAutoShowErrorTip }) {
return request({
method: 'GET',
url,
params,
isAutoShowErrorTip,
});
}
export function post({ url, params, isAutoShowErrorTip }) {
return request({
method: 'POST',
url,
params,
isAutoShowErrorTip,
});
}
export function put({ url, params, isAutoShowErrorTip }) {
return request({
method: 'PUT',
url,
params,
isAutoShowErrorTip,
});
}
export function patch({ url, params, isAutoShowErrorTip }) {
return request({
method: 'PATCH',
url,
params,
isAutoShowErrorTip,
});
}
export function del({ url, params, isAutoShowErrorTip }) {
return request({
method: 'DELETE',
url,
params,
isAutoShowErrorTip,
});
}
export function head({ url, params, isAutoShowErrorTip }) {
return request({
method: 'HEAD',
url,
params,
isAutoShowErrorTip,
});
}
复制代码
在 /src
目录下新建 api
文件夹,存放项目中全部的接口文件,便于后期的扩展与维护。
对于用户相关的接口请求,所有存放于 /src/api/user.js
,如下是相关示例:
import { get, post, put, del } from '@/utils/request';
const url = 'users';
// 获取用户列表
export function index(params, isAutoShowErrorTip = true) {
return get({
url,
params,
isAutoShowErrorTip,
});
}
// 获取某个用户的我的资料
export function show(id, isAutoShowErrorTip = true) {
return get({
url: `${url}/${id}`,
isAutoShowErrorTip,
});
}
// 注册一个新用户
export function store(params, isAutoShowErrorTip = true) {
return post({
url,
params,
isAutoShowErrorTip
});
}
// 修改某个用户的我的资料
export function update(id, params, isAutoShowErrorTip = true) {
return put({
url: `${url}/${id}`,
params,
isAutoShowErrorTip
});
}
// 注销某个用户
export function destroy(id, isAutoShowErrorTip = true) {
return del({
url: `${url}/${id}`,
isAutoShowErrorTip
});
}
复制代码
最后在页面组件进行调用,例如 /src/views/user/index.vue
是用户列表页,其 script
内容为以下:
import { index, destroy } from '@/api/user';
export default {
data: () => ({
isLoading: false,
isDeleting: false,
count: 0,
users: [],
queryList: {
is_asc: 0,
page: 1,
size: 8,
},
}),
methods: {
async load(route, next) {
if (this.isLoading) return;
const { queryList } = this;
const { query } = route;
const is_asc = query.is_desc ?? 1;
const size = +(query.size ?? 0);
const page = +query.page;
queryList.is_desc = is_asc ? 1 : 0;
queryList.page = page > 0 ? page : 1;
queryList.size = (size < 8 || size > 16) ? 8 : size;
this.isLoading = true;
const { isError, data } = await index(this.queryList);
this.isLoading = false;
if (next) next();
if (isError) return;
this.count = data.count;
this.users = data.users;
},
async handleDelete(id) {
if (this.isDeleting) return;
this.isDeleting = true;
const { isError } = await destroy(id);
this.isDeleting = false;
if (isError) return;
this.load();
},
},
beforeRouteUpdate(to, from, next) {
this.load(to, next);
},
beforeMount() {
this.load(this.$route);
},
};
复制代码
若是是我的项目,例如个人博客,不注重兼容性,能够直接使用浏览器自带的 fetch
发送请求,对其简单封装便可使用,而没必要使用 axios
:
export default async function({ method, url, params }) {
const init = {
method,
mode: process.env.VUE_APP_CORS_MODE,
credentials: process.env.VUE_APP_CREDENTIALS,
headers: { 'Content-Type': 'application/json; charset=utf-8' },
};
if (params) {
if (method === 'GET' || method === 'DELETE') {
const data = [];
Object.keys(params).forEach((k) => {
data.push(`${encodeURIComponent(k)}=${encodeURIComponent(params[k])}`);
});
url += `?${data.join('&')}`;
} else {
init.body = JSON.stringify(params);
}
}
url = url.includes('http') ? url : `${process.env.VUE_APP_BASE_API}${url}`;
const response = await fetch(url, init);
const { status } = response;
let data;
try {
data = await response.json();
} catch (e) {
data = {};
}
if (status > 199 && status < 300) return Promise.resolve(data);
if (typeof data !== 'object') data = {};
if (!data.error_code) data.error_code = 1001;
switch (status) {
case 403:
if (!data.message) data.message = '参数错误';
break;
case 404:
if (!data.message) data.message = '未找到相关服务';
break;
case 401:
if (!data.message) data.message = '登陆已失效,请从新登陆!';
store.dispatch('user/logout').catch(() => {});
break;
default:
if (!data.message) data.message = '网络繁忙';
}
return Promise.reject(data);
}
复制代码
我根据本身独立开发的 惟舞网 及 惟舞 APP 站在全干开发者的角度,从通讯协议到具体请求文件的封装,尽量详细地描述了如何实践 RESTful 架构。而现实中的项目确定是变幻无穷的,最终的设计仍是要考虑本身系统的架构规模,设计一套适合本身系统的规范,你们好才是真的好,不必定要严格遵循 RESTful 理论。