【W(wǎng)eb技術(shù)】1514- 前端架構(gòu)帶你封裝axios

原文鏈接: https://juejin.cn/post/7124573626161954823
作者:sincenir, 源碼git地址:https://github.com/Sincenir/si-api 覺得對大家有幫助的,望留下 點贊 、 收藏 、 star ~~~
前言(為何做)
過去的一段時間,我都認(rèn)為 接口請求 封裝是前端的必修課。只要是寫過生產(chǎn)環(huán)境前端代碼的人,應(yīng)該都脫離不了異步接口請求,那么 接口請求 的 封裝 是必經(jīng)之路。
直到前些天,我們屋某個美團(tuán)寫后臺的小姑娘問我前端問題時。我才發(fā)現(xiàn)她們代碼中的 接口請求 ,都是沒有任何的封裝,直接采用以下方式進(jìn)行:
axios.post(`/api/xxxx/xxxx?xxx=${xxx}`, { ...data })
.then((result) => {
if (result.errno) {
....
} else {
....
}
})
.catch((err) => {
....
})
這樣寫也不是說不好,在某種程度上,這增加了代碼的可讀性。
但是我們大多數(shù)頁面需要的接口都不止一個,那么我們的組件中極有可能出現(xiàn) 數(shù)十上百 行重復(fù)代碼。
那么隨著請求的體量越來越大,我們的項目便越來越難以維護(hù)。
效果演示
const [e, r] = await api.getUserInfo(userid)
if (!e && r) this.userInfo = r.data.userinfo
上面是我們最終的實現(xiàn)效果。
接下來,我將帶大家一步一步封裝一套屬于我們自己的 接口請求工具 ,同時也希望大家分享更好的思路。
注:
如果你希望直接看源碼,請翻到 《完整代碼》 這里以 axios作示范,同樣換成fetch、 小程序的request都是可以的我將會采用 typeScript書寫這段教程,如果你不需要,忽略掉對應(yīng)的類型即可
思路清晰,先說分析(做什么)
在我們正式開發(fā)前,首先需要清楚請求一個接口都做了什么。
為此,消耗了兩個小時時間,做了一個請求流程圖,以便于我們后續(xù)進(jìn)行需求分析(小聲bb:Processon真難用 ??)

有了一個清晰的請求流程圖,我們便可以區(qū)分出來兩塊重要的內(nèi)容來進(jìn)行拆分:基礎(chǔ)請求流程 、 攔截器 。
接下來我們將兩塊兒內(nèi)容展開講。
基礎(chǔ)請求流程
基礎(chǔ)請求流程,我們大致可以分為三塊, 一是 請求進(jìn)入請求攔截前 、二是 真正發(fā)起的請求 、三是 請求從響應(yīng)攔截出來后 。
這其中可以歸為兩類,
一類是 針對單獨接口的處理
二類是 針對所有接口需要的內(nèi)容
針對單獨接口的處理 請求前的參數(shù)處理 請求后的返回值處理 針對所有接口的處理 Post Get Put Del
攔截器
攔截器,我們大致可以分為兩類, 一類是 請求接口前的統(tǒng)一處理(請求攔截) 、 一類是 請求接口后的統(tǒng)一處理(響應(yīng)攔截)
請求攔截 請求調(diào)整 用戶標(biāo)識 響應(yīng)攔截 網(wǎng)絡(luò)錯誤處理 授權(quán)錯誤處理 普通錯誤處理 代碼異常處理
統(tǒng)一調(diào)用
隨著我們的 Api 越來越多,我們可能需要給他們不同的分類,但我們并不希望每次調(diào)用都從不同的文件夾引入不同的 Api ,因此在 基礎(chǔ)請求 + 攔截器 之外,我們還需要一個封包操作。
開發(fā)順序
隨著我們要做的內(nèi)容越來越多,我們希望它有一個順序以便于我們按部就班的開發(fā)(相信大家對開發(fā)中出現(xiàn)的不確定性都深惡痛絕)。
以便于我們按照流程,無意外、無驚喜 的完成此次封裝。
在我們的開發(fā)中,我們基本要遵循先處理通用內(nèi)容在處理個性化內(nèi)容的邏輯:
針對所有接口的處理(Get) 請求攔截 響應(yīng)攔截 針對單獨接口的處理 封包處理 針對所有接口的處理(Post、Put、Del)
tips
?? 這里大家可能意外為什么 Post、Put、Del 的處理在最后開發(fā):因為大多數(shù)情況,我們開發(fā)中希望所編寫的內(nèi)容有一個及時的回饋。
舉個栗子:我在生活中發(fā)現(xiàn) → 我們學(xué)習(xí)吉他時,大多數(shù)人半途而廢了。但堅持下來的人基本無一例外的通過吉他在不同的階段都獲得了好處,包括但不限于 異性 的夸獎、舍友的鼓掌、 get女朋友 。這也是我們在畢業(yè)獨處后,很難學(xué)會彈吉他的原因(無處炫耀)。
因此,我們需要讓所開發(fā)的內(nèi)容盡快達(dá)到可用的階段(MVP)。
萬事俱備、只欠東風(fēng)(怎么做)
按照我們之前定好的順序,按部就班的開發(fā)⑧!
針對所有接口的處理(Get)
我們希望以 const [e, r] = await api.getUserInfo(id) 的方式調(diào)用,代表著我們需要保證返回值穩(wěn)定的返回 [err, result] ,所以我們需要在請求無論成功失敗時,都以 resolve 方式調(diào)用。
同時,我們希望我們可以處理返回值,因此在這里封裝了 clearFn 的回調(diào)函數(shù)。
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])
})
})
請求攔截
請求攔截中,我們需要兩塊內(nèi)容,一是 請求的調(diào)整 ,二是 配置用戶標(biāo)識
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
})
響應(yīng)攔截
響應(yīng)錯誤由三類錯誤組成:
網(wǎng)絡(luò)錯誤處理 授權(quán)錯誤處理 普通錯誤處理
因此,要優(yōu)雅的處理響應(yīng)攔截,我們必須先將三類錯誤函數(shù)寫好,以便于我們增強代碼擴(kuò)展性及后期維護(hù)。
錯誤處理函數(shù)
const handleNetworkError = (errStatus) => {
let errMessage = '未知錯誤'
if (errStatus) {
switch (errStatus) {
case 400:
errMessage = '錯誤的請求'
break
case 401:
errMessage = '未授權(quán),請重新登錄'
break
case 403:
errMessage = '拒絕訪問'
break
case 404:
errMessage = '請求錯誤,未找到該資源'
break
case 405:
errMessage = '請求方法未允許'
break
case 408:
errMessage = '請求超時'
break
case 500:
errMessage = '服務(wù)器端出錯'
break
case 501:
errMessage = '網(wǎng)絡(luò)未實現(xiàn)'
break
case 502:
errMessage = '網(wǎng)絡(luò)錯誤'
break
case 503:
errMessage = '服務(wù)不可用'
break
case 504:
errMessage = '網(wǎng)絡(luò)超時'
break
case 505:
errMessage = 'http版本不支持該請求'
break
default:
errMessage = `其他連接錯誤 --${errStatus}`
}
} else {
errMessage = `無法連接到服務(wù)器!`
}
message.error(errMessage)
}
const handleAuthError = (errno) => {
const authErrMap: any = {
'10031': '登錄失效,需要重新登錄', // token 失效
'10032': '您太久沒登錄,請重新登錄~', // token 過期
'10033': '賬戶未綁定角色,請聯(lián)系管理員綁定角色',
'10034': '該用戶未注冊,請聯(lián)系管理員注冊用戶',
'10035': 'code 無法獲取對應(yīng)第三方平臺用戶',
'10036': '該賬戶未關(guān)聯(lián)員工,請聯(lián)系管理員做關(guān)聯(lián)',
'10037': '賬號已無效',
'10038': '賬號未找到',
}
if (authErrMap.hasOwnProperty(errno)) {
message.error(authErrMap[errno])
// 授權(quán)錯誤,登出賬戶
logout()
return false
}
return true
}
const handleGeneralError = (errno, errmsg) => {
if (err.errno !== '0') {
meessage.error(err.errmsg)
return false
}
return true
}
適配
當(dāng)我們將所有的錯誤類型處理函數(shù)寫完,在 axios 的攔截器中進(jìn)行調(diào)用即可。
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)
}
)
針對單獨接口的處理
基于上面的幾類通用處理,我們這個請求的封裝基本已經(jīng)可用了。
但是我們還有一些額外的操作無處存放(參數(shù)處理、返回值處理),且我們并不想將他們耦合在頁面中每次調(diào)用進(jìn)行處理,那么我們顯然需要一個位置來處理這些內(nèi)容。
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 })
}
封包處理
接口分類封包
用戶數(shù)據(jù):api/path/user.ts
import { Get } from "../server"
export function getUserInfo(id) { ... }
export function getUserName(id) { ... }
export const userApi = {
getUserInfo,
getUserName
}
訂單數(shù)據(jù):api/path/shoporder.ts
import { Get } from "../server"
function getShoporderDetail() { ... }
function getShoporderList() { ... }
export const shoporderApi = {
getShoporderDetail,
getShoporderList
}
調(diào)用點統(tǒng)一
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 同理
完整代碼
業(yè)務(wù)處理函數(shù):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 = '未授權(quán),請重新登錄'
break
case 403:
errMessage = '拒絕訪問'
break
case 404:
errMessage = '請求錯誤,未找到該資源'
break
case 405:
errMessage = '請求方法未允許'
break
case 408:
errMessage = '請求超時'
break
case 500:
errMessage = '服務(wù)器端出錯'
break
case 501:
errMessage = '網(wǎng)絡(luò)未實現(xiàn)'
break
case 502:
errMessage = '網(wǎng)絡(luò)錯誤'
break
case 503:
errMessage = '服務(wù)不可用'
break
case 504:
errMessage = '網(wǎng)絡(luò)超時'
break
case 505:
errMessage = 'http版本不支持該請求'
break
default:
errMessage = `其他連接錯誤 --${errStatus}`
}
} else {
errMessage = `無法連接到服務(wù)器!`
}
message.error(errMessage)
}
const handleAuthError = (errno) => {
const authErrMap: any = {
'10031': '登錄失效,需要重新登錄', // token 失效
'10032': '您太久沒登錄,請重新登錄~', // token 過期
'10033': '賬戶未綁定角色,請聯(lián)系管理員綁定角色',
'10034': '該用戶未注冊,請聯(lián)系管理員注冊用戶',
'10035': 'code 無法獲取對應(yīng)第三方平臺用戶',
'10036': '該賬戶未關(guān)聯(lián)員工,請聯(lián)系管理員做關(guān)聯(lián)',
'10037': '賬號已無效',
'10038': '賬號未找到',
}
if (authErrMap.hasOwnProperty(errno)) {
message.error(authErrMap[errno])
// 授權(quán)錯誤,登出賬戶
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])
})
})
}
統(tǒng)一調(diào)用點: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
}