Nuxt.js useFetch与useAsyncData #

一、useFetch详解 #

1.1 完整签名 #

typescript
function useFetch<T>(
  url: string | Ref<string> | (() => string),
  options?: UseFetchOptions<T>
): AsyncData<T, Error>

interface UseFetchOptions<T> {
  key?: string
  method?: string
  query?: Record<string, any>
  params?: Record<string, any>
  body?: RequestInit['body'] | Record<string, any>
  headers?: Record<string, string>
  baseURL?: string
  server?: boolean
  lazy?: boolean
  immediate?: boolean
  deep?: boolean
  dedupe?: boolean | 'cancel' | 'defer'
  timeout?: number
  retry?: number | false
  transform?: (input: T) => T
  pick?: string[]
  default?: () => T
  watch?: WatchSource[]
  getCachedData?: (key: string) => T
  onResponse?: (context: { response: Response }) => void
  onRequest?: (context: { request: Request }) => void
  onRequestError?: (context: { request: Request; error: Error }) => void
  onResponseError?: (context: { response: Response; error: Error }) => void
}

1.2 响应式URL #

vue
<script setup lang="ts">
const userId = ref(1)

const url = computed(() => `/api/users/${userId.value}`)

const { data } = await useFetch(url)
</script>

1.3 动态查询参数 #

vue
<script setup lang="ts">
const filters = reactive({
  status: 'active',
  role: '',
  search: ''
})

const { data } = await useFetch('/api/users', {
  query: filters
})
</script>

1.4 请求拦截 #

vue
<script setup lang="ts">
const { data } = await useFetch('/api/users', {
  onRequest({ request }) {
    console.log('请求:', request)
  },
  onResponse({ response }) {
    console.log('响应:', response)
  },
  onRequestError({ request, error }) {
    console.error('请求错误:', error)
  },
  onResponseError({ response, error }) {
    console.error('响应错误:', error)
  }
})
</script>

二、useAsyncData详解 #

2.1 完整签名 #

typescript
function useAsyncData<T>(
  key: string,
  handler: (ctx?: NuxtApp) => Promise<T>,
  options?: AsyncDataOptions<T>
): AsyncData<T, Error>

interface AsyncDataOptions<T> {
  server?: boolean
  lazy?: boolean
  immediate?: boolean
  deep?: boolean
  dedupe?: boolean | 'cancel' | 'defer'
  transform?: (input: T) => T
  pick?: string[]
  default?: () => T
  watch?: WatchSource[]
  getCachedData?: (key: string) => T
}

2.2 唯一键管理 #

vue
<script setup lang="ts">
const route = useRoute()

const { data } = await useAsyncData(
  `article-${route.params.slug}`,
  () => $fetch(`/api/articles/${route.params.slug}`),
  {
    watch: [() => route.params.slug]
  }
)
</script>

2.3 复杂数据获取 #

vue
<script setup lang="ts">
const { data } = await useAsyncData(
  'dashboard-stats',
  async () => {
    const [users, posts, analytics] = await Promise.all([
      $fetch('/api/users/stats'),
      $fetch('/api/posts/stats'),
      $fetch('/api/analytics')
    ])
    
    return {
      totalUsers: users.total,
      totalPosts: posts.total,
      pageViews: analytics.pageViews,
      growth: {
        users: users.growth,
        posts: posts.growth
      }
    }
  }
)
</script>

2.4 条件获取 #

vue
<script setup lang="ts">
const { data } = await useAsyncData(
  'conditional-data',
  async () => {
    if (import.meta.server) {
      return await $fetch('/api/server-data')
    }
    return await $fetch('/api/client-data')
  }
)
</script>

三、高级选项 #

3.1 dedupe选项 #

vue
<script setup lang="ts">
const { data } = await useFetch('/api/users', {
  dedupe: 'cancel'
})
</script>
说明
true 跳过重复请求
'cancel' 取消之前的请求
'defer' 等待之前的请求完成

3.2 deep选项 #

vue
<script setup lang="ts">
const { data } = await useFetch('/api/users', {
  deep: false
})
</script>

3.3 immediate选项 #

vue
<script setup lang="ts">
const { data, execute } = await useFetch('/api/users', {
  immediate: false
})

const loadUsers = () => {
  execute()
}
</script>

3.4 watch选项 #

vue
<script setup lang="ts">
const page = ref(1)
const pageSize = ref(10)
const sortBy = ref('name')

const { data } = await useFetch('/api/users', {
  query: { page, pageSize, sortBy },
  watch: [page, pageSize, sortBy]
})
</script>

四、数据刷新 #

4.1 手动刷新 #

vue
<script setup lang="ts">
const { data, refresh } = await useFetch('/api/users')

const handleRefresh = async () => {
  await refresh()
}
</script>

4.2 强制刷新 #

vue
<script setup lang="ts">
const { data, refresh } = await useFetch('/api/users')

const forceRefresh = async () => {
  await refresh({ dedupe: false })
}
</script>

4.3 定时刷新 #

vue
<script setup lang="ts">
const { data, refresh } = await useFetch('/api/notifications')

let interval: NodeJS.Interval

onMounted(() => {
  interval = setInterval(() => {
    refresh()
  }, 30000)
})

onUnmounted(() => {
  clearInterval(interval)
})
</script>

五、错误处理 #

5.1 基本错误处理 #

vue
<script setup lang="ts">
const { data, error } = await useFetch('/api/users')

if (error.value) {
  console.error('请求失败:', error.value)
}
</script>

<template>
  <div v-if="error" class="error">
    {{ error.message }}
  </div>
</template>

5.2 自定义错误处理 #

vue
<script setup lang="ts">
const { data, error } = await useFetch('/api/users', {
  onResponseError({ response, error }) {
    if (response.status === 401) {
      navigateTo('/login')
    } else if (response.status === 403) {
      showError({ statusCode: 403, message: '没有权限' })
    }
  }
})
</script>

5.3 重试机制 #

vue
<script setup lang="ts">
const { data, error } = await useFetch('/api/users', {
  retry: 3,
  retryDelay: 1000,
  onRequestError({ error }) {
    console.log('请求失败,正在重试...')
  }
})
</script>

六、数据转换 #

6.1 transform函数 #

vue
<script setup lang="ts">
interface ApiUser {
  id: number
  first_name: string
  last_name: string
  email: string
}

interface User {
  id: number
  name: string
  email: string
}

const { data } = await useFetch<ApiUser[]>('/api/users', {
  transform: (users) => users.map(user => ({
    id: user.id,
    name: `${user.first_name} ${user.last_name}`,
    email: user.email
  }))
})
</script>

6.2 pick选择字段 #

vue
<script setup lang="ts">
const { data } = await useFetch('/api/users', {
  pick: ['id', 'name', 'email']
})
</script>

6.3 默认值 #

vue
<script setup lang="ts">
const { data } = await useFetch('/api/users', {
  default: () => []
})
</script>

七、缓存策略 #

7.1 自定义缓存 #

vue
<script setup lang="ts">
const cache = new Map()

const { data } = await useFetch('/api/users', {
  key: 'users-list',
  getCachedData(key) {
    const cached = cache.get(key)
    if (cached && Date.now() - cached.timestamp < 60000) {
      return cached.data
    }
    return null
  }
})
</script>

7.2 禁用缓存 #

vue
<script setup lang="ts">
const { data } = await useFetch('/api/users', {
  key: `users-${Date.now()}`,
  getCachedData: () => null
})
</script>

7.3 缓存失效 #

vue
<script setup lang="ts">
const { data: users } = await useFetch('/api/users', {
  key: 'users-list'
})

const deleteUser = async (id: number) => {
  await $fetch(`/api/users/${id}`, { method: 'DELETE' })
  
  clearNuxtData('users-list')
  
  await refresh()
}
</script>

八、最佳实践 #

8.1 封装API请求 #

composables/useApi.ts

typescript
export const useApi = () => {
  const config = useRuntimeConfig()
  
  const fetch = async <T>(url: string, options: any = {}) => {
    return useFetch<T>(url, {
      baseURL: config.public.apiBase,
      headers: {
        'Authorization': `Bearer ${useCookie('token').value}`
      },
      ...options
    })
  }
  
  return { fetch }
}

使用:

vue
<script setup lang="ts">
const { fetch } = useApi()

const { data } = await fetch<User[]>('/users')
</script>

8.2 类型安全 #

typescript
interface User {
  id: number
  name: string
  email: string
}

interface ApiResponse<T> {
  data: T
  total: number
}

const { data } = await useFetch<ApiResponse<User[]>>('/api/users')

8.3 组合使用 #

vue
<script setup lang="ts">
const userId = ref(1)

const { data: user } = await useFetch(() => `/api/users/${userId.value}`)

const { data: posts } = await useFetch(() => `/api/users/${userId.value}/posts`, {
  watch: [userId],
  immediate: !!user.value
})
</script>

九、完整示例 #

9.1 数据表格组件 #

vue
<script setup lang="ts">
interface User {
  id: number
  name: string
  email: string
  role: string
  createdAt: string
}

interface Pagination {
  page: number
  pageSize: number
  total: number
  totalPages: number
}

interface Sort {
  field: string
  order: 'asc' | 'desc'
}

const pagination = reactive<Pagination>({
  page: 1,
  pageSize: 10,
  total: 0,
  totalPages: 0
})

const sort = reactive<Sort>({
  field: 'createdAt',
  order: 'desc'
})

const filters = reactive({
  search: '',
  role: ''
})

const debouncedSearch = refDebounced(toRef(filters, 'search'), 300)

const { data, pending, refresh } = await useFetch<{ data: User[], pagination: Pagination }>(
  '/api/users',
  {
    query: {
      page: pagination.page,
      pageSize: pagination.pageSize,
      sortField: sort.field,
      sortOrder: sort.order,
      search: debouncedSearch,
      role: filters.role
    },
    watch: [
      () => pagination.page,
      () => pagination.pageSize,
      () => sort.field,
      () => sort.order,
      debouncedSearch,
      () => filters.role
    ],
    transform: (response) => {
      pagination.total = response.pagination.total
      pagination.totalPages = response.pagination.totalPages
      return response
    }
  }
)

const handleSort = (field: string) => {
  if (sort.field === field) {
    sort.order = sort.order === 'asc' ? 'desc' : 'asc'
  } else {
    sort.field = field
    sort.order = 'asc'
  }
}

const handlePageChange = (page: number) => {
  pagination.page = page
}
</script>

<template>
  <div>
    <div class="filters">
      <input
        v-model="filters.search"
        placeholder="搜索用户..."
      />
      <select v-model="filters.role">
        <option value="">全部角色</option>
        <option value="admin">管理员</option>
        <option value="user">用户</option>
      </select>
      <button @click="refresh">刷新</button>
    </div>
    
    <table v-if="!pending">
      <thead>
        <tr>
          <th @click="handleSort('name')">
            姓名
            <span v-if="sort.field === 'name'">
              {{ sort.order === 'asc' ? '↑' : '↓' }}
            </span>
          </th>
          <th>邮箱</th>
          <th>角色</th>
          <th @click="handleSort('createdAt')">
            创建时间
            <span v-if="sort.field === 'createdAt'">
              {{ sort.order === 'asc' ? '↑' : '↓' }}
            </span>
          </th>
        </tr>
      </thead>
      <tbody>
        <tr v-for="user in data?.data" :key="user.id">
          <td>{{ user.name }}</td>
          <td>{{ user.email }}</td>
          <td>{{ user.role }}</td>
          <td>{{ user.createdAt }}</td>
        </tr>
      </tbody>
    </table>
    
    <div class="pagination">
      <button
        :disabled="pagination.page === 1"
        @click="handlePageChange(pagination.page - 1)"
      >
        上一页
      </button>
      <span>{{ pagination.page }} / {{ pagination.totalPages }}</span>
      <button
        :disabled="pagination.page >= pagination.totalPages"
        @click="handlePageChange(pagination.page + 1)"
      >
        下一页
      </button>
    </div>
  </div>
</template>

十、总结 #

本章深入介绍了 useFetchuseAsyncData

  • 完整的选项配置
  • 响应式 URL 和参数
  • 高级选项(dedupe、deep、immediate)
  • 数据刷新和错误处理
  • 数据转换和缓存策略
  • 最佳实践和封装技巧

掌握这些知识可以高效地管理应用数据,下一章我们将学习服务端 API 开发。

最后更新:2026-03-28