基于 axios 实现的 Vue 组合式函数,使请求状态管理变得更简单
Axios + Vue = Axues ✌️
English | 简体中文
- 🦾 支持 axios 的所有特性
- ✨ 同时支持 Vue 3 和 Vue 2.7
- 🎭 全局的请求配置、响应处理及错误处理
- 🎃 响应数据可缓存,请求可重试、可取消
- 🕰️ 易于集成全局交互组件,例如 loading、confirm 弹窗,或者 toast 等
- 🏍️ 内置防抖
npm i axues
# 或者
pnpm add axues
# 或者
yarn add axues
注意: 必须安装 vue >= v3 或 >= 2.7, 且 axios >= 1.0
首先,创建 axues 实例,并把他当成一个插件注册到 app 中,就像使用vue-router and pinia 一样。
// main.js
import { createApp } from 'vue'
import axios from 'axios'
import { createAxues } from 'axues'
import App from './App.vue'
const app = createApp(App)
const axues = createAxues(axios)
app.use(axues)
app.mount('#app')
// main.js
import Vue from 'vue'
import axios from 'axios'
import { createAxues } from 'axues'
import App from './App.vue'
const axues = createAxues(axios)
Vue.use(axues.vue2Plugin)
new Vue({
render: h => h(App)
}).$mount('#app')
然后我们就可以在任意组件使用它:
<script setup>
import { useAxues } from 'axues'
const { loading, success, error, data } = useAxues('/api/foo', { immediate: true })
</script>
<template>
<div>
<p v-if="loading">loading...</p>
<div v-if="success">{{ data }}</div>
<p v-if="error">Something went error: {{ error.message }}</p>
</div>
</template>
单看
useAxues
, 它看起来很像 vueuse 或 nuxt 的useFetch
, 那 为什么 axues 需要创建并且注册成组件?
这个例子就是最简单的使用示例,将 url
传给第一个参数,options
传给第二个参数,
useAxues
将返回一些非常好用的状态和方法,我们可以将这些状态、方法直接绑定到模板中。
options
可以传给第一个参数:
const { loading, success, error, data } = useAxues({
url: '/api/foo',
method: 'post',
data: { foo: 'bar' },
immediate: true
})
// 等同于
const { loading, success, error, data } = useAxues('/api/foo', {
method: 'post',
data: { foo: 'bar' },
immediate: true
})
当然,我们也可以直接使用无渲染组件:
<template>
<axues url="/api/foo" :immediate="true" v-slot="{ loading, success, data, error }">
<div>
<p v-if="loading">loading...</p>
<div v-if="success">{{ data }}</div>
<p v-if="error">Something went error: {{ error.message }}</p>
</div>
</axues>
</template>
有些场景下我们需要将请求封装起来以便复用,所以 useAxues
也支持传入任意的返回 promise 的函数。
import { useAxues, axues } from 'axues'
const fetchUsers = () => fetch('/api/users')
const { loading, error, data } = useAxues(fetchUsers, { immediate: true })
const anyPromiseFn = () => Promise.resolve('foo')
const { loading: loading2 } = useAxues(anyPromiseFn, { immediate: true })
const fetchBooks = () => axues.get('/api/books') // same as axios api, just change a name
const { loading: loading3, data: bookData } = useAxues({
promise: fetchBooks,
immediate: true
})
以上的例子都传入 immediate
这个配置项,意味着一载入即发起请求。如果想要手动触发请求,我们需要调用 useAxues
返回的 action
方法。
<script setup>
import { useAxues } from 'axues'
const { loading, success, error, data, action } = useAxues('/api/foo')
</script>
<template>
<div>
<p v-if="loading">loading...</p>
<div v-if="success">{{ data }}</div>
<p v-if="error">Something went error: {{ error.message }}</p>
<button @click="action">execute</button>
</div>
</template>
如果 action
可能会被多次调用,那么 action
支持传参是很有必要的:
<script setup>
import { ref } from 'vue'
import { useAxues } from 'axues'
const list = ref([1, 2, 3])
const { loading, action } = useAxues({
url: '/api/foo',
method: 'post',
data: actionPayload => ({ idx: actionPayload })
})
</script>
<template>
<div v-for="li in list">
<input v-model="li" />
<button @click="action(li)" :disabled="loading">save</button>
</div>
</template>
每次从请求开始到渲染完成的过程,就像是一场戏,浏览器是剧场,JS 代码是剧本,HTML 是演员,css 是道具和装扮,只要导演发出 action 指令,演员们就会根据剧本完成演绎,那谁是那个导演呢?当然是我们的用户啦。
默认情况下,如果 action
调用频率过高,axues 只会执行第一次请求,在第一次请求完成前,调用 action
都是无效的。
<script setup>
import { useAxues } from 'axues'
// 只会发起一次请求
const { action } = useAxues('/api/foo')
</script>
<template>
<div>
<button @click="action">double click me</button>
</div>
</template>
如果是做搜索建议,我们需要反过来,只让最后一次调用去触发请求:
<script setup>
import { useAxues } from 'axues'
const keyword = ref('')
const { data, action } = useAxues({
url: '/api/foo',
params: () => ({ keyword: keyword.value }),
debounce: true,
debounceTime: 600 // default: 500 (ms)
})
</script>
<template>
<div>
<input v-model="keyword" @input="action" />
<div>
<p v-for="k in data" @click="() => (keyword = k)">{{ k }}</p>
</div>
</div>
</template>
在弱网环境下,请求错误后可重试对用户来说非常重要。
<script setup>
import { useAxues } from 'axues'
const { error, action, retryTimes, retryCountdown, retry, retrying } = useAxues({
url: '/api/foo',
autoRetryTimes: 2, // 自动重试次数
autoRetryInterval: 3 // 自动重试间隔,默认: 2 (s)
})
</script>
<template>
<div>
<div v-if="error">
<p>请求出错: {{ error.message }}</p>
<p>
重试次数: {{ retryTimes }}
<button @click="retry">立即重试</button>
</p>
</div>
<p v-if="retryCountdown > 0">
{{ `将在 ${retryCountdown} 秒后重试` }}
</p>
<p v-if="retrying">正在重试...</p>
</div>
</template>
刷新也是基操。
<script setup>
import { useAxues } from 'axues'
const { loading, success, error, data, action, refresh, refreshing } = useAxues('/api/foo')
</script>
<template>
<div>
<p v-if="loading">正在加载...</p>
<p v-if="refreshing">正在刷新...</p>
<div v-if="success">{{ data }}</div>
<p v-if="error">请求出错: {{ error.message }}</p>
<button @click="action">执行</button>
<button @click="refresh">刷新</button>
</div>
</template>
提供反悔撤消操作也是良好用户体验的基础。
<script setup>
import { useAxues } from 'axues'
const { loading, action, canAbort, abort, aborted } = useAxues('/api/foo')
</script>
<template>
<div>
<p v-if="loading">加载中...</p>
<p v-if="aborted">已取消</p>
<button @click="action">执行</button>
<button @click="abort" v-if="canAbort">取消请求</button>
</div>
</template>
我们还可以为传入的 promise 方法提供取消操作。
import { useAxues, axues } from 'axues'
const fetchUsers = (actionPayload, signal) => fetch('/api/users', { signal })
const { loading, canAbort, abort } = useAxues(fetchUsers, { immediate: true })
const fetchBooks = (actionPayload, signal) => axues.get('/api/books', { signal })
const { loading: loading2, abort: abort2 } = useAxues({ promise: fetchBooks, immediate: true })
分页查询是 web 开发中非常常见的场景,使用 axues 做分页也很简单。
<script setup>
import { reactive } from 'vue'
import { useAxues } from 'axues'
const pagination = reactive({
current: 1,
pageSize: 20,
total: 0
})
const { loading, action, data } = useAxues({
url: '/api/pagination',
params: p => ({ p: pagination.current + ~~p, s: pagination.pageSize }),
immediate: true,
onSuccess(data) {
pagination.current = data.current
pagination.total = data.total
}
})
</script>
<template>
<div>
<p v-if="loading">加载中...</p>
<p>current page: {{ pagination.current }}</p>
<p>{{ data }}</p>
<button v-if="pagination.current > 1" @click="action(-1)">上一页</button>
<button @click="action(1)">下一页</button>
</div>
</template>
如果是要做追加分页(无限滚动分页):
<script setup>
// ...
const { loading, action, data } = useAxues({
// ...
initialData: [],
onData: (data, newData) => data.value.push(...newData)
// ...
})
</script>
为了与 UI 与交互的一致性,我们通常会编写通用的交互组件。 这些组件在请求期间被调用,根据请求成功或失败通知用户请求状态。 比如一个删除数据的场景,如果按照传统的方式,我们会这样写:
<script setup>
import { Loading, Confirm, Toast } from 'some-UI-lib'
import axios from 'axios'
function deleteItem(id) {
Confirm('确定要删除这条数据吗?').then(
// 让用户确认以防误操作
() => {
Loading.open() // 调用loading动画组件
axios
.delete(`/api/delete/${id}`)
.then(
res => {
Toast('已删除') // 提示用户当前状态
},
err => {
Toast.error(`删除id为: [${id}] 时出错: ${err}`) // 告知用户出错
}
)
.finally(Loading.close) // 关闭loading动画
},
() => {}
)
}
</script>
<template>
<div>
<button @click="deleteItem(1)"></button>
</div>
</template>
过程式的调用代码看起来就像意大利面,现在有了 axues,你可以使用声明式极大的简化代码。
<script setup>
import { useAxues } from 'axues'
const { action } = useAxues({
url: id => `/api/delete/${id}`,
method: 'delete',
confirmOverlay: '确定要删除这条数据吗?',
loadingOverlay: true,
successOverlay: '已删除',
errorOverlay: (id, err) => `删除id为: [${id}] 时出错: ${err}`
})
</script>
<template>
<div>
<button @click="action(1)"></button>
</div>
</template>
当然,这一切的前提是你必须在根组件中注册这些交互组件。 在根组件中注册一次总是比每次调用都注册要好,对吧?
<!-- App.vue -->
<script setup>
import { Loading, Confirm, Toast, Modal } from 'some-UI-lib'
import { useOverlayImplement } from 'axues'
useOverlayImplement({
loadingOpen(options) {
Loading.open({
text: options.text
})
},
loadingClose: Loading.close,
confirm(options) {
return Confirm({
title: options.title,
content: options.content
})
},
success(options) {
if (options.style === 1) {
// 可以使用多种样式
Toast(options.title)
} else {
Modal({
title: options.title,
content: options.content
})
}
},
error(options) {
if (options.style === 1) {
Toast.error(options.title)
} else {
Modal({
title: options.title,
content: options.content
})
}
}
})
</script>
<template>
<router-view></router-view>
</template>
在实际应用中,我们通常需要在一个统一的地方处理请求和响应, 比如在每个请求头中携带 Authorization,转换响应数据,或处理错误并上报错误。
// main.js
// ...
const axues = createAxues(axios, {
requestConfig: () => ({
baseURL: 'https://axues.io',
timeout: 30000,
headers: { Authorization: localStorage.getItem('Authorization') }
}),
responseHandle(response) {
if (response.data.myBussinessCode === 401) {
router.push('/login')
return new Error('Unauthorized')
}
return response.data
},
errorHandle(err) {
return new Error(`[${err.response.status}]${err.config.url}: ${err.message}`)
}
})
app.use(axues)
你可能认为对每个请求都执行 requestConfig 方法会损耗一点性能,直接使用 axios.create
来创建 axios 实例可能是一个更好的主意.
// main.js
// ...
const axiosInstance = axios.create({
baseURL: 'https://axues.io',
timeout: 30000
})
const axues = createAxues(axiosInstance, {
requestConfig: () => ({
// Authorization 可能会变化,这类可能变化的数据就不应该放到 axios.create 中
headers: { Authorization: localStorage.getItem('Authorization') }
})
// ...
})
app.use(axues)
如你所见,createAxues
会返回一个 axues
的实例,这样我们在创建应用的时候就可以共享全局的请求配置,比如在 router
中调用请求:
// main.js
const axues = createAxues(axios, {
requestConfig: () => ({
headers: { Authorization: localStorage.getItem('Authorization') }
})
})
const router = createRouter({
// ...
})
router.beforeEach((to, from, next) => {
if (to.meta.sendToAnalytics) {
axues.post('/api/sendToAnalytics', { path: to.fullPath })
}
next()
})
app.use(axues).use(router)
这就是要先创建 axues
实例的原因.
Click to show
type MaybeComputedRef<T> = MaybeRef<T> | (() => T) | ComputedRef<T>
interface CreateAxuesOptions {
requestConfig?: MaybeComputedRef<AxiosRequestConfig>
transformUseOptions?: (options: UseAxuesOptions) => UseAxuesOptions
responseHandle?: (response: AxiosResponse, requestConfig: AxuesRequestConfig) => unknown
errorHandle?: (err: AxiosError, requestConfig: AxuesRequestConfig) => Error
cacheInstance?: {
get: (key: string) => unknown
set: (key: string, value: string) => void
delete: (key: string) => void
}
errorReport?: (err: Error) => void
rewriteDefault?: {
immediate?: boolean
shallow?: boolean
loadingDelay?: number
debounce?: boolean
debounceTime?: number
autoRetryTimes?: number
autoRetryInterval?: number
throwOnActionFailed?: boolean
}
overlayImplement?: {
loadingOpen?: (options: LoadingOverlayType) => void
loadingClose?: () => void
success?: (options: SuccessOrErrorOverlayType) => void
error?: (options: SuccessOrErrorOverlayType) => void
confirm?: (options: ConfirmOverlayType) => Promise<unknown>
}
}
interface CreateReturn extends Axues {
install: (app: App) => void
vue2Plugin: Plugin
}
declare function createAxues(axiosInstance: AxiosInstance, createOptions?: CreateAxuesOptions): CreateReturn
type MaybeRef<T> = T | Ref<T>
type MaybeComputedRefWithoutFn<T> = ComputedRef<T> | MaybeRef<T>
type MaybeComputedOrActionRef<T, TAction = any> = MaybeComputedRefWithoutFn<T> | ((actionPayload?: TAction) => T)
interface AxuesRequestConfig<TI = any, TAction = any> extends Omit<AxiosRequestConfig, 'url' | 'headers'> {
url?: MaybeComputedOrActionRef<string, TAction>
params?: MaybeComputedOrActionRef<TI, TAction>
data?: MaybeComputedOrActionRef<TI, TAction>
contentType?: MaybeComputedOrActionRef<ContentType, TAction>
headers?: MaybeComputedOrActionRef<RawAxiosRequestHeaders, TAction>
responseHandlingStrategy?: any
errorHandlingStrategy?: any
}
interface Axues {
<TI = any, TO = any>(config: AxuesRequestConfig<TI>): Promise<TO>
request: <TI = any, TO = any>(config: AxuesRequestConfig<TI>) => Promise<TO>
get: <TI = any, TO = any>(url: string, config?: AxuesRequestConfig<TI>) => Promise<TO>
delete: <TI = any, TO = any>(url: string, config?: AxuesRequestConfig<TI>) => Promise<TO>
head: <TI = any, TO = any>(url: string, config?: AxuesRequestConfig<TI>) => Promise<TO>
options: <TI = any, TO = any>(url: string, config?: AxuesRequestConfig<TI>) => Promise<TO>
post: <TI = any, TO = any>(url: string, data?: TI, config?: AxuesRequestConfig<TI>) => Promise<TO>
put: <TI = any, TO = any>(url: string, data?: TI, config?: AxuesRequestConfig<TI>) => Promise<TO>
patch: <TI = any, TO = any>(url: string, data?: TI, config?: AxuesRequestConfig<TI>) => Promise<TO>
postForm: <TI = any, TO = any>(url: string, data?: TI, config?: AxuesRequestConfig<TI>) => Promise<TO>
putForm: <TI = any, TO = any>(url: string, data?: TI, config?: AxuesRequestConfig<TI>) => Promise<TO>
patchForm: <TI = any, TO = any>(url: string, data?: TI, config?: AxuesRequestConfig<TI>) => Promise<TO>
}
declare let axues: Axues
type CanWatch = 'url' | 'params' | 'data' | 'headers'
interface UseAxuesOptions<TI = any, TO = any, TAction = any> extends AxuesRequestConfig<TI, TAction> {
promise?: (actionPayload?: TAction, signal?: AbortSignal) => Promise<TO>
immediate?: boolean
watch?: CanWatch | CanWatch[]
initialData?: TO
shallow?: boolean
debounce?: boolean
debounceTime?: number
autoRetryTimes?: number
autoRetryInterval?: number
cacheKey?: MaybeComputedOrActionRef<string, TAction>
throwOnActionFailed?: boolean
confirmOverlay?: ConfirmOverlayOptions<TAction>
loadingOverlay?: LoadingOverlayOptions<TAction>
successOverlay?: SuccessOverlayOptions<TAction, TO>
errorOverlay?: ErrorOverlayOptions<TAction>
onAction?: (act: 'action' | 'resetAction' | 'retry' | 'refresh' | 'abort' | 'deleteCache') => void
onData?: (data: Ref<TO>, newData: unknown | unknown[], actionPayload?: TAction) => void
onSuccess?: (data: TO, actionPayload?: TAction) => void
onError?: (err: Error, actionPayload?: TAction) => void
onFinally?: (actionPayload?: TAction) => void
}
type UseAxuesFirstArg<TI, TO, TAction> = MaybeComputedRefWithoutFn<string> | ((actionPayload?: TAction, signal?: AbortSignal) => Promise<TO>) | UseAxuesOptions<TI, TO, TAction>
interface UseAxuesOutput<TI, TO, TAction = any> {
pending: Ref<boolean>
loading: Ref<boolean>
success: Ref<boolean>
error: Ref<Error | null>
refreshing: Ref<boolean>
refreshed: Ref<boolean>
retrying: Ref<boolean>
retryTimes: Ref<number>
retryCountdown: Ref<number>
requestTimes: Ref<number>
canAbort: ComputedRef<boolean>
aborted: Ref<boolean>
data: Ref<TO>
action: (actionPayload?: TAction) => PromiseLike<TO>
resetAction: (actionPayload?: TAction) => PromiseLike<TO>
retry: () => PromiseLike<TO>
refresh: () => PromiseLike<TO>
abort: () => void
deleteCache: (actionPayload?: TAction) => void
get: (params?: MaybeComputedOrActionRef<any, TAction>, actionPayload?: TAction) => PromiseLike<TO>
head: (params?: MaybeComputedOrActionRef<any, TAction>, actionPayload?: TAction) => PromiseLike<TO>
options: (params?: MaybeComputedOrActionRef<any, TAction>, actionPayload?: TAction) => PromiseLike<TO>
delete: (params?: MaybeComputedOrActionRef<any, TAction>, actionPayload?: TAction) => PromiseLike<TO>
post: (data?: MaybeComputedOrActionRef<TI, TAction>, actionPayload?: TAction) => PromiseLike<TO>
put: (data?: MaybeComputedOrActionRef<TI, TAction>, actionPayload?: TAction) => PromiseLike<TO>
patch: (data?: MaybeComputedOrActionRef<TI, TAction>, actionPayload?: TAction) => PromiseLike<TO>
}
declare function useAxues<TI = any, TO = any, TAction = any>(urlOrPromiseOrOptions: UseAxuesFirstArg<TI, TO, TAction>, options?: UseAxuesOptions<TI, TO, TAction>): UseAxuesOutput<TI, TO, TAction>