Skip to content

Nuxt createUseFetch:一个类型安全的 API 客户端,告别封装样板代码 ​

译注:本文编译自 dev.to 文章《Nuxt createUseFetch: One Typed API Client Without the Wrapper Boilerplate》,原文链接见文末。内容基于 Nuxt 4.4 的新特性,介绍如何用官方工厂函数替代手写的 useFetch 封装。

每个与真实后端通信的 Nuxt 应用最终都会遇到同一个问题:在一个页面里调用 useFetch('/users'),在另一个页面里调用 useFetch('/orders'),很快每个调用点都在重复同样的 baseURL、同样的 Authorization 头,以及同样的「401 就跳转登录」逻辑。于是你写了一个封装。然后你发现封装丢掉了 useFetch 的部分类型信息,或者泛型变得很难看,又或者你不确定它是否还能和 SSR 去重机制正常配合。

Nuxt 4.4 给出了官方答案:createUseFetch 和 createUseAsyncData。它们是工厂函数,返回一个签名与 useFetch(或 useAsyncData)完全一致的可组合函数,只是把你的默认配置预先烘焙进去。不需要手写泛型,也不用猜测 key 该怎么传。

基础工厂 ​

工厂函数和其他可组合函数一样放在 composables/ 目录下。下面是一个面向外部后端的 API 客户端,改编自 Nuxt 文档中的示例:

typescript
// app/composables/useAPI.ts
export const useAPI = createUseFetch({
  baseURL: "https://api.example.com",
  onRequest({ options }) {
    const { session } = useUserSession();
    if (session.value?.token) {
      options.headers.set("Authorization", `Bearer ${session.value.token}`);
    }
  },
  async onResponseError({ response }) {
    if (response.status === 401) {
      await navigateTo("/login");
    }
  },
});

现在页面只需请求数据:

vue
<script setup lang="ts">
const { data: profile } = await useAPI("/me");
const { data: orders, status } = await useAPI("/orders", { lazy: true });
</script>

useAPI 返回的仍然是你熟悉的 data、status、error 和 refresh,类型也完全一致。useFetch 支持的每一个选项(query、transform、pick、server、getCachedData 等等)在调用点依然可用。

有一条规则需要记住:createUseFetch 是一个编译器宏。它必须是 composables/(或 Nuxt 扫描的其他目录)中的导出声明。Nuxt 正是借此在构建时找到它,并注入去重 key,让 SSR 水合时复用服务端 payload 而不是重复请求。如果你把它定义在组件内部或某个随意的工具文件里,就会失去这一能力。

默认值与覆盖 ​

工厂有两种模式,选对模式很重要。

传入普通对象时,你的选项是默认值,发生冲突时调用方优先:

typescript
export const useAPI = createUseFetch({
  baseURL: "https://api.example.com",
  lazy: true,
});

// 调用方为这一次调用覆盖 baseURL
const { data } = await useAPI("/status", {
  baseURL: "https://status.example.com",
});

传入函数时,你的选项会覆盖调用方的选项。函数会接收到调用方传入的内容,因此合并方式由你决定:

typescript
// base URL 被强制固定,调用方无法把该客户端指向别处
export const useBillingAPI = createUseFetch((callerOptions) => ({
  ...callerOptions,
  baseURL: useRuntimeConfig().public.billingApiUrl,
}));

当默认值需要从 Nuxt 上下文中获取东西(比如 useRuntimeConfig() 或 useNuxtApp())时,也应该使用函数形式。函数在调用点、也就是 setup 内部执行,此时 Nuxt 实例存在;而普通对象在模块作用域只求值一次,那里没有 Nuxt 实例。

一个实用的理解方式:对象模式用于便利(「大多数调用都想要这个」),函数模式用于策略(「每次调用都必须有这个」)。

接入自定义 $fetch ​

如果你已经从插件中获得了一个配置好的 ofetch 实例(也许带有重试逻辑或日志),可以把它交给工厂:

typescript
// app/plugins/api.ts
export default defineNuxtPlugin(() => {
  const api = $fetch.create({
    baseURL: "https://api.example.com",
    retry: 2,
    retryStatusCodes: [502, 503, 504],
  });
  return { provide: { api } };
});
typescript
// app/composables/useAPI.ts
export const useAPI = createUseFetch((callerOptions) => ({
  $fetch: useNuxtApp().$api as typeof $fetch,
  ...callerOptions,
}));

注意这里再次使用了函数形式:useNuxtApp() 必须在 setup 中运行,因此不能放在文件顶部的普通对象里。

createUseAsyncData 遵循同样的模式,适用于「fetch」根本不是 HTTP 调用的场景,比如 GraphQL 客户端、SDK 或 Supabase 查询,但你仍然想要共享 lazy、deep 或 getCachedData 这类默认值。

什么时候不必多此一举 ​

如果你的应用只访问同源的 /api 路由且不需要任何请求头,那么直接用 useFetch 就好;工厂只会多出一个文件和一个名字,没有任何收益。另外要记住,useFetch 这类可组合函数是用于在渲染期间加载数据的。对于表单提交或按钮点击,应直接调用 $fetch(或你插件提供的 $api)。把事件处理器包进 useAPI 不会带来 SSR 收益,反而可能导致令人困惑的缓存行为。

小结 ​

createUseFetch 把几乎每个 Nuxt 团队都重新发明过的模式,变成了一行代码,同时完整保留类型和 SSR key 处理。如果你使用的是 Nuxt 4.4 或更高版本,不妨找出已有的 useFetch 封装,把它们作为工厂迁移到 composables/ 中,并有意识地选择对象模式或函数模式。其余选项可参考 createUseFetch 文档 和 自定义 useFetch 示例。

原文链接 ​

https://dev.to/grimicorn/nuxt-createusefetch-one-typed-api-client-without-the-wrapper-boilerplate-4mj0