前言(為何做)
過去的一段時間,我都認為 接口請求 封裝是前端的必修課。 只要是寫過生產環境前端代碼的人,應該都脫離不了異步接口請求,那么 接口請求 的 封裝 是必經之路。
直到前些天,我們屋某個美團寫后臺的小姑娘問我前端問題時。我才發現她們代碼中的 接口請求 ,都是沒有任何的封裝,直接采用以下方式進行:
axios.post(`/api/xxxx/xxxx?xxx=${xxx}`, { ...data })
.then((result) => {
if (result.errno) {
....
} else {
....
}
})
.catch((err) => {
....
})
這樣寫也不是說不好,在某種程度上,這增加了代碼的可讀性。 但是我們大多數頁面需要的接口都不止一個,那么我們的組件中極有可能出現 數十上百 行重復代碼。
那么隨著請求的體量越來越大,我們的項目便越來越難以維護。
效果演示
const [e, r] = await api.getUserInfo(userid)
if (!e && r) this.userInfo = r.data.userinfo
上面是我們最終的實現效果。
接下來,我將帶大家一步一步封裝一套屬于我們自己的 接口請求工具 ,同時也希望大家分享更好的思路。
注:
- 如果你希望直接看源碼,請翻到 《完整代碼》
- 這里以
axios
作示范,同樣換成fetch
、 小程序的request
都是可以的- 我將會采用
typeScript
書寫這段教程,如果你不需要,忽略掉對應的類型即可
思路清晰,先說分析(做什么)
在我們正式開發前,首先需要清楚請求一個接口都做了什么。
為此,消耗了兩個小時時間,做了一個請求流程圖,以便于我們后續進行需求分析(小聲bb:Processon真難用 )
有了一個清晰的請求流程圖,我們便可以區分出來兩塊重要的內容來進行拆分: 基礎請求流程 、 攔截器 。
接下來我們將兩塊兒內容展開講。
基礎請求流程
基礎請求流程,我們大致可以分為三塊, 一是 請求進入請求攔截前 、二是 真正發起的請求 、三是 請求從響應攔截出來后 。
這其中可以歸為兩類, 一類是 針對單獨接口的處理 二類是 針對所有接口需要的內容
-
針對單獨接口的處理
-
請求前的參數處理
-
請求后的返回值處理
-
針對所有接口的處理
-
Post
-
Get
-
Put
-
Del
攔截器
攔截器,我們大致可以分為兩類, 一類是 請求接口前的統一處理(請求攔截) 、 一類是 請求接口后的統一處理(響應攔截)
-
請求攔截
-
請求調整
-
用戶標識
-
響應攔截
-
網絡錯誤處理
-
授權錯誤處理
-
普通錯誤處理
-
代碼異常處理
統一調用
隨著我們的 Api
越來越多,我們可能需要給他們不同的分類,但我們并不希望每次調用都從不同的文件夾引入不同的 Api
,因此在 基礎請求 + 攔截器 之外,我們還需要一個封包操作。
開發順序
隨著我們要做的內容越來越多,我們希望它有一個順序以便于我們按部就班的開發(相信大家對開發中出現的不確定性都深惡痛絕)。 以便于我們按照流程,無意外、無驚喜 的完成此次封裝。
在我們的開發中,我們基本要遵循先處理通用內容在處理個性化內容的邏輯:
- 針對所有接口的處理(Get)
- 請求攔截
- 響應攔截
- 針對單獨接口的處理
- 封包處理
- 針對所有接口的處理(Post、Put、Del)
tips
這里大家可能意外為什么 Post、Put、Del 的處理在最后開發: 因為大多數情況,我們開發中希望所編寫的內容有一個及時的回饋。
舉個栗子:我在生活中發現 → 我們學習吉他時,大多數人半途而廢了。但堅持下來的人基本無一例外的通過吉他在不同的階段都獲得了好處,包括但不限于 異性 的夸獎、舍友的鼓掌、 get女朋友 。 這也是我們在畢業獨處后,很難學會彈吉他的原因(無處炫耀)。
因此,我們需要讓所開發的內容盡快達到可用的階段(MVP)。
萬事俱備、只欠東風(怎么做)
按照我們之前定好的順序,按部就班的開發⑧!
針對所有接口的處理(Get)
我們希望以 const [e, r] = await api.getUserInfo(id)
的方式調用,代表著我們需要保證返回值穩定的返回 [err, result]
,所以我們需要在請求無論成功失敗時,都以 resolve
方式調用。
同時,我們希望我們可以處理返回值,因此在這里封裝了 clearFn
的回調函數。
type Fn = (data: FcResponse<any>) => unknown
interface IAnyObj {
[index: string]: unknown
}
interface FcResponse<T> {
errno: string
errmsg: string
data: T
}
const get = <T,>(url: string, params: IAnyObj = {}, clearFn?: Fn): Promise<[any, FcResponse<T> | undefined]> =>
new Promise((resolve) => {
axios
.get(url, { params })
.then((result) => {
let res: FcResponse<T>
if (clearFn !== undefined) {
res = clearFn(result.data) as unknown as FcResponse<T>
} else {
res = result.data as FcResponse<T>
}
resolve([null, res as FcResponse<T>])
})
.catch((err) => {
resolve([err, undefined])
})
})
請求攔截
請求攔截中,我們需要兩塊內容,一是 請求的調整 ,二是 配置用戶標識
const handleRequestHeader = (config) => {
config['xxxx'] = 'xxx'
return config
}
const handleAuth = (config) => {
config.header['token'] = localStorage.getItem('token') || token || ''
return config
}
axios.interceptors.request.use((config) => {
config = handleChangeRequestHeader(config)
config = handleConfigureAuth(config)
return config
})
響應攔截
響應錯誤由三類錯誤組成:
- 網絡錯誤處理
- 授權錯誤處理
- 普通錯誤處理
因此,要優雅的處理響應攔截,我們必須先將三類錯誤函數寫好,以便于我們增強代碼擴展性及后期維護。
錯誤處理函數
const handleNetworkError = (errStatus) => {
let errMessage = '未知錯誤'
if (errStatus) {
switch (errStatus) {
case 400:
errMessage = '錯誤的請求'
break
case 401:
errMessage = '未授權,請重新登錄'
break
case 403:
errMessage = '拒絕訪問'
break
case 404:
errMessage = '請求錯誤,未找到該資源'
break
case 405:
errMessage = '請求方法未允許'
break
case 408:
errMessage = '請求超時'
break
case 500:
errMessage = '服務器端出錯'
break
case 501:
errMessage = '網絡未實現'
break
case 502:
errMessage = '網絡錯誤'
break
case 503:
errMessage = '服務不可用'
break
case 504:
errMessage = '網絡超時'
break
case 505:
errMessage = 'http版本不支持該請求'
break
default:
errMessage = `其他連接錯誤 --${errStatus}`
}
} else {
errMessage = `無法連接到服務器!`
}
message.error(errMessage)
}
const handleAuthError = (errno) => {
const authErrMap: any = {
'10031': '登錄失效,需要重新登錄', // token 失效
'10032': '您太久沒登錄,請重新登錄~', // token 過期
'10033': '賬戶未綁定角色,請聯系管理員綁定角色',
'10034': '該用戶未注冊,請聯系管理員注冊用戶',
'10035': 'code 無法獲取對應第三方平臺用戶',
'10036': '該賬戶未關聯員工,請聯系管理員做關聯',
'10037': '賬號已無效',
'10038': '賬號未找到',
}
if (authErrMap.hasOwnProperty(errno)) {
message.error(authErrMap[errno])
// 授權錯誤,登出賬戶
logout()
return false
}
return true
}
const handleGeneralError = (errno, errmsg) => {
if (err.errno !== '0') {
meessage.error(err.errmsg)
return false
}
return true
}
適配
當我們將所有的錯誤類型處理函數寫完,在 axios
的攔截器中進行調用即可。
axios.interceptors.response.use(
(response) => {
if (response.status !== 200) return Promise.reject(response.data)
handleAuthError(response.data.errno)
handleGeneralError(response.data.errno, response.data.errmsg)
return response
},
(err) => {
handleNetworkError(err.response.status)
Promise.reject(err.response)
}
)
針對單獨接口的處理
基于上面的幾類通用處理,我們這個請求的封裝基本已經可用了。
但是我們還有一些額外的操作無處存放(參數處理、返回值處理),且我們并不想將他們耦合在頁面中每次調用進行處理,那么我們顯然需要一個位置來處理這些內容。
import { Get } from "../server"
interface FcResponse<T> {
errno: string
errmsg: string
data: T
}
type ApiResponse<T> = Promise<[any, FcResponse<T> | undefined]>
function getUserInfo<T extends { id: string; name: string; }>(id): ApiResponse<T> {
return Get<T>('/user/info', { userid: id })
}
封包處理
接口分類封包
用戶數據: api/path/user.ts
import { Get } from "../server"
export function getUserInfo(id) { ... }
export function getUserName(id) { ... }
export const userApi = {
getUserInfo,
getUserName
}
訂單數據: api/path/shoporder.ts
import { Get } from "../server"
function getShoporderDetail() { ... }
function getShoporderList() { ... }
export const shoporderApi = {
getShoporderDetail,
getShoporderList
}
調用點統一
api/index.ts
import { userApi } from "./path/user"
import { shoporderApi } from "./path/shoporder"
export const api = {
...userApi,
...shoporderApi
}
針對所有接口的處理(Post、Put、Del)
export const post = <T,>(url: string, data: IAnyObj, params: IAnyObj = {}): Promise<[any, FcResponse<T> | undefined]> => {
return new Promise((resolve) => {
axios
.post(url, data, { params })
.then((result) => {
resolve([null, result.data as FcResponse<T>])
})
.catch((err) => {
resolve([err, undefined])
})
})
}
// Put / Del 同理
完整代碼
業務處理函數:
src/api/tool.ts
const handleRequestHeader = (config) => {
config['xxxx'] = 'xxx'
return config
}
const handleAuth = (config) => {
config.header['token'] = localStorage.getItem('token') || token || ''
return config
}
const handleNetworkError = (errStatus) => {
let errMessage = '未知錯誤'
if (errStatus) {
switch (errStatus) {
case 400:
errMessage = '錯誤的請求'
break
case 401:
errMessage = '未授權,請重新登錄'
break
case 403:
errMessage = '拒絕訪問'
break
case 404:
errMessage = '請求錯誤,未找到該資源'
break
case 405:
errMessage = '請求方法未允許'
break
case 408:
errMessage = '請求超時'
break
case 500:
errMessage = '服務器端出錯'
break
case 501:
errMessage = '網絡未實現'
break
case 502:
errMessage = '網絡錯誤'
break
case 503:
errMessage = '服務不可用'
break
case 504:
errMessage = '網絡超時'
break
case 505:
errMessage = 'http版本不支持該請求'
break
default:
errMessage = `其他連接錯誤 --${errStatus}`
}
} else {
errMessage = `無法連接到服務器!`
}
message.error(errMessage)
}
const handleAuthError = (errno) => {
const authErrMap: any = {
'10031': '登錄失效,需要重新登錄', // token 失效
'10032': '您太久沒登錄,請重新登錄~', // token 過期
'10033': '賬戶未綁定角色,請聯系管理員綁定角色',
'10034': '該用戶未注冊,請聯系管理員注冊用戶',
'10035': 'code 無法獲取對應第三方平臺用戶',
'10036': '該賬戶未關聯員工,請聯系管理員做關聯',
'10037': '賬號已無效',
'10038': '賬號未找到',
}
if (authErrMap.hasOwnProperty(errno)) {
message.error(authErrMap[errno])
// 授權錯誤,登出賬戶
logout()
return false
}
return true
}
const handleGeneralError = (errno, errmsg) => {
if (err.errno !== '0') {
meessage.error(err.errmsg)
return false
}
return true
}
通用操作封裝: src/api/server.ts
import axios from 'axios'
import { message } from 'antd'
import {
handleChangeRequestHeader,
handleConfigureAuth,
handleAuthError,
handleGeneralError,
handleNetworkError
} from './tools'
type Fn = (data: FcResponse<any>) => unknown
interface IAnyObj {
[index: string]: unknown
}
interface FcResponse<T> {
errno: string
errmsg: string
data: T
}
axios.interceptors.request.use((config) => {
config = handleChangeRequestHeader(config)
config = handleConfigureAuth(config)
return config
})
axios.interceptors.response.use(
(response) => {
if (response.status !== 200) return Promise.reject(response.data)
handleAuthError(response.data.errno)
handleGeneralError(response.data.errno, response.data.errmsg)
return response
},
(err) => {
handleNetworkError(err.response.status)
Promise.reject(err.response)
}
)
export const Get = <T,>(url: string, params: IAnyObj = {}, clearFn?: Fn): Promise<[any, FcResponse<T> | undefined]> =>
new Promise((resolve) => {
axios
.get(url, { params })
.then((result) => {
let res: FcResponse<T>
if (clearFn !== undefined) {
res = clearFn(result.data) as unknown as FcResponse<T>
} else {
res = result.data as FcResponse<T>
}
resolve([null, res as FcResponse<T>])
})
.catch((err) => {
resolve([err, undefined])
})
})
export const Post = <T,>(url: string, data: IAnyObj, params: IAnyObj = {}): Promise<[any, FcResponse<T> | undefined]> => {
return new Promise((resolve) => {
axios
.post(url, data, { params })
.then((result) => {
resolve([null, result.data as FcResponse<T>])
})
.catch((err) => {
resolve([err, undefined])
})
})
}
統一調用點: src/api/index.ts
import { userApi } from "./path/user"
import { shoporderApi } from "./path/shoporder"
export const api = {
...userApi,
...shoporderApi
}
接口: src/api/path/user.ts
| src/api/path/shoporder.ts
import { Get } from "../server"
export function getUserInfo(id) { ... }
export function getUserName(id) { ... }
export const userApi = {
getUserInfo,
getUserName
}
import { Get } from "../server"
function getShoporderDetail() { ... }
function getShoporderList() { ... }
export const shoporderApi = {
getShoporderDetail,
getShoporderList
}