簡介

自訂參數可以其調用時更客製化,像是要連兩個不同的 RESTful API 來源時,或著對不同的 API 有不同的等待時間時,都可以更容易的調整想要的呼叫方式。

開發

加入 Axios 的抽像轉換函式

主要為不同的 Axios 實例可以實作自己的轉換函式,所以才需要先定義成抽像的。

建立 ./src/utils/http/axios/axiosTransform.ts

1
2
3
4
5
6
7
8
9
import type { AxiosRequestConfig, AxiosResponse } from 'axios'
import type { RequestOptions, Result } from './types'

export abstract class AxiosTransform {
// 呼叫 Axios.request 前的轉換函式
beforeRequestHook?: (config: AxiosRequestConfig, options: RequestOptions) => AxiosRequestConfig
// 取得 Request Data 後的轉換函式
transformRequestData?: (res: AxiosResponse<Result>, options: RequestOptions) => any
}

調整呼叫 Axios 的 Interface

讓轉換函式及自訂參考都先定義好明確的 Interface,在初使化 Axios 或調用 request 時,可以透過強型態取得

調整 ./src/utils/http/axios/types.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import type { AxiosRequestConfig } from 'axios'
import type { AxiosTransform } from './axiosTransform'

// 繼承 AxiosRequestConfig 並添加自訂的條件,在 Axios 調用或攔截器(Interceptors)中可以使用
export interface CreateAxiosOptions extends AxiosRequestConfig {
// 轉換函式
transform?: AxiosTransform
// 客製化設定參數
requestOptions?: RequestOptions
}

// 自訂 Axios Request 觸發時的一些參數
export interface RequestOptions {
// ----↓ beforeRequestHook 中使用 ↓----
// RESTful API 的 Host URL
apiUrl?: string
// URL 的前綴值(/api/v1)
urlPrefix?: string
// 在 QueryString 後加入 timestamp
joinTime?: boolean
// ----↑ beforeRequestHook 中使用 ↑----

// ----↓ transformRequestData 中使用 ↓----
// 直接回傳原始 AxiosResponse
isReturnNativeResponse?: boolean
// 需要對 AxiosResponse 進行解析並只回傳 Result ,如果為 false 時會回傳 AxiosResponse.data
isTransformResponse?: boolean
// 是否顯示成功或失敗訊息(需 isReturnNativeResponse = false 且 isTransformResponse = true 才會運行)
isShowMessage?: boolean
// ----↑ transformRequestData 中使用 ↑----
}

// 定義回傳的 res.data 格式,這部份要配合 API 的回傳格式,方便取值
export interface Result<T = any> {
code: number
type?: 'success' | 'error' | 'warning'
message: string
result?: T
}

添加 ResultEnum 的可讀性 Enums

調整 ./src/enums/httpEnum.ts 加入 ResultEnum

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
* @description: Request result
*/
export enum ResultEnum {
SUCCESS = 200,
ERROR = -1,
TIMEOUT = 10042,
TYPE = 'success',
}

/**
* @description: Request Verb
*/
export enum RequestEnum {
GET = 'GET',
POST = 'POST',
PATCH = 'PATCH',
PUT = 'PUT',
DELETE = 'DELETE',
}

/**
* @description: Content Type
*/
export enum ContentTypeEnum {
// json
JSON = 'application/json;charset=UTF-8',
// json
TEXT = 'text/plain;charset=UTF-8',
// form-data
FORM_URLENCODED = 'application/x-www-form-urlencoded;charset=UTF-8',
// form-data upload
FORM_DATA = 'multipart/form-data;charset=UTF-8',
}

加入 helper 放置輔助函数

其實就是把一些常用的函数取出,也可以放置原頁面。

建立 ./src/utils/http/axios/helper.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// restful 為 true 時回傳 QueryString ,反之回傳 Object
export function joinTimestamp(join: boolean, restful = false): string | object {
if (!join)
return restful ? '' : {}

const now = new Date().getTime()
if (restful)
return `?_t=${now}`

return { _t: now }
}

// 確認是否完整的 URL
export function isUrl(url: string): boolean {
return /(^http|https:\/\/)/g.test(url)
}

撰寫轉換函式並在建立 Axios 時帶入

實作 Axios 所需要的轉換函式,並在建立 Axios 實體時將其帶入,於 Axios.request 時就可以予之呼叫使用。

另外在建立 Axios 實體時也一併將相關的客製化參數填入。

也可以先看下一章是用在那邊,會更好明白轉換函式的寫法

調整 ./src/utils/http/axios/index.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
import type { AxiosRequestConfig, AxiosResponse } from 'axios'
import { isString } from '@vueuse/core'
import { LKAxios } from './Axios'
import type { CreateAxiosOptions, RequestOptions, Result } from './types'
import type { AxiosTransform } from './axiosTransform'
import { isUrl, joinTimestamp } from './helper'
import { ContentTypeEnum, RequestEnum, ResultEnum } from '~/enums/httpEnum'
import { deepMerge } from '~/utils'

const transform: AxiosTransform = {
beforeRequestHook(config: AxiosRequestConfig, options: RequestOptions) {
const { apiUrl, urlPrefix, joinTime = true } = options
// 是否調用的 URL 為完整的 URL(https://... or /partition)
const isUrlStr = isUrl(config.url as string)

// 當為參考路徑時,填加前綴值在 URL 前面
if (!isUrlStr && urlPrefix && isString(urlPrefix))
config.url = `${urlPrefix}${config.url}`

// 當為參考路徑時,填加預設的 apiUrl 在 URL 前面
if (!isUrlStr && apiUrl && isString(apiUrl))
config.url = `${apiUrl}${config.url}`

// 取得使用者設定的 params ,沒有的話預設為空物件
const params = config.params || {}
// 當 Method 為 GET 時填入 timestamp
if (config.method?.toUpperCase() === RequestEnum.GET) {
if (!isString(params)) {
config.params = Object.assign(params || {}, joinTimestamp(joinTime, false))
}
else {
config.url = `${config.url + params}${joinTimestamp(joinTime, true)}`
config.params = undefined
}
}
return config
},
transformRequestData(res: AxiosResponse<Result>, options: RequestOptions) {
const {
isReturnNativeResponse,
isTransformResponse,
isShowMessage,
} = options
// 當設定為回傳原始 Response 時
if (isReturnNativeResponse)
return res
// 當設定不要對 Response 值進行轉換時
if (!isTransformResponse)
return res.data
const { data } = res
if (!data)
throw new Error('request error, please try again later.')
// 這邊要確保後端回傳的格式是這種,才取的出來
const { code, result, message } = data
const hasSuccess = data && Reflect.has(data, 'code') && code === ResultEnum.SUCCESS
// 如果設定要呈現訊息時
if (isShowMessage) {
// TODO: 最好避免用 console 來呈現
if (hasSuccess)
console.info(`[axios-request]-${message || 'Successful Operation!'}`)
}
// 正確調用時回傳 result
if (code === ResultEnum.SUCCESS)
return result

// 調用失敗時的處理
let errorMsg = message
let neededLogout = false
switch (code) {
case ResultEnum.ERROR:
errorMsg = `${errorMsg || 'Operation Failed!'}`
break
case ResultEnum.TIMEOUT:
errorMsg = 'timeout, please login again'
neededLogout = true
break
}

// 是否要呈現失敗訊息
if (isShowMessage)
console.error(`[axios-request]-${errorMsg}`)

// 是否需要登出
if (neededLogout)
window.location.href = '/'

throw new Error(errorMsg)
},
}

function createAxios(opt?: Partial<CreateAxiosOptions>) {
return new LKAxios(
deepMerge({
transform,
timeout: 10 * 1000,
headers: { 'Content-Type': ContentTypeEnum.JSON },
// 添加額外的客製參數
requestOptions: {
apiUrl: '',
urlPrefix: '/api/v1',
joinTime: true,
isReturnNativeResponse: false,
isTransformResponse: true,
isShowMessage: true,
},
}, opt || {}))
}

// 公開讓其它頁面去調用
export const http = createAxios()

實作調用轉換函式

調整 ./src/utils/http/axios/Axios.ts 中呼叫 request 時去調用轉換函式

可以直接查詢轉換函式的位置並進行添加

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
import axios from 'axios'
import { isFunction } from '@vueuse/core'
import type { CreateAxiosOptions, RequestOptions, Result } from './types'
import { deepMerge } from '~/utils'

export class LKAxios {
// 建立 Axios 實例
private axiosInstance: AxiosInstance
// 初使化的 Axios Options
private options: CreateAxiosOptions
// Initial 該 Class 時呼叫
constructor(options: CreateAxiosOptions) {
this.options = options
this.axiosInstance = axios.create(options)
}

/*
* 調用 Axios.request
* config Axios 支援的設定值
* option 調用時的額外參數
*/
request<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
// 要丟入 Axios 的參數,從傳入值先 Copy 出來
let conf: CreateAxiosOptions = deepMerge({}, config)
// 把初使化的額外參數取出
const { requestOptions } = this.options
// 用調用時的額外參數去蓋掉初使化的(保留初使化中未重覆的)
const opt: RequestOptions = Object.assign({}, requestOptions, options)
conf.requestOptions = opt

// 取得 beforeRequestHook 的轉換函式,並在實際調用 request 前進行轉換
const { transform } = this.options
const { beforeRequestHook, transformRequestData } = transform || {}
if (beforeRequestHook && isFunction(beforeRequestHook))
conf = beforeRequestHook(conf, opt)

return new Promise((resolve, reject) => {
this.axiosInstance
.request<any, AxiosResponse<Result>>(conf)
.then((res: AxiosResponse<Result>) => {
const isCancel = axios.isCancel(res)
// 確認有調用成功時,對回傳的 res 進行轉換
if (transformRequestData && isFunction(transformRequestData) && !isCancel) {
try {
const ret = transformRequestData(res, opt)
resolve(ret)
}
catch (err) {
reject(err || new Error('request error!'))
}
return
}
// 沒寫轉換函式時就直接 resolve
resolve(res as unknown as Promise<T>)
})
.catch((e: Error) => {
reject(e)
})
})
}
}

測試

在任意的 .vue 檔案中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<script setup lang="ts">
import { http } from '~/utils/http/axios'

http.request({ url: '/test', method: 'get', params: { id: 3 } }).then((res) => {
console.log(res)
})

http.request({ url: '/test', method: 'post', data: { name: 'new name' } }).then((res) => {
console.log(res)
})

http.request({ url: '/test/6', method: 'put', data: { name: 'edit name' } }).then((res) => {
console.log(res)
})

http.request({ url: '/test/1', method: 'delete' }).then((res) => {
console.log(res)
})
</script>

確認 URL 有正確被替換

確認呼叫各 Method 都可以正確回傳結果

參考

Naive UI Third-Party Libraries
Naive UI Admin
Axios
Vitesse
Sample Code Download