First, let’s set the stage. There are many ways to encapsulate Axios, and numerous open-source frameworks and tools available online that provide powerful encapsulations. These can be referenced and used in actual work.
However, I believe that these framework encapsulations, due to their extensive functionalities, lead to more code and a tighter coupling with their own frameworks, making customization more complex. For someone learning, the cost can be quite high. For instance, if you just want to learn how to use Axios to send requests or create a simple encapsulation suitable for your own system, you might find yourself overwhelmed by the multiple files involved. You could jump from method B in file A to method D in file C, and then check the configuration in file E, which can easily confuse beginners and distract from the main focus.
This article is merely a personal record of my usage of Axios in learning and work, focusing on core functionalities. Some code may not address situations encountered in actual production environments, or the implementation may differ from other mature frameworks; readers should view it dialectically.
There’s no need for a lengthy introduction to Axios; let’s dive straight into the code.
src/utils/axiosService.ts
import axios, { type AxiosInstance, type InternalAxiosRequestConfig, type AxiosResponse, type Method } from 'axios';import qs from 'qs';
/** * Define the configuration interface for canceling requests */export interface CancelRequestConfig { /** Request URL */ url: string; /** Request method (e.g., GET/POST) */ method?: Method; /** Query parameters for GET requests */ params?: Record<string, any="">; /** Payload data for POST requests */ data?: any;}
/** * Encapsulate Axios request service class * Features include: basic configuration, interceptors, request cancellation management */class AxiosService { private instance: AxiosInstance // Axios instance private abortControllerMap: Map<string, abortcontroller;="" controller:="" number="" timestamp:="" {="" }=""> // Added timestamp private cleanupTimer: number | null = null constructor() { // Initialize Axios instance configuration this.instance = axios.create({ baseURL: 'https://youapi.com/api', // Base URL for backend API. Can be obtained from environment variables or configuration items timeout: 4000, // Request timeout })
this.abortControllerMap = new Map() this.setupInterceptors() // Set up interceptors this.startAutoCleanup() // Custom cleanup interval, timeout threshold // this.startAutoCleanup(5 * 60 * 1000,30 * 60 * 1000) }
private startAutoCleanup(interval: number = 5 * 60 * 1000, threshold: number = 30 * 60 * 1000) { this.cleanupTimer = setInterval(() => { const now = Date.now() this.abortControllerMap.forEach((value, key) => { // Clean up old entries older than 30 minutes (customizable threshold) if (now - value.timestamp > threshold) { // Exceeds 30 minutes console.log('[Auto Cleanup] Removing outdated request:', key) value.controller.abort() // Optional: force cancel outdated requests this.abortControllerMap.delete(key) } }) }, interval)// Runs every 5 minutes }
// Stop the timer. // For example, in single-page applications, during route transitions, or when the page is unmounted // axiosService.stopAutoCleanup() public stopAutoCleanup() { if (this.cleanupTimer) { clearInterval(this.cleanupTimer) this.cleanupTimer = null } }
/** * Generate a unique identifier key for the request * @param config Request configuration * @returns A string concatenated from method, URL, parameters, and serialized data */ private generateRequestKey(config: InternalAxiosRequestConfig): string { // Serialization logic ensures that parameter order does not affect uniqueness // For POST requests, consider both request body and request parameters if (config.method === 'post' || config.method === 'POST') { // The response data is a JSON string that needs to be converted to a JSON object. // If not converted, the generated requestKey will not include the request body // The request data is a JSON object, no need to convert // Generally, POST requests will not lack a request body; if there is such a case, handle it separately if (typeof config.data === 'string') { config.data = JSON.parse(config.data) } const reqKey = [ config.method, config.url, config.params ? qs.stringify(config.params, { arrayFormat: 'brackets', sort: (a, b) => a.localeCompare(b) }) : "", config.data ? qs.stringify(config.data, { arrayFormat: 'brackets', sort: (a, b) => a.localeCompare(b) }) : "" ].join('&').toLowerCase()
return reqKey } else { // For GET requests, do not consider the request body const reqKey = [ config.method, config.url, config.params ? qs.stringify(config.params, { arrayFormat: 'brackets', sort: (a, b) => a.localeCompare(b) }) : "" ].join('&').toLowerCase()
return reqKey }
}
/** * Set up request and response interceptors */ private setupInterceptors() { // Request interceptor: handle logic for canceling duplicate requests this.instance.interceptors.request.use( (config: InternalAxiosRequestConfig) => { const controller = new AbortController() const requestKey = this.generateRequestKey(config)
// If a similar request exists, cancel the previous one if (this.abortControllerMap.has(requestKey)) { this.abortControllerMap.get(requestKey)?.controller.abort() }
// Bind the cancel controller and store it config.signal = controller.signal this.abortControllerMap.set(requestKey, { controller, timestamp: Date.now() // Record creation time })
return config }, (error) => Promise.reject(error) )
// Response interceptor: clean up completed requests this.instance.interceptors.response.use( (response: AxiosResponse) => { const requestKey = this.generateRequestKey(response.config) this.abortControllerMap.delete(requestKey) // Request completed, remove controller return response }, (error) => { // Remove controller on response failure as well if (error.config) { const requestKey = this.generateRequestKey(error.config) this.abortControllerMap.delete(requestKey) }
// Handle special cases for canceled requests if (axios.isCancel(error)) { console.warn('Request canceled:', error.message) } else { console.error('Request failed', error.message) } return Promise.reject(error) } ) }
/** * Core method for sending requests * @param method HTTP method * @param url Request path * @param data POST data * @param params GET parameters * @returns Promise containing the response result */ public request<t =="" any="">( method: Method, url: string, data?: unknown, params?: unknown ): Promise<axiosresponse<t>> { return this.instance.request({ method, url, data, params }) }
public post<t =="" any="">( url: string, data?: unknown, ): Promise<axiosresponse<t>> { return this.instance.post(url, data) }
public get<t =="" any="">( url: string, params?: unknown ): Promise<axiosresponse<t>> { return this.instance.get(url, { params: params }) }
/** * Cancel a specific request * @param config Configuration matching the request */ public cancelRequest(config: CancelRequestConfig): void { const requestKey = this.generateRequestKey({ url: config.url, method: config.method, params: config.params, data: config.data } as InternalAxiosRequestConfig)
// Find the corresponding request and cancel it if (this.abortControllerMap.has(requestKey)) { this.abortControllerMap.get(requestKey)?.controller.abort() this.abortControllerMap.delete(requestKey) } }
/** * Cancel all ongoing requests */ public cancelAllRequests(): void { this.abortControllerMap.forEach(value => value.controller.abort()) this.abortControllerMap.clear() }}
// Export singleton instanceexport const axiosService = new AxiosService()</axiosresponse<t></t></axiosresponse<t></t></axiosresponse<t></t></string,></string,>
Notes:
-
In older versions of Axios, request cancellation used CancelToken; the new version uses AbortController, which is the method used in this article.
-
abortControllerMap stores each request’s AbortController and request time, used for canceling requests and cleaning up outdated requests.
-
startAutoCleanup starts a timer to automatically clean up requests older than 30 minutes (configurable) to prevent memory leaks.
-
stopAutoCleanup stops the timer. This can be used during route transitions in single-page applications or when the page is unmounted.
-
generateRequestKey concatenates method (this article only supports GET/POST), URL, request parameters, and request body to form a unique key for each request. The qs library is used here to serialize objects into query strings.
-
To send HTTP requests, the request method is sufficient; get and post are just encapsulations.
src/hooks/useAxios.ts
import { ref, type Ref } from 'vue'import axios from 'axios'import { axiosService, type CancelRequestConfig } from '@/utils/axiosService'
/** * Define the return type of the composable function * @template T Type of response data */interface UseAxiosReturn<t> { /** Response data (reactive reference) */ data: Ref<t null="" |=""> /** Error information (reactive reference) */ error: Ref<unknown> /** Loading status (reactive reference) */ isLoading: Ref<boolean> /** Method to execute requests */ execute: <r =="" t="">( method: 'GET' | 'POST', url: string, payload?: unknown, params?: Record<string, any=""> ) => Promise<r null="" |="">
get: <r =="" t="">( url: string, params?: Record<string, any=""> ) => Promise<r null="" |="">
post: <r =="" t="">( url: string, payload?: unknown, ) => Promise<r null="" |=""> /** Method to cancel requests */ cancel: (config?: CancelRequestConfig) => void}
/** * Encapsulate Axios in a composable function to uniformly handle request states, errors, and cancellation logic * @template T Default response data type */export function useAxios<t =="" any="">(): UseAxiosReturn<t> { // Reactive state const data = ref<t null="" |="">(null) // Store returned data from requests const error = ref<unknown>(null) // Store request error information const isLoading = ref(false) // Indicate whether a request is ongoing
/** Reset state to ensure a clean state before each request */ const resetState = () => { data.value = null error.value = null isLoading.value = true }
/** * Core method for executing requests * @param method HTTP method (GET/POST) * @param url Request address * @param payload Payload data for POST requests * @param params Query parameters for GET requests * @returns Promise containing request result or null (in case of error) */ const execute = async <r =="" t="">( method: 'GET' | 'POST', url: string, payload?: unknown, params?: Record<string, any=""> ): Promise<r null="" |=""> => { resetState() // Reset state, start new request try { const response = await axiosService.request<r>(method, url, payload, params) // Process response data data.value = response.data as T extends R ? T : R // Type adaptation return response.data } catch (err) { // Filter out errors from canceled requests (not considered exceptions) if (!axios.isCancel(err)) { error.value = err } data.value = null return null } finally { isLoading.value = false // End loading state regardless of success or failure } }
const get = async <r =="" t="">( url: string, params?: Record<string, any=""> ): Promise<r null="" |=""> => { resetState() // Reset state, start new request try { const response = await axiosService.get(url, params) // Process response data data.value = response.data as T extends R ? T : R // Type adaptation return response.data } catch (err) { // Filter out errors from canceled requests (not considered exceptions) if (!axios.isCancel(err)) { error.value = err } data.value = null return null } finally { isLoading.value = false // End loading state regardless of success or failure } }
const post = async <r =="" t="">( url: string, payload?: unknown, ): Promise<r null="" |=""> => { resetState() // Reset state, start new request try { const response = await axiosService.post(url, payload) // Process response data data.value = response.data as T extends R ? T : R // Type adaptation return response.data } catch (err) { // Filter out errors from canceled requests (not considered exceptions) if (!axios.isCancel(err)) { error.value = err } data.value = null return null } finally { isLoading.value = false // End loading state regardless of success or failure } }
/** * Method to cancel requests * @param config Cancellation configuration (optional, if not provided, all requests will be canceled) */ const cancel = (config?: CancelRequestConfig) => { if (config) { // Precisely cancel a specific request axiosService.cancelRequest(config) } else { // Cancel all ongoing requests axiosService.cancelAllRequests() } }
return { data: data as Ref<t null="" |="">, // Type assertion to ensure type safety error, isLoading, execute, get, post, cancel }}</t></r></r></r></string,></r></r></r></string,></r></unknown></t></t></t></r></r></r></string,></r></r></string,></r></boolean></unknown></t></t>
Notes:
-
Theoretically, with axiosService, you can directly use it in business code, and its core functionality is
await axiosService.get(url, params)However, as we can see, there are many things to consider around this line of code, making it quite redundant if placed in business code, hence it is encapsulated again.
-
To send HTTP requests, the execute method is sufficient; get and post are just encapsulations.
Usage Example:
<template> <div id="app"> <div v-if="isLoading" class="overlay">Loading...</div> <button :disabled="isLoading" @click="getUser">Get Single User</button> <button :disabled="isLoading" @click="listUser">Get All Users</button> <button :disabled="isLoading" @click="addUser">Add Single User</button> <button @click="cancelRequest">Cancel Get Single User Request</button> <button @click="cancelPostRequest">Cancel Add Single User Request</button> <div v-if="!isLoading && data">{{ data }}</div> <div v-if="!isLoading && error" class="error">Error: {{ error }}</div> </div></template>
<script lang="ts" setup name="App">import { type ResultDto } from './params';import { useAxios } from './hooks/useAxios';
interface User { userId: string, name: string, age: number}
const { data, error, isLoading, execute, get, post, cancel} = useAxios<ResultDto<User>>()
const getUser = async () => { await get('/getUserById', { userId: '1' })}
const listUser = async () => { await execute('GET', '/listUser')}
const addUser = async () => { const user: User = { userId: '4', name: 'Driver D', age: 44 } await execute('POST', '/addUser', user, { timestamp: 1111111 })}const cancelRequest = () => { cancel({ url: '/getUserById', method: 'GET', data: { userId: '1' } }) //cancel()}
const cancelPostRequest = () => { const user: User = { userId: '4', name: 'Driver D', age: 44 } cancel({ url: '/addUser', method: 'POST', data: user, params: { timestamp: 1111111 } }) //cancel()}</script>
<style scoped>/* Global styles */html,body { margin: 0; padding: 0; width: 100%; height: 100%;}
#app { width: 100%; height: 100%; display: flex; flex-direction: column; /* Vertical layout */}
.overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.5); display: flex; align-items: center; justify-content: center; font-size: 1.5em; color: white; z-index: 1000; /* Ensure overlay is above other content */}
.error { color: red;}</style>
This writing style still has room for improvement. For example, an API call might occur on different business pages, and if one day this interface changes, such as the URL address or parameters, updating all those pages would be a disaster. Therefore, in practice, a third encapsulation has emerged.
There are two solutions.
API
src/api/user.ts:
import { axiosService, type CancelRequestConfig } from '@/utils/axiosService'// Query user listexport function listUser(query) { return axiosService.get({ url: '/listUser', params: query })}
This is just an example; in my project, I actually use the next method.
Custom Hook Function
src/hooks/useUserAxion.ts:
import { type Ref } from 'vue'import { useAxios } from './useAxios'import { type ResultDto, type User, type Pagination, type UserPageQueryParam } from '@/params'
export function useUserAxios() { const { data: userData, error: userError, isLoading: userIsLoading, get: userGet, post: userPost, cancel: userCancel } = useAxios<ResultDto<User>>() const { data: userListData, error: userListError, isLoading: userListIsLoading, get: userListGet, post: userListPost, cancel: userListCancel } = useAxios<ResultDto<Pagination<User[]>>>()
const getUserById = async (id: string) => { await userGet('/user/getUserById', { userId: id }) }
const deleteUserById = async (id: string) => { await userGet('/user/delUser', { userId: id }) }
const deleteUsers= async (ids: string) => { await userGet('/user/delUsers', { userIds: ids }) }
const insertUser = async (user: User) => { await userPost('/user/addUser', user) }
const updUser = async (user: User) => { await userPost('/user/updUser', user) }
const cancelGetUserById = (id: string) => { userCancel({ url: '/user/getUserById', method: 'GET', params: { userId: id } }) }
const cancelInsertUser = (user: User) => { userCancel({ url: '/user/addUser', method: 'POST', data: user }) }
const pageUser = async (page: number, pageSize: number, phone?: string, gender?: string) => { const param: UserPageQueryParam = { page: page, pageSize: pageSize, phone: phone, gender: gender } await userListPost('/user/page', param) }
return { userDetail: userData, userList: userListData, userError, userListError, userIsLoading, userListIsLoading, getUserById, deleteUserById, deleteUsers, insertUser, updUser, pageUser, cancelGetUserById, cancelInsertUser }}
You can use it just like the previous hook function. Refer to Vue3 Development Minimalist Introduction (9): Logic Reuse with Composable Functions.
I have divided the requests involving User into two parts for export: one for individual User and one for paginated User (if needed, a list User can also be exported), along with corresponding methods, data, error information, and loading status.
Compared to the previous API-style encapsulation, this writing is a bit more cumbersome, but the business code can be flexibly controlled.
There is no absolute right or wrong between these two solutions; everyone should choose what suits their actual work.