Nuxt 生态 精选推荐

Nuxt 3 状态管理完整指南 - Pinia 实战

HTMLPAGE 团队
12 分钟阅读

深度讲解 Nuxt 3 中使用 Pinia 进行状态管理,包括 Store 设计、跨组件通信、异步状态、调试等完整实战指南。

#Nuxt 3 #Pinia #状态管理 #Store #最佳实践

Nuxt 3 状态管理完整指南 - Pinia 实战

Pinia 是 Vue 3 的官方推荐状态管理库。本文详细讲解如何在 Nuxt 3 中高效使用 Pinia。

1. Pinia 基础概念

什么是 Pinia

Pinia vs Vuex:
├─ 语法更简洁 (无需 mutations)
├─ 完整的 TypeScript 支持
├─ 多个 store 可互相依赖
├─ 体积更小 (约 2KB)
├─ Vue DevTools 集成
└─ Hot Module Replacement

安装和配置

# Nuxt 3 已内置 Pinia,无需单独安装
pnpm add pinia @pinia/nuxt

# nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@pinia/nuxt'],
  pinia: {
    storesDirs: ['./stores/**']
  }
})

2. Store 设计模式

模式 1: 选项式 API

// stores/useAuthStore.ts
export const useAuthStore = defineStore('auth', {
  // 1. 状态
  state: () => ({
    user: null as User | null,
    token: '',
    isLoading: false,
    error: ''
  }),
  
  // 2. 计算属性
  getters: {
    // 是否已登录
    isLoggedIn: (state) => !!state.user,
    
    // 用户权限列表
    permissions: (state) => state.user?.permissions || []
  },
  
  // 3. 方法 (同步和异步)
  actions: {
    // 同步方法
    setUser(user: User) {
      this.user = user
    },
    
    // 异步方法
    async login(email: string, password: string) {
      this.isLoading = true
      this.error = ''
      
      try {
        const response = await $fetch('/api/login', {
          method: 'POST',
          body: { email, password }
        })
        
        this.user = response.user
        this.token = response.token
      } catch (e: any) {
        this.error = e.message
        throw e
      } finally {
        this.isLoading = false
      }
    },
    
    // 异步方法 2
    async logout() {
      await $fetch('/api/logout', { method: 'POST' })
      this.user = null
      this.token = ''
    }
  }
})

// 使用
<script setup lang="ts">
const authStore = useAuthStore()

// 访问状态
console.log(authStore.user)
console.log(authStore.isLoading)

// 访问 getter
console.log(authStore.isLoggedIn)

// 调用 action
await authStore.login('user@example.com', 'password')
</script>

模式 2: 组合式 API

// stores/useCounterStore.ts
export const useCounterStore = defineStore('counter', () => {
  // 状态
  const count = ref(0)
  const history = ref<number[]>([])
  
  // 计算属性
  const double = computed(() => count.value * 2)
  const average = computed(() => 
    history.value.length > 0 
      ? history.value.reduce((a, b) => a + b) / history.value.length 
      : 0
  )
  
  // 方法
  const increment = () => {
    count.value++
    history.value.push(count.value)
  }
  
  const decrement = () => {
    count.value--
    history.value.push(count.value)
  }
  
  const reset = () => {
    count.value = 0
    history.value = []
  }
  
  // 返回暴露的成员
  return {
    count,
    history,
    double,
    average,
    increment,
    decrement,
    reset
  }
})

模式 3: Store 依赖

// stores/useCartStore.ts
export const useCartStore = defineStore('cart', () => {
  // 依赖另一个 store
  const authStore = useAuthStore()
  const productStore = useProductStore()
  
  const items = ref<CartItem[]>([])
  
  // 计算总价 (依赖 productStore)
  const total = computed(() =>
    items.value.reduce((sum, item) => {
      const product = productStore.getProduct(item.productId)
      return sum + (product?.price || 0) * item.quantity
    }, 0)
  )
  
  // 添加到购物车 (检查认证)
  const addItem = (productId: string, quantity: number) => {
    if (!authStore.isLoggedIn) {
      navigateTo('/login')
      return
    }
    
    items.value.push({ productId, quantity })
  }
  
  // 结账 (调用其他 store 的 action)
  const checkout = async () => {
    // 先获取最新的产品信息
    await productStore.fetchProducts()
    
    // 然后提交订单
    const order = await $fetch('/api/orders', {
      method: 'POST',
      body: {
        userId: authStore.user?.id,
        items: items.value,
        total: total.value
      }
    })
    
    items.value = []
    return order
  }
  
  return {
    items,
    total,
    addItem,
    checkout
  }
})

3. 实战案例

案例 1: 用户认证管理

// stores/useAuthStore.ts
interface User {
  id: string
  email: string
  name: string
  avatar?: string
  role: 'user' | 'admin' | 'teacher'
}

export const useAuthStore = defineStore('auth', {
  state: () => ({
    user: null as User | null,
    token: null as string | null,
    loading: false,
    error: null as string | null
  }),
  
  getters: {
    isLoggedIn: (state) => !!state.user,
    isAdmin: (state) => state.user?.role === 'admin',
    isTeacher: (state) => state.user?.role === 'teacher',
    
    // 权限检查
    hasPermission: (state) => (permission: string) => {
      if (state.user?.role === 'admin') return true
      return state.user?.permissions?.includes(permission) || false
    }
  },
  
  actions: {
    // 从 token 恢复用户
    async restoreUser() {
      const token = useCookie('token')
      if (!token.value) return
      
      try {
        const user = await $fetch('/api/me', {
          headers: { Authorization: `Bearer ${token.value}` }
        })
        this.user = user
        this.token = token.value
      } catch {
        useCookie('token').value = null
      }
    },
    
    // 登录
    async login(email: string, password: string) {
      this.loading = true
      this.error = null
      
      try {
        const response = await $fetch('/api/auth/login', {
          method: 'POST',
          body: { email, password }
        })
        
        this.user = response.user
        this.token = response.token
        useCookie('token').value = response.token
      } catch (e: any) {
        this.error = e.data?.message || '登录失败'
        throw e
      } finally {
        this.loading = false
      }
    },
    
    // 注册
    async signup(data: { email: string; password: string; name: string }) {
      this.loading = true
      this.error = null
      
      try {
        const response = await $fetch('/api/auth/signup', {
          method: 'POST',
          body: data
        })
        
        this.user = response.user
        this.token = response.token
        useCookie('token').value = response.token
      } catch (e: any) {
        this.error = e.data?.message || '注册失败'
        throw e
      } finally {
        this.loading = false
      }
    },
    
    // 登出
    async logout() {
      try {
        await $fetch('/api/auth/logout', { method: 'POST' })
      } finally {
        this.user = null
        this.token = null
        useCookie('token').value = null
      }
    }
  }
})

案例 2: 表单状态管理

// stores/useFormStore.ts
interface FormData {
  [key: string]: any
}

export const useFormStore = defineStore('form', () => {
  // 多个表单的状态
  const forms = ref<Map<string, FormData>>(new Map())
  const errors = ref<Map<string, Record<string, string>>>(new Map())
  const loading = ref<Map<string, boolean>>(new Map())
  
  // 初始化表单
  const initForm = (formId: string, initialData: FormData = {}) => {
    forms.value.set(formId, { ...initialData })
    errors.value.set(formId, {})
  }
  
  // 更新表单字段
  const updateField = (formId: string, field: string, value: any) => {
    const form = forms.value.get(formId)
    if (form) {
      form[field] = value
      // 实时验证
      validateField(formId, field)
    }
  }
  
  // 批量更新表单
  const updateForm = (formId: string, data: FormData) => {
    forms.value.set(formId, {
      ...forms.value.get(formId),
      ...data
    })
  }
  
  // 验证单个字段
  const validateField = (formId: string, field: string) => {
    const form = forms.value.get(formId)
    const fieldErrors = errors.value.get(formId) || {}
    
    const value = form?.[field]
    
    // 简单验证规则
    if (field === 'email' && value) {
      const isValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
      if (!isValid) {
        fieldErrors[field] = '请输入有效的邮箱'
      } else {
        delete fieldErrors[field]
      }
    }
    
    errors.value.set(formId, fieldErrors)
  }
  
  // 验证整个表单
  const validateForm = (formId: string) => {
    // 验证所有字段
    const form = forms.value.get(formId)
    if (!form) return true
    
    Object.keys(form).forEach(field => {
      validateField(formId, field)
    })
    
    const fieldErrors = errors.value.get(formId)
    return !fieldErrors || Object.keys(fieldErrors).length === 0
  }
  
  // 提交表单
  const submitForm = async (formId: string, submitHandler: (data: FormData) => Promise<any>) => {
    if (!validateForm(formId)) {
      return false
    }
    
    loading.value.set(formId, true)
    
    try {
      const form = forms.value.get(formId)
      const result = await submitHandler(form || {})
      
      // 清空表单
      forms.value.delete(formId)
      errors.value.delete(formId)
      
      return result
    } catch (error: any) {
      // 处理错误
      if (error.data?.errors) {
        errors.value.set(formId, error.data.errors)
      }
      throw error
    } finally {
      loading.value.set(formId, false)
    }
  }
  
  // 重置表单
  const resetForm = (formId: string) => {
    forms.value.delete(formId)
    errors.value.delete(formId)
    loading.value.delete(formId)
  }
  
  return {
    forms,
    errors,
    loading,
    initForm,
    updateField,
    updateForm,
    validateField,
    validateForm,
    submitForm,
    resetForm
  }
})

// 在组件中使用
<script setup lang="ts">
const formStore = useFormStore()

onMounted(() => {
  formStore.initForm('loginForm', {
    email: '',
    password: ''
  })
})

const email = computed({
  get: () => formStore.forms.get('loginForm')?.email || '',
  set: (value) => formStore.updateField('loginForm', 'email', value)
})

const handleSubmit = async () => {
  await formStore.submitForm('loginForm', async (data) => {
    return await useAuthStore().login(data.email, data.password)
  })
}
</script>

4. Store 最佳实践

// ✅ 最佳实践检查清单

// 1. 职责分离
// ✅ 好: 明确的职责
stores/
  ├── useAuthStore.ts      (用户认证)
  ├── useProductStore.ts   (产品数据)
  ├── useCartStore.ts      (购物车)
  └── useUIStore.ts        (UI 状态)

// ❌ 避免: 一个大的 store
stores/
  └── useAppStore.ts       (什么都做)

// 2. 命名约定
// ✅ 好: 清晰的命名
const authStore = useAuthStore()
const userList = authStore.users

// ❌ 避免: 模糊的命名
const store = useStore()
const list = store.l

// 3. 类型安全
interface User {
  id: string
  name: string
  email: string
}

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

// 4. 避免直接修改状态
// ✅ 好: 通过 action 修改
authStore.setUser(newUser)

// ❌ 避免: 直接修改
authStore.user = newUser

// 5. 异步操作规范
async fetchData() {
  this.loading = true
  this.error = null
  
  try {
    const data = await fetchFromAPI()
    this.data = data
  } catch (error) {
    this.error = error.message
  } finally {
    this.loading = false
  }
}

5. 调试和监控

// 1. Pinia DevTools (自动集成)
// 在浏览器 Vue DevTools 中:
// - 查看所有 store 的状态
// - 追踪 actions 的执行
// - 时间旅行调试

// 2. 手动日志
export const useAuthStore = defineStore('auth', {
  actions: {
    async login(email: string, password: string) {
      console.log('[Auth] 开始登录', { email })
      // ...
      console.log('[Auth] 登录成功', { user: this.user })
    }
  }
})

// 3. 监控状态变化
const authStore = useAuthStore()

watch(
  () => authStore.user,
  (newUser, oldUser) => {
    console.log('用户状态改变:', { from: oldUser, to: newUser })
  }
)

// 4. 性能监控
export const useAuthStore = defineStore('auth', {
  actions: {
    async heavyOperation() {
      const start = performance.now()
      // 执行操作
      const duration = performance.now() - start
      console.log(`操作耗时: ${duration}ms`)
    }
  }
})

总结

特性优势
灵活的状态管理支持选项式和组合式 API
完整的类型支持TypeScript 集成
简洁的语法无需 mutations
Store 间协作轻松共享数据
开发工具集成DevTools 支持

相关资源