在視頻網站項目中實踐 RESTful 架構經驗總結

在社區常常看到前端的兄弟萌吐槽後端的年輕人不講碼德,來!騙!來!糊弄!亂改接口,動不動格式就變了,我大意了,字符串沒有判空,控制檯一片紅。要麼就是返回的數據嵌套太深,一層包一層,你擱這俄羅斯套娃呢?而若是按照 RESTful 架構來設計接口,就不會存在這種相似的問題。php

耗子喂汁.png

衆所周知,RESTful API 是一套成熟的 API 設計理論,它不只有結構清晰、易於理解、方便擴展等諸多優勢,並且它的做者 Roy Thomas Fielding 是位巨佬,他是 HTTP 規範的主要做者、Apache 服務器的共同創始人並在 Adobe 擔任首席科學家,跟隨巨佬的腳步,能夠少走不少彎路。css

本文我將記錄在視頻網站項目中實踐 RESTful 架構的經驗與心得。例如,設計 Laravel 的接口、在 Vue 中作相應的對接工做等,這樣媽媽就不再用擔憂個人接口問題了,針不戳!html

通訊協議

服務端使用 HTTPS 做爲通訊協議,不只比 HTTP 更加安全,並且現代瀏覽器對 HTTP 2 的支持已經逐漸成熟,性能方面也有很大提升。因此即使用戶以 HTTP 協議訪問接口,咱們也直接將訪問重定向至 HTTPS 協議,非常省心!前端

Nginx 配置

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.pemssl_certificate_key /etc/nginx/ssl/www.key 是配置 HTTPS 所須要的 SSL 證書,直接使用 阿里雲免費證書 就好,話提及來,我已經白嫖好幾年了,嚶嚶嚶~web

我也要 給我也弄一個.jpg

域名

專用域名

大型項目通常都會將接口部署在專用域名之下。例如,掘金的接口項目部署在 api.juejin.cn 下,前端 Vue 項目部署在 juejin.cn 下。這樣作的優勢是方便擴展,缺點是存在跨域問題,瀏覽器每次發送複雜請求時(例如掘金的點贊接口),都會先發送一個 OPTIONS 預檢請求,探測服務端的跨域規則,若服務端容許跨域纔會繼續發送真正的異步請求。以下圖所示:正則表達式

options.png

能夠從上圖中發現掘金服務端設置的一些跨域規則,有一條 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) 狀態碼,意爲直接從內存中讀取該圖片,以下圖:

dishcache.png

假設咱們開發人員在距離用戶第一次訪問 30 秒內在服務端對 Vue 項目的代碼進行了更新,用戶在 30 秒後使用以上方式再次刷新頁面,瀏覽器則從新請求服務器,根據請求頭中的 last-modifiedetagexpires 等規則,判斷是否須要下載最新資源,若是資源發生改變,則下載最新資源、更新緩存。

一個小細節

若是用戶點擊刷新按鈕點擊地址欄並按回車鍵,瀏覽器每次都會從新訪問服務器,若服務器資源已發生改變,則從新下載資源,若未改變,則從緩存、硬盤中讀取。兄弟萌能夠用 Chrome 試試,若是返回 304 狀態碼,就表明從新訪問了服務器,但資源未發生改變,再次從緩存、硬盤中讀取資源,以下圖:

304.png

版本號

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 架構設計的,以下圖所示:

path.png

使用名詞複數形式

仍是那句話, 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-zA-Z0-9 和下劃線。例如,hello_world 將被視爲一個單詞字符,而 hello-world 將被視爲兩個單詞。大部分狀況下,前端的路由名稱與接口的路徑名稱保持統一,不只規範而且利於搜索引擎的關鍵詞收錄。

  • 使用分隔符 - 分隔單詞,比下劃線 _ 看起來更加容易分辨,鍵盤上也能夠少按一個 Shift 鍵。

綜上所述,我推薦使用分隔符 - 對名詞進行分隔。

查詢字符串

查詢字符串是 URL 的最後一部分,通常用於對結果返回結果的過濾。例如,獲取文章列表第一頁的 20 條記錄:

/articles?page=1&size=20
複製代碼

只獲取 user_id233 的用戶的文章:

/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-Typetext/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 作出不一樣的動做。例如,惟舞網的註冊組件就是這樣作的:

errorcode.png

下面列舉我在項目中使用 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_id233 的用戶的我的資料:

/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_id233 的用戶我的資料:

/users/233
複製代碼

假設 user 表有如下 4 個字段儲存用戶我的資料,則將這 4 個字段所有提交:

{
    "nickname": "聰聰",
    "avatar": "avatar.jpg",
    "phone": 12345678910,
    "psw": "abc123456"
}
複製代碼

服務端返回 201 狀態碼:

{
  "message": "ok",
  "error_code": 0
}
複製代碼

PATCH

修改 user_id233 的用戶手機號:

/users/233
複製代碼

提交的數據中只須要包含手機號與驗證碼便可,後端將不會對其餘信息進行更改:

{
    "phone": 12345678910,
    "code": "123456"
}
複製代碼

服務端返回 201 狀態碼:

{
  "message": "ok",
  "error_code": 0
}
複製代碼

DELETE

註銷 user_id233 的用戶:

/users/233
複製代碼

服務端返回 204 No Content 狀態碼

其實所謂的刪除,實際項目中都是軟刪除,例如將字段 is_del 的值從 0 更新爲 1,後端不可能使用 DELETE 操做真正對數據進行物理刪除,以便用戶誤操做後找回數據。

HEAD

視頻播放頁須要先獲取視頻的大小,作一些初始化操做。獲取 video_id233 的視頻元數據:

/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 理論。

相關文章
相關標籤/搜索