Zademy

Nuxt 4:$fetch、useFetch 和 useAsyncData - 完整数据获取指南

Nuxt; Vue; Fetching; SSR; DataFetching
2805 字

Nuxt 中的数据获取管理分为 三个层级,每个工具都有不同的架构角色:

工具架构角色主要目的SSR 状态传输(水合)
$fetch低级实用程序(原始 HTTP 客户端)执行纯 HTTP 网络请求。否。 如果在 SSR 组件中直接使用,会导致双重 获取
useAsyncData状态原语(核心原语)管理异步状态的生命周期并保证服务器和客户端之间的数据传输。是。 捕获结果,将其序列化到 Nuxt 的 payload 中,并避免双重请求。
useFetch便利包装器(开发者包装器)简化从单个 HTTP 端点 获取数据的最常见用例。是。 在内部与 useAsyncData 一起工作,继承其功能。

useFetch 本质上是实现 useAsyncData$fetch 的语法糖(快捷方式)。

何时使用每个工具

选择从根本上取决于操作的架构上下文:

1. useFetch:简单的初始数据检索(GET)

建议对通过 GET 请求从单个 端点 进行初始数据检索使用 useFetch,特别是当您需要服务器端渲染(SSR)支持并希望避免双重数据获取时。它是语法最简洁的选项。

清晰示例: 获取页面的文章列表。

// app/pages/posts.vue
<script setup lang="ts">
  // 负责在服务器上进行一次请求并传输状态。
  const { data: posts } = await useFetch('/api/posts')
</script>

2. useAsyncData:复杂的异步逻辑或非 HTTP 数据源

当异步操作不是来自单个 端点 的简单 HTTP 调用时,必须使用 useAsyncData。它是包装任何异步 promise 的主要工具。

清晰示例:

  • 并行调用: 协调多个同时的 $fetch 调用并组合它们。

    const { data } = await useAsyncData("dashboard", async () => {
    	const [user, stats] = await Promise.all([$fetch("/api/user"), $fetch("/api/stats")]);
    	return { user, stats };
    });
  • 使用第三方客户端: 集成不使用 $fetch 的客户端,如 GraphQL 客户端或数据库 ORM。

    // myGetFunction 是您的数据客户端中的自定义函数
    const { data } = await useAsyncData("users", () => myGetFunction("users"));

3. $fetch:突变和客户端/服务器驱动的操作

明确建议对从根本上是 客户端 的交互使用 $fetch,这些交互由用户事件(如点击或表单提交)驱动。它也是不需要 SSR 状态同步的低级实用程序调用的最佳选项。

清晰示例:

  • 突变(POST/PUT/DELETE): 发送表单数据。对突变使用 useFetch 通常效率低下,因为它不会从状态水合中受益。

    // 在事件处理程序内使用
    async function submitForm() {
    	await $fetch("/api/contact", {
    		method: "POST",
    		body: formData.value
    	});
    }
  • 内部服务器路由调用: 在 Nuxt 的 API 路由内(例如 server/api),$fetch 避免 HTTP 请求并直接调用处理程序函数,优化服务器端速度。

优势和劣势

工具优势劣势
$fetch内部调用优化: 调用内部服务器路由而不进行 HTTP 往返(延迟节省)。 适合突变: 直接和简单的客户端操作使用。双重 获取 风险: 如果在没有 useAsyncData 的组件中使用,客户端会在水合期间再次执行请求。
useAsyncData完全控制: 允许包装 任何 异步逻辑(不仅仅是 HTTP)。 SSR 友好: 保证数据仅在服务器上获取一次并传输到客户端(防止 双重获取)。 完整响应状态: 返回 datapendingerrorstatus(指示状态的字符串:"idle"、"pending"、"success"、"error")。 Nuxt 4 优化: data 值默认为 shallowRef(浅响应),减少大型数据结构的 CPU 开销,提高性能。更冗长: 需要显式定义处理程序和唯一键。
useFetch简单语法(DX): 最简单的 SSR 安全请求方式。 自动 机制: 根据 URL 和选项自动生成键。 强类型: 可以从服务器路由推断响应类型和 URL 类型。有限: 仅适用于对单个 HTTP 端点 的调用。不建议在 setup 阶段对突变(POST、PUT、DELETE)使用。

何时从客户端 vs 服务器端使用

这里的关​​键区别是 状态传输 的需要,用于通用渲染(SSR),这可以防止"双重数据获取"。

服务器端(SSR)和通用渲染

当使用 Nuxt 进行服务器端渲染并且您需要数据出现在初始 HTML 中(对 SEO 和性能至关重要)时,您必须使用通过 payload 处理状态传输的函数:

上下文推荐工具原因
初始数据检索(GET) 在页面/组件中。useFetchuseAsyncData确保数据仅在服务器上获取一次并在客户端水合,避免双重请求。
内部 API 路由调用(来自服务器代码)。$fetchNuxt 拦截调用并直接执行,不进行真正的 HTTP 请求,速度更快。

警告: 如果您仅对服务器端渲染组件(<script setup>)中的初始数据获取使用 $fetch,客户端将在水合期间再次执行网络请求,导致双重 获取

客户端

在水合后或响应用户交互执行的操作应使用低级实用程序:

上下文推荐工具原因
突变(POST、PUT、DELETE) 由用户事件触发。$fetch这是进行直接客户端 HTTP 调用的首选方式。状态不需要从服务器传输,因此 useAsyncData 开销是不必要的。
非 SEO 关键数据 或客户端依赖数据(例如地理位置)。useFetch/useAsyncDataserver: false这禁用服务器上的处理程序执行,推迟数据获取直到客户端水合完成。
刷新现有数据 或执行延迟函数。refresh()/execute()(由 useFetch/useAsyncData 返回)允许手动重新执行数据获取逻辑,无需使用 $fetch

useFetchuseAsyncData 视为从服务器到客户端旅行的安全箱:服务器用数据填充它并密封它。当它到达客户端时,客户端不必外出获取数据(避免双重请求),只需打开安全箱即可。另一方面,$fetch 是请求数据的简单电话;如果服务器打电话,客户端必须再次打电话以获得自己的副本。

高级实际示例

useAsyncData 的优化加载模式

// pages/dashboard.vue
<script setup lang="ts">
// 带有详细状态管理的优化加载
const {
  data: dashboard,
  pending,
  error,
  refresh
} = await useAsyncData('dashboard', async () => {
  // 并行调用以获得更好的性能
  const [user, stats, notifications] = await Promise.all([
    $fetch('/api/user/profile'),
    $fetch('/api/analytics/stats'),
    $fetch('/api/notifications/unread')
  ])

  return {
    user,
    stats,
    notifications
  }
}, {
  // 高级选项
  server: true,        // 在服务器上执行
  lazy: false,         // 等待渲染前
  default: () => ({    // 加载时的默认值
    user: null,
    stats: { views: 0, users: 0 },
    notifications: []
  })
})

// 带防抖的手动刷新
const debouncedRefresh = useDebounceFn(refresh, 300)

// 监视自动刷新
watch(() => route.path, debouncedRefresh)
</script>

带错误处理的 $fetch 突变

// components/ContactForm.vue
<script setup lang="ts">
const formData = ref({
  name: '',
  email: '',
  message: ''
})

const isSubmitting = ref(false)
const submitError = ref<string | null>(null)

async function submitForm() {
  isSubmitting.value = true
  submitError.value = null

  try {
    const response = await $fetch('/api/contact', {
      method: 'POST',
      body: formData.value,
      // 额外选项
      timeout: 10000,
      retry: 2,
      retryDelay: 1000
    })

    // 成功处理
    await navigateTo('/thank-you')
  } catch (error) {
    if (error.data?.message) {
      submitError.value = error.data.message
    } else {
      submitError.value = '提交表单时出错。请重试。'
    }
  } finally {
    isSubmitting.value = false
  }
}
</script>

带数据转换的 useFetch

// pages/products/[id].vue
<script setup lang="ts">
interface Product {
  id: number
  name: string
  price: number
  description: string
  category: string
}

interface ProductWithDetails extends Product {
  formattedPrice: string
  isInStock: boolean
  relatedProducts: Product[]
}

const { data: product, pending } = await useFetch<ProductWithDetails>(
  `/api/products/${route.params.id}`,
  {
    // 数据转换
    transform: (data: Product) => ({
      ...data,
      formattedPrice: new Intl.NumberFormat('en-US', {
        style: 'currency',
        currency: 'USD'
      }).format(data.price),
      isInStock: data.price > 0,
      relatedProducts: [] // 稍后填充
    }),
    // 高级缓存
    server: true,
    key: `product-${route.params.id}`,
    // 缓存选项
    getCachedData: (key) => {
      // 自定义缓存逻辑
      return nuxtApp.staticData[key] || nuxtApp.payload.data[key]
    }
  }
)

// 之后加载相关产品
watchEffect(async () => {
  if (product.value?.category) {
    const { data: related } = await $fetch<Product[]>(`/api/products/related/${product.value.category}`)
    product.value.relatedProducts = related
  }
})
</script>

最佳实践和模式

1. 唯一键策略

// ✅ 良好的键策略
const { data } = await useAsyncData(`user-${userId}`, () => $fetch(`/api/users/${userId}`));

// 对于依赖多个参数的数据
const { data } = await useAsyncData(`search-${searchTerm}-${page}-${category}`, () =>
	$fetch("/api/search", {
		query: { q: searchTerm, page, category }
	})
);

2. 加载状态管理

// components/DataLoader.vue
<script setup lang="ts">
interface AsyncDataState<T> {
  data: Ref<T | null>
  pending: Ref<boolean>
  error: Ref<Error | null>
  refresh: () => Promise<void>
}

// 可重用组合式函数
function useAsyncDataWithState<T>(
  key: string,
  handler: () => Promise<T>
): AsyncDataState<T> {
  const { data, pending, error, refresh } = useAsyncData(key, handler)

  return {
    data,
    pending,
    error,
    refresh
  }
}

// 在组件中使用
const { data: posts, pending, error } = useAsyncDataWithState(
  'posts',
  () => $fetch('/api/posts')
)
</script>

<template>
  <div>
    <div v-if="pending" class="loading">
      加载文章...
    </div>

    <div v-else-if="error" class="error">
      错误:{{ error.message }}
      <button @click="refresh">重试</button>
    </div>

    <div v-else-if="data" class="posts">
      <PostCard
        v-for="post in data"
        :key="post.id"
        :post="post"
      />
    </div>
  </div>
</template>

3. useLazyFetch 的网络优化

// 对于可以稍后加载的非关键数据
const { data: comments } = await useLazyFetch(`/api/posts/${postId}/comments`, {
	server: false, // 仅客户端
	lazy: true // 不阻塞渲染
});

// 对于频繁更新的数据
const { data: liveScore } = await useFetch("/api/live-score", {
	server: false,
	refresh: {
		// 每 30 秒刷新
		interval: 30000
	}
});

性能考虑

1. Nuxt 4 中的浅引用

Nuxt 4 通过默认对数据使用 shallowRef 来优化性能:

// 在 Nuxt 4 中,这默认是 shallowRef
const { data } = await useAsyncData("large-dataset", () => $fetch("/api/large-dataset"));

// 如果需要深度响应性(谨慎使用)
const { data } = await useAsyncData("deep-reactive", () => $fetch("/api/nested-data"), {
	deep: true // 启用深度响应性
});

2. 缓存策略

// 组件级缓存
const { data } = await useFetch("/api/config", {
	key: "app-config",
	// 缓存 5 分钟
	server: true,
	transform: data => {
		// 数据将保留在缓存中
		return data;
	}
});

// 条件缓存
const { data } = await useFetch("/api/user-preferences", {
	key: `user-prefs-${userId}`,
	server: false,
	// 仅在没有错误时缓存
	getCachedData: key => {
		const cached = nuxtApp.staticData[key] || nuxtApp.payload.data[key];
		return cached && !cached.error ? cached : null;
	}
});

TypeScript 集成

useFetch 的强类型

// types/api.ts
export interface User {
  id: number
  name: string
  email: string
  avatar?: string
}

export interface ApiResponse<T> {
  data: T
  message: string
  success: boolean
}

// pages/users/[id].vue
<script setup lang="ts">
const route = useRoute()
const { data: user } = await useFetch<ApiResponse<User>>(`/api/users/${route.params.id}`)

// 类型安全访问
if (user.value?.data) {
  console.log(user.value.data.name) // TypeScript 知道类型
}
</script>

useAsyncData 的类型

// composables/useDashboard.ts
export interface DashboardData {
	user: User;
	stats: {
		views: number;
		clicks: number;
		conversions: number;
	};
	recentActivity: Activity[];
}

export function useDashboardData() {
	return useAsyncData<DashboardData>("dashboard", async () => {
		const [userResponse, statsResponse, activityResponse] = await Promise.all([$fetch<ApiResponse<User>>("/api/user/profile"), $fetch<ApiResponse<Stats>>("/api/analytics/stats"), $fetch<ApiResponse<Activity[]>>("/api/activity/recent")]);

		return {
			user: userResponse.data,
			stats: statsResponse.data,
			recentActivity: activityResponse.data
		};
	});
}

调试和监控

调试工具

// 在开发中启用调试模式
const { data } = await useFetch("/api/debug-example", {
	// 在控制台显示调试信息
	onRequestError({ request, error }) {
		console.error("请求错误:", { request, error });
	},
	onResponseError({ response }) {
		console.error("响应错误:", response.status, response.statusText);
	},
	onResponse({ response }) {
		console.log("收到响应:", response._data);
	}
});

性能监控

// 用于测量性能的组合式函数
function useTrackedFetch<T>(url: string, options = {}) {
	const startTime = Date.now();

	return useFetch<T>(url, {
		...options,
		onResponse() {
			const duration = Date.now() - startTime;
			console.log(`获取 ${url} 耗时 ${duration}ms`);

			// 如果慢则发送到分析
			if (duration > 1000) {
				$fetch("/api/analytics/slow-request", {
					method: "POST",
					body: { url, duration }
				});
			}
		}
	});
}

结论

$fetchuseFetchuseAsyncData 之间做出正确选择对于构建高效且可维护的 Nuxt 4 应用程序至关重要:

  • $fetch 用于突变和直接客户端调用
  • useFetch 用于带 SSR 的简单数据获取
  • useAsyncData 用于复杂的异步逻辑和完全的状态控制

理解这些差异不仅可以防止双重获取等常见问题,还允许您充分利用 Nuxt 4 的通用渲染功能,构建更快、更高效且用户体验更好的应用程序。


本指南基于官方 Nuxt 4 文档和社区建立的最佳实践。要深入了解,请查阅 官方 Nuxt 文档 和 Nuxt 仓库中的示例。