在社區常常看到前端的兄弟萌吐槽後端的年輕人不講碼德,來!騙!來!糊弄!亂改接口,動不動格式就變了,我大意了,字符串沒有判空,控制檯一片紅。要麼就是返回的數據嵌套太深,一層包一層,你擱這俄羅斯套娃呢?而若是按照 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 理論。