mcpField
N
nuxt-data

nuxt-data

github.com/secondsky开发240392 次安装5 周前更新
代码仓库

关于 nuxt-data

nuxt-data 是面向 Nuxt 4 开发者的数据获取与状态管理技能指南,介绍支持 SSR 的组合式函数、缓存、响应性、错误处理和 Pinia 集成。

主要功能

  1. 01比较 useFetch、useAsyncData 和 $fetch
  2. 02用 useState 管理 SSR 安全的共享状态
  3. 03使用响应式请求参数
  4. 04配置缓存与请求去重
  5. 05处理错误和数据刷新
  6. 06将 Pinia 集成到 Nuxt

适用场景

  1. 01在服务端渲染期间获取 API 数据
  2. 02构建共享的认证或购物车状态
  3. 03实现响应式搜索和分页
  4. 04排查数据未更新或水合问题

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/secondsky/claude-skills --skill nuxt-data

安装后在 Claude Code 中输入/nuxt-data确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
composables.md
data-fetching.md
templates/composables/
useAuth.ts

详细介绍

Nuxt 4 数据管理

Nuxt 4 应用中的组合式函数、数据获取和状态管理模式。

快速参考

数据获取方法
方法使用场景SSR缓存响应式
useFetch简单的 API 调用是是是
useAsyncData自定义异步逻辑是是是
$fetch仅限客户端、事件处理否否否
组合式函数命名
前缀用途示例
use状态/逻辑组合式函数useAuth、useCart
fetch仅获取数据fetchUsers(较少使用)

何时加载参考资料

在以下情况加载 references/composables.md:

  • 编写具有复杂状态的自定义组合式函数
  • 调试状态管理问题或内存泄漏
  • 使用浏览器 API 实现 SSR 安全模式
  • 构建身份验证或复杂状态组合式函数
  • 了解单例与每次调用创建的组合式函数模式

在以下情况加载 references/data-fetching.md:

  • 使用响应式参数获取 API 数据
  • 排查浅层与深层响应性问题
  • 调试参数变化时数据未刷新的问题
  • 实现分页、无限滚动或搜索
  • 了解转换函数、缓存或错误处理

在以下情况加载 references/pinia-integration.md:

  • 为复杂状态管理配置 Pinia
  • 创建包含 getter 和 action 的 store
  • 将 Pinia 与 SSR 集成
  • 在页面重新加载后保留状态

组合式函数

useState——基础

useState 会创建 SSR 安全的共享响应式状态,并在组件实例之间持久保留。

// composables/useCounter.ts
export const useCounter = () => {
  // Singleton - shared across all components
  const count = useState('counter', () => 0)

  const increment = () => count.value++
  const decrement = () => count.value--
  const reset = () => count.value = 0

  return { count, increment, decrement, reset }
}
useState 与 ref——关键区别
// CORRECT: Shared state (singleton pattern)
export const useAuth = () => {
  const user = useState('auth-user', () => null)  // Shared!
  return { user }
}

// WRONG: Creates new instance every call!
export const useAuth = () => {
  const user = ref(null)  // Not shared!
  return { user }
}

规则:共享/全局状态使用 useState。仅在组件本地状态中使用 ref。

完整的身份验证组合式函数
// composables/useAuth.ts
export const useAuth = () => {
  const user = useState<User | null>('auth-user', () => null)
  const isAuthenticated = computed(() => !!user.value)
  const isLoading = useState('auth-loading', () => false)

  const login = async (email: string, password: string) => {
    isLoading.value = true
    try {
      const data = await $fetch('/api/auth/login', {
        method: 'POST',
        body: { email, password }
      })
      user.value = data.user
      return { success: true }
    } catch (error) {
      return { success: false, error: error.message }
    } finally {
      isLoading.value = false
    }
  }

  const logout = async () => {
    await $fetch('/api/auth/logout', { method: 'POST' })
    user.value = null
    navigateTo('/login')
  }

  const checkSession = async () => {
    if (import.meta.server) return  // Skip on server
    try {
      const data = await $fetch('/api/auth/session')
      user.value = data.user
    } catch {
      user.value = null
    }
  }

  return { user, isAuthenticated, isLoading, login, logout, checkSession }
}
SSR 安全的浏览器 API
// composables/useLocalStorage.ts
export const useLocalStorage = <T>(key: string, defaultValue: T) => {
  const data = useState<T>(key, () => defaultValue)

  // Only access localStorage on client
  if (import.meta.client) {
    const stored = localStorage.getItem(key)
    if (stored) {
      data.value = JSON.parse(stored)
    }

    // Watch and persist changes
    watch(data, (newValue) => {
      localStorage.setItem(key, JSON.stringify(newValue))
    }, { deep: true })
  }

  return data
}

数据获取

useFetch——基本用法
// Simple GET request
const { data, error, pending, refresh } = await useFetch('/api/users')

// With options
const { data: users } = await useFetch('/api/users', {
  method: 'GET',
  query: { limit: 10, offset: 0 },
  headers: { 'X-Custom-Header': 'value' }
})
响应式参数
<script setup lang="ts">
const page = ref(1)
const search = ref('')

// Auto-refetches when page or search changes
const { data: users, pending } = await useFetch('/api/users', {
  query: {
    page,
    search,
    limit: 10
  }
})

// Or with computed
const query = computed(() => ({
  page: page.value,
  search: search.value,
  limit: 10
}))

const { data } = await useFetch('/api/users', { query })
</script>
转换数据
const { data: userNames } = await useFetch('/api/users', {
  transform: (users) => users.map(u => u.name)
})

// data.value is now string[] instead of User[]
选取特定字段
const { data } = await useFetch('/api/user', {
  pick: ['id', 'name', 'email']  // Only these fields in payload
})
useAsyncData——自定义逻辑
// Multiple parallel requests
const { data } = await useAsyncData('dashboard', async () => {
  const [users, posts, stats] = await Promise.all([
    $fetch('/api/users'),
    $fetch('/api/posts'),
    $fetch('/api/stats')
  ])
  return { users, posts, stats }
})

// Access: data.value.users, data.value.posts, data.value.stats
错误处理
const { data, error, status } = await useFetch('/api/users')

// Check error
if (error.value) {
  console.error('Error:', error.value.message)
  console.error('Status:', error.value.statusCode)
}

// Status values: 'idle' | 'pending' | 'success' | 'error'
if (status.value === 'error') {
  showError(error.value)
}
手动刷新
const { data, refresh, execute } = await useFetch('/api/users', {
  immediate: false  // Don't fetch on mount
})

// Fetch manually
await execute()

// Refresh (re-fetch)
await refresh()

// Refresh with new params
await refresh({ dedupe: true })
浅层与深层响应性(v4 变更)
// Nuxt 4 default: Shallow reactivity
const { data } = await useFetch('/api/user')
data.value.name = 'New Name'  // Won't trigger reactivity!

// Enable deep reactivity for mutations
const { data } = await useFetch('/api/user', {
  deep: true
})
data.value.name = 'New Name'  // Now works!

// Or refresh instead of mutating
const { data, refresh } = await useFetch('/api/user')
await $fetch('/api/user', { method: 'PATCH', body: { name: 'New Name' } })
await refresh()  // Re-fetch updated data
缓存与去重
const { data } = await useFetch('/api/users', {
  key: 'users-list',           // Custom cache key
  dedupe: 'cancel',            // Cancel duplicate requests
  getCachedData: (key, nuxtApp) => {
    // Return cached data if valid
    return nuxtApp.payload.data[key]
  }
})
延迟加载数据
// useLazyFetch - Navigation happens immediately, data loads in background
const { data, pending } = useLazyFetch('/api/users')

// useLazyAsyncData
const { data, pending } = useLazyAsyncData('users', () => $fetch('/api/users'))
$fetch——仅限客户端
// In event handlers (not during SSR)
const submitForm = async () => {
  const result = await $fetch('/api/submit', {
    method: 'POST',
    body: formData.value
  })
}

// In server routes
export default defineEventHandler(async (event) => {
  const externalData = await $fetch('https://api.example.com/data')
  return externalData
})

状态管理

useState 模式
// Simple counter
const count = useState('count', () => 0)

// Complex object
const settings = useState('settings', () => ({
  theme: 'light',
  notifications: true,
  language: 'en'
}))

// Typed state
interface User {
  id: string
  name: string
  email: string
}
const user = useState<User | null>('user', () => null)
共享购物车示例
// composables/useCart.ts
interface CartItem {
  id: string
  name: string
  price: number
  quantity: number
}

export const useCart = () => {
  const items = useState<CartItem[]>('cart-items', () => [])

  const total = computed(() =>
    items.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
  )

  const itemCount = computed(() =>
    items.value.reduce((sum, item) => sum + item.quantity, 0)
  )

  const addItem = (product: Omit<CartItem, 'quantity'>) => {
    const existing = items.value.find(i => i.id === product.id)
    if (existing) {
      existing.quantity++
    } else {
      items.value.push({ ...product, quantity: 1 })
    }
  }

  const removeItem = (id: string) => {
    items.value = items.value.filter(i => i.id !== id)
  }

  const updateQuantity = (id: string, quantity: number) => {
    const item = items.value.find(i => i.id === id)
    if (item) {
      item.quantity = Math.max(0, quantity)
      if (item.quantity === 0) removeItem(id)
    }
  }

  const clearCart = () => {
    items.value = []
  }

  return { items, total, itemCount, addItem, removeItem, updateQuantity, clearCart }
}
Pinia 集成
bun add pinia @pinia/nuxt
// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@pinia/nuxt']
})

// stores/auth.ts
import { defineStore } from 'pinia'

export const useAuthStore = defineStore('auth', {
  state: () => ({
    user: null as User | null,
    token: null as string | null
  }),

  getters: {
    isAuthenticated: (state) => !!state.user,
    userName: (state) => state.user?.name ?? 'Guest'
  },

  actions: {
    async login(email: string, password: string) {
      const { user, token } = await $fetch('/api/auth/login', {
        method: 'POST',
        body: { email, password }
      })
      this.user = user
      this.token = token
    },

    logout() {
      this.user = null
      this.token = null
    }
  }
})

// Usage in components
const authStore = useAuthStore()
await authStore.login('[email protected]', 'password')
console.log(authStore.userName)

常见反模式

使用 ref 而非 useState
// WRONG - Creates new instance every time!
export const useAuth = () => {
  const user = ref(null)  // Not shared
  return { user }
}

// CORRECT
export const useAuth = () => {
  const user = useState('auth-user', () => null)
  return { user }
}
缺少错误处理
// WRONG
const { data } = await useFetch('/api/users')
console.log(data.value.length)  // Crashes if error!

// CORRECT
const { data, error } = await useFetch('/api/users')
if (error.value) {
  showToast({ type: 'error', message: error.value.message })
  return
}
console.log(data.value.length)
非确定性转换
// WRONG - Causes hydration mismatch!
const { data } = await useFetch('/api/users', {
  transform: (users) => users.sort(() => Math.random() - 0.5)
})

// CORRECT
const { data } = await useFetch('/api/users', {
  transform: (users) => users.sort((a, b) => a.name.localeCompare(b.name))
})
修改浅层 ref
// WRONG - v4 uses shallow refs by default
const { data } = await useFetch('/api/user')
data.value.name = 'New Name'  // Won't trigger reactivity!

// CORRECT - Option 1: Enable deep
const { data } = await useFetch('/api/user', { deep: true })
data.value.name = 'New Name'

// CORRECT - Option 2: Replace entire value
data.value = { ...data.value, name: 'New Name' }

// CORRECT - Option 3: Refresh after mutation
await $fetch('/api/user', { method: 'PATCH', body: { name: 'New Name' } })
await refresh()

故障排查

参数变化时数据未刷新:

  • 确保参数是响应式的:{ query: { page } },其中 page = ref(1)
  • 确认使用的是 ref 本身,而非 .value

使用 useState 时出现水合不匹配:

  • 确保键唯一:useState('unique-key', () => value)
  • 避免在初始值中使用 Math.random() 或 Date.now()

导航后状态丢失:

  • 持久状态使用 useState 而非 ref
  • 检查各组件使用的键是否相同

无限重新获取循环:

  • 检查转换函数中的响应式依赖
  • 对副作用使用带有 { immediate: false } 的 watch

相关技能

  • nuxt-core:项目设置、路由、配置
  • nuxt-server:服务端路由、API 模式
  • nuxt-production:性能、测试、部署

版本:4.0.0 | 最后更新:2025-12-28 | 许可证:MIT

评论

支持 Markdown