在setup中优雅调用API:组合式函数封装异步请求实战
作者:技术拾光时间:2026-05-07 12:16:02浏览:522
在 Vue 3 项目中,setup 函数里调用 API,真正的挑战往往不在于发起请求本身,而在于如何将这套逻辑组织得清晰、可复用,并且不污染组件的核心业务逻辑。直接在 setup 里写 axios.get() 虽然快捷,但很快就会让你陷入重复处理 loading 状态、错误捕获、缓存和请求取消的泥潭

在 Vue 3 项目中,setup 函数里调用 API,真正的挑战往往不在于发起请求本身,而在于如何将这套逻辑组织得清晰、可复用,并且不污染组件的核心业务逻辑。直接在 setup 里写 axios.get() 虽然快捷,但很快就会让你陷入重复处理 loading 状态、错误捕获、缓存和请求取消的泥潭。要跳出这个循环,组合式函数(Composable)的合理封装,才是通往优雅代码的必经之路。
明确职责:组合式函数只管“请求逻辑”,不管“UI 状态”
一个设计精良的 API 组合式函数,其职责边界应该非常清晰:它只关心如何发起请求、如何处理响应结构,并暴露一个可控的执行接口。它不应该越界去直接修改组件内的 ref,也不该主动触发 UI 更新。
那么,一个理想的封装应该长什么样?
- 返回值推荐解构对象:
{ data, error, loading, execute }。这套结构语义清晰,调用方可以按需取用,非常直观。 execute是函数,而非自动执行:这意味着控制权交给了组件。数据是在按钮点击后拉取,还是在路由就绪后加载,完全由调用方决定。- 状态用
ref包裹:错误和加载状态被封装为响应式引用,这样在模板中就能直接通过v-if="loading"进行绑定,实现 UI 的自动响应。
基础封装:带 loading 和 error 处理的通用模式
理论说再多,不如看代码。下面是一个生产环境可用的、最小化的封装示例(以 axios 为例,并支持 TypeScript):
import { ref, Ref } from 'vue'
import axios from 'axios'
interface UseApiOptions {
manual: boolean // 是否手动触发,默认 false(自动执行)
}
export function useApi(url: string, options: UseApiOptions = {}) {
const data = ref(null)
const error = ref(null)
const loading = ref(false)
const execute = async () => {
loading.value = true
error.value = null
try {
const res = await axios.get(url)
data.value = res.data
return res.data
} catch (e) {
error.value = e as Error
throw e
} finally {
loading.value = false
}
}
if (!options.manual) {
execute()
}
return {
data,
error,
loading,
execute
}
}
在组件中使用时,代码会变得异常干净:
import { useApi } from '@/composables/useApi'
export default defineComponent({
setup() {
const { data, loading, error, execute } = useApi('/api/user/1')
const handleRefresh = () => execute()
return () => (
{loading.value '加载中...' : null}
{error.value `错误:${error.value.message}` : null}
{data.value {data.value.name}
: null}
)
}
})
进阶技巧:支持参数、取消请求、缓存与防抖
当然,真实业务场景远比基础示例复杂。以下几个进阶能力,你很快就会用到。关键在于,这些逻辑都应该沉淀在组合式函数内部,而不是散落在各个组件里。
- 动态参数:将固定的
url改为一个函数,例如(id: string) => `/api/user/${id}`。让execute方法接收参数,并在每次执行前重置相关状态。 - 请求取消:利用
AxiosController或原生的AbortController。在每次execute调用时生成一个新的 controller,并在下一次调用前取消上一次的请求。这对于搜索输入框等场景至关重要。 - 简单内存缓存:使用一个 Map 对象,以
url + JSON.stringify(params)作为键来缓存响应结果。命中缓存时直接返回,能有效避免不必要的重复请求。 - 节流/防抖:对于高频触发的
execute调用(比如实时搜索),可以使用lodash.debounce等工具对执行函数进行包装,返回一个新的防抖函数。
避坑提醒:别让组合式函数变成“黑盒”
封装的目的在于简化使用,而非隐藏必要的复杂性。有几个常见的反模式需要警惕:
- 避免制造“万能函数”:把所有 API 请求都塞进一个叫
useRequest的巨型函数里。这会导致参数列表不断膨胀,类型推导困难,最终让维护成本飙升。 - 保持关注点分离:不要在组合式函数内部直接调用
notification.success()或router.push()。这违反了单一职责原则,会让组件失去对副作用和流程的控制权。 - 暴露必要的原始响应:不要只返回
data,而忽略了res.headers、res.status等信息。在处理分页、鉴权等场景时,这些信息往往是必需的。 - 重视类型安全:不要用
any或unknown敷衍了事。充分利用 TypeScript 的类型推导,这是组合式函数封装带来的核心优势之一。
说到底,一个好的组合式函数应该像一块乐高积木:功能纯粹且独立,但又能和其他积木完美拼接,共同构建出复杂的应用。它不替你做所有决定,只是为你提供一个稳固、可靠的“把手”。
