前言
最近在跟后端對接請求的加解密時,發現之前封裝的 axios 模塊存在觸發兩次攔截器的問題,找了許久問題方才發現是 axios 沒有實例化而直接使用造成的。溯其根源,是之前封裝時並未仔細去看 axios 的文檔,沒完全理解代碼,直接就參考別人封裝好的例子,殊不知別人封裝的也是不完善。因此,結合自己的項目,梳理下 axios 的封裝及 API 接口的管理模塊。
Axios 封裝
axios 本身就是封裝好了的請求庫,為什么我們要對其進行二次封裝呢?這個原因很簡單,和大多數的封裝一樣,目的是為了簡化代碼使用以及便於管理維護。
安裝
npm install axios
// or
yarn add axios
使用
官方的文檔一開始就列舉了直接引入 axios 使用的方法,然而這樣子對我們進行后面的個性化定制不利,所以我們需要進行實例話后再使用,防止重復觸發攔截器。
// request.ts
import axios, { AxiosResponse, AxiosRequestConfig, AxiosError } from 'axios'
// 判斷是否為開發模式
const isDev = process.env.NODE_ENV === 'development'
// 根據不同環境設置不同的請求地址
const baseURL = isDev ? '' : 'http://www.example.com'
// 請求超時設置
const timeout = 6000
// 創建 axios 實例
const http = axios.create({ baseURL, timeout })
// TODO: 攔截器
export { http, axios }
攔截器
在請求數據前和請求數據后我們通常需要做一些認證、錯誤處理、加解密等操作,利用 axios 提供的 interceptors
方法分別對請求和響應進行攔截處理。
// 攔截請求
http.interceptors.request.use(
(config: AxiosRequestConfig) => {
// TODO: 請求加密
return config
},
(error: AxiosError) => {
return Promise.reject(error)
}
)
// 攔截響應
http.interceptors.response.use(
(response: AxiosResponse) => {
if (response) {
// 返回文件類型時不做處理直接返回
if (response.request.responseType === 'arraybuffer') return response
const { code, msg, success } = response.data
// TODO: 解密響應數據
// 根據后端自定義的狀態碼進行操作
switch (code) {
case 7001:
// TODO
break
default:
// Pop message
break
}
}
return Promise.reject(response)
},
(error: AxiosError) => {
if (error.response) {
const { status, data } = error.response
// 處理 HTTP 狀態碼
switch (status) {
case 401:
// TODO: Login
break
default:
// TODO: Pop message
break
}
}
return Promise.reject(error)
}
)
以上只是簡單列舉了攔截器的使用,還有加解密、Loading 控制、請求超時處理等需要另外補充。
封裝
通常情況下我們可以直接使用 axios 的實例方法來調用 get、post 等請求,假如后端返回數據的默認結構為:
{
code: 200,
success: true,
data: {},
msg: '請求成功'
}
正常處理請求返回數據如下:
import { http } from './request'
http.get('/path', { id: 1 }).then(response => {
const { data, code, success, msg } = response.data
if (success) {
// TODO: 處理獲取成功的數據
} else {
// TODO: 錯誤處理
}
})
而通常情況下,我們請求數據只想手動處理頁面顯示所需的數據,那些異常的數據應該全部放到 catch 中處理或者交由全局進行統一處理,比如:
import { http } from './request'
// 處理特殊異常
http.get('/path', { id: 1 }).then(res => {
// TODO: 處理獲取成功的數據
}).catch(err => {
// TODO: 處理異常
})
// 不使用 catch 時
// 異常交由全局默認處理
http.get('/path', { id: 1 }).then(res => {
// TODO: 處理獲取成功的數據
})
要默認處理異常,需要重寫 Promise 的 then
, catch
, finally
方法:
function RequestPromise(originalPromise: Promise<any>) {
this._promise = originalPromise
this._catchyPromise = Promise.resolve()
.then(() => this._promise)
.catch(err => console.error('Error', err)) // 默認處理異常
const methods = ['then', 'catch', 'finally']
for (const method of methods) {
this[method] = function (...args: any) {
this._promise = this._promise[method](...args)
return this
}
}
}
從上面的方法可知,我們的異步請求會被攔截一道,獲取異常並進行錯誤輸出處理,重寫了 catch 方法,方便使用時能進行特殊處理。
// get 請求封裝
const get = <R = any>(uri: string, data?: any, config?: AxiosRequestConfig): Promise<R> => {
// @ts-ignore
return new RequestPromise(new Promise((resolve, reject) => {
http
.get(uri, { params: data, ...config })
.then(response => {
const { success, data } = response.data
if (success) return resolve(data)
return reject(response.data)
})
.catch(reject)
}))
}
除了以上處理,假設我們要請求分頁列表,其后端返回的數據格式為:
{
code: 200,
success: true,
data: {
results: []
},
msg: '請求成功'
}
如果想默認返回 results
字段內容,我們可以在 post、get 請求方法的基礎上更進一步定制化請求方法,如:
// 列表請求封裝
const table = <R = ListResponse>(path: string, pagination?: number, data?: any, config?: AxiosRequestConfig): Promise<R> => {
return new Promise((resolve, reject) => {
const listConfig = {
pageSize: 10,
pageNo: pagination || 1
}
uri = `${uri}/action/table`
get(uri, Object.assign({}, listConfig, data), config).then(data => resolve(data.results)).catch(reject)
})
}
其實,如果是做管理后台系統,可以根據以上實例對常用的增刪查改進行封裝,調用時可大大節省時間。
掛載全局
考慮到會污染全局實例且不便於維護對接,很多人是不推薦掛載請求方法到全局實例上的,這些見仁見智吧。如果拋開不利因素,只為了在頁面中便捷地使用,可以將封裝好的請求方法綁定到全局。具體做法如下:
// main.js or main.ts
// Vue 2.x
import Vue from 'vue'
Vue.prototype.$get = get
Vue.prototype.$post = post
Vue.prototype.$table = table
// Vue 3.x
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
app.config.globalProperties.$get = get
app.config.globalProperties.$post = post
app.config.globalProperties.$table = table
在頁面中使用:
// xxx.vue
// Vue 2.x
this.$post()
// Vue 3.x
const { proxy } = getCurrentInstance()
proxy.$post()
如果項目中使用了 typescript ,為了能智能提示,需要在項目根目錄增加聲明文件:
// request.d.ts
import { AxiosInstance, AxiosRequestConfig } from 'axios'
declare module 'vue/types/vue' {
interface Vue {
$post<R = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<R>
$get<R = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<R>
$table<R = any>(uri: string, pagination?: number, data?: any, config?: AxiosRequestConfig): Promise<R>
}
}
API 接口管理
如果我們將請求方法掛載到了全局,只要新建一個文件來管理請求的接口,在要使用的頁面引入對應接口即可。
// api.ts
export const ACCOUNT = {
logout: 'account/logout',
login: 'account/login'
}
在頁面配合請求方法進行使用:
import { ACCOUNT } from '@/config/api'
this.$post(ACCOUNT.logout).then(() => {
// TODO
}
這樣子,請求和接口就完全分離了。再想想,還有哪里寫得不爽的?在使用的過程中,發現接口定義的時候總是要手動去命名,接口少還能接受,但是接口很多的情況下命名就是一件相當頭疼的事。可為了能夠使用 typescript 的智能提示功能,命名又是必要的。思前想后,最后通過 nodeJS 進行自動化生成接口文件,自動命名。用的時候在配置文件配置下就好,配置文件如下:
// config/api.js
/**
* 接口配置文件
* 接口名稱:[ 接口前綴, ...業務路徑 ]
*/
module.exports = {
account: [
'account',
'logout',
'login'
]
}
為了能夠實現自動更新,使用 nodemon
對 config/api.js
文件進行監聽。文件更改時會自動執行生成腳本,具體的自動生成腳本可以根據自己要輸出的接口結構進行定制。
// build/api.js
// 接口生成腳本
const path = require('path')
const chalk = require('chalk')
const fs = require('fs-extra')
const api = require('../config/api')
const resolve = dir => {
return path.join(__dirname, '..', dir)
}
/**
* 生成 API 文件
* 路徑:src/api/index.ts
*/
function initAPI() {
let tmpStr = ''
for (let key in api) {
let uri = ''
let str = `export const ${key.toUpperCase()} = {\n`
const comma = index => api[key].length - 1 !== index ? ',' : ''
api[key].forEach((item, index) => {
const _key = key.toLowerCase()
if (!index) {
uri = item
str = `const ${_key} = '${uri}/'\n` + str
str += ` uri: ${_key}${comma(index)}\n`
} else {
str += ` ${item.replace(/(\/|\-)/g, '_')}: ${_key} + '${item}'${comma(index)}\n`
}
})
str += '}\n\n'
tmpStr += str
}
return tmpStr
}
const apiPath = resolve('src/api/index.ts')
const status = fs.outputFileSync(apiPath, initAPI())
if (status) {
throw status
} else {
console.log(chalk.blue('生成') + ' 接口文件 ' + apiPath);
}
當然這種做法也是為了偷懶,如果有更好的做法歡迎留言討論。需要注意的是,如果通過 nodeJS 動態生成接口,需要在 .gitignore
中排除掉生成文件的提交,否則會產生提交沖突。
總結
不管是小程序、H5 還是管理系統,請求封裝還是很有必要的,倘若能統一,能在各個項目中復用那就再好不過了。接口管理不僅在項目中管理,使用時還需要配合接口文檔,項目中知道這個接口屬於那個模塊下就好。