在社区经常看到前端的兄弟萌吐槽后端的年轻人不讲码德,来!骗!来!糊弄!乱改接口,动不动格式就变了,我大意了,字符串没有判空,控制台一片红。要么就是返回的数据嵌套太深,一层包一层,你搁这俄罗斯套娃呢?而如果按照 RESTful 架构来设计接口,就不会存在这种类似的问题。
众所周知,RESTful API 是一套成熟的 API 设计理论,它不仅有结构清晰、易于理解、方便扩展等诸多优点,而且它的作者 Roy Thomas Fielding 是位巨佬,他是 HTTP 规范的主要作者、Apache 服务器的共同创始人并在 Adobe 担任首席科学家,跟随巨佬的脚步,可以少走很多弯路。
本文我将记录在视频网站项目中实践 RESTful 架构的经验与心得。例如,设计 Laravel 的接口、在 Vue 中做相应的对接工作等,这样妈妈就再也不用担心我的接口问题了,针不戳!
通信协议
服务端使用 HTTPS 作为通信协议,不仅比 HTTP 更加安全,而且现代浏览器对 HTTP 2 的支持已经逐渐成熟,性能方面也有很大提高。所以即便用户以 HTTP 协议访问接口,我们也直接将访问重定向至 HTTPS 协议,很是省心!
Nginx 配置
在 nginx.conf
中添加如下配置完成重定向的配置:
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/*
,兄弟萌可以随意访问进行测试。
接着添加如下代码即可配置 HTTPS 并开启 HTTP 2:
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 证书,直接使用 阿里云免费证书 就好,话说起来,我已经白嫖好几年了,嘤嘤嘤~
域名
专用域名
大型项目一般都会将接口部署在专用域名之下。例如,掘金的接口项目部署在 api.juejin.cn
下,前端 Vue 项目部署在 juejin.cn
下。这样做的优点是方便扩展,缺点是存在跨域问题,浏览器每次发送复杂请求时(例如掘金的点赞接口),都会先发送一个 OPTIONS
预检请求,探测服务端的跨域规则,若服务端允许跨域才会继续发送真正的异步请求。如下图所示:
可以从上图中发现掘金服务端设置的一些跨域规则,有一条 access-control-max-age: 86400
,意为浏览器对点赞接口发送了一次 OPTIONS
预检请求后,会缓存一天的时间,一天内对点赞接口的后续访问都不会再次发送预检请求。此规则很好地避免了浏览器发送过多的预检请求,浪费服务器资源。
其实跨域还会存在一些例如 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
将版本号放入 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
数据库的表名为什么使用单数
1、直观
你有一个袋子,里面有好多个苹果,你会说这是个苹果袋。但无论里面有 0、1 还是 1000 个苹果,它依然是个袋子。表也是如此,表名需要描述清楚它所包含的对象,而非有多少个数据。
2、便利
单数形式更简单。有一些单词,它的复数形式可能不是常规的,或者就没有复数形式,但是单数不一样,单数形式则没那么多讲究。有些单词的复数,可能会让你想到头大,可能得好好谷歌才能找到。
3、优雅
特别是一些 master_detail
形式的资源名称,统一用单数,读起来更方便,对齐更整齐,从顺序上更有逻辑性。例如:
// 单数:
order
// 复数:
orders
// 单数:
order_detail
// 复数:
order_details
4、简单朴素
设想下,不论是表名、主键、关系还是实例,你都可以统一用单数,看上去非常统一,也不用费心地各种复数单数中转换你的思维。例如:
# 表名
customer
# 主键
customer.customer_id
# 关联表
customer_address
# 方法名
public function getCustomer { }
# 查询语句
SELECT FROM customer WHERE customer_id = 100
一旦你确定将这个对象名称定为 customer
,那么所有和数据库相关的交互、编程就都可以使用这个单词。
5、全球化
假设你身处一个全球化的团队,成员中有些人的母语不是英文(说的就是我),对于他们来说,辨认和书写一个单词的复数形式更加困难,会给他们带来麻烦,也给团队合作带来麻烦。
6、效率
可以节省你的拼写时间与硬盘空间,甚至让你的键盘更“长寿”。
综上所述,我推荐在数据库中使用单数表名,而在 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 的子集,浏览器对它的支持有着天然的优势。
Vue 中的设置
如果使用 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 中的设置
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 动词与状态码
客户端使用不同的 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 动词的一些例子。
GET
获取用户列表:
/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"
}
POST
注册一个新用户:
/users
假设通过手机验证码注册,则提交的数据如下:
{
"sign_mode": 1,
"phone": 12345678910,
"code": 123456,
"nickname": "聪聪",
"psw": "abc123456"
}
服务端返回 201
状态码:
{
"id": 233,
"token": "abc123",
"nickname": "聪聪",
"avatar": "avatar.jpg",
"phone": "181****9876"
}
PUT
修改 user_id
为 233
的用户个人资料:
/users/233
假设 user
表有以下 4 个字段储存用户个人资料,则将这 4 个字段全部提交:
{
"nickname": "聪聪",
"avatar": "avatar.jpg",
"phone": 12345678910,
"psw": "abc123456"
}
服务端返回 201
状态码:
{
"message": "ok",
"error_code": 0
}
PATCH
修改 user_id
为 233
的用户手机号:
/users/233
提交的数据中只需要包含手机号与验证码即可,后端将不会对其他信息进行更改:
{
"phone": 12345678910,
"code": "123456"
}
服务端返回 201
状态码:
{
"message": "ok",
"error_code": 0
}
DELETE
注销 user_id
为 233
的用户:
/users/233
服务端返回 204 No Content
状态码
其实所谓的删除,实际项目中都是软删除,例如将字段 is_del
的值从 0
更新为 1
,后端不可能使用 DELETE
操作真正对数据进行物理删除,以便用户误操作后找回数据。
HEAD
视频播放页需要先获取视频的大小,做一些初始化操作。获取 video_id
为 233
的视频元数据:
/videos/233
OPTIONS
前面提过该动词,但我在实际项目中也很少主动使用,都是浏览器用于探测跨域规则自动发送的。
Laravel 对返回数据的处理
在生产环境中,服务端一定要关闭 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();
}
}
封装 axios
在 /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
如果是个人项目,例如我的博客,不注重兼容性,可以直接使用浏览器自带的 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 理论。
