深入理解Pinia:Options API vs Composition API两种Store定义方式完全指南

当我们从Vuex迁移到Pinia时,经常会遇到一个困惑:为什么有些教程使用state、actions、getters的写法,而有些却使用ref、computed、函数的写法?这两种写法有什么区别?哪种更好?

本文将通过实战案例------用户登录的"记住我"功能,深入对比Pinia的两种Store定义方式,理解它们的区别、优劣势和选择标准。

一、认识Pinia的两种Store定义方式

Pinia提供了两种截然不同的Store定义方式,它们分别对应Vue 3的两种编程范式:

1.1 方式概览

js 复制代码
// Options API风格 - 类似Vuex
defineStore('auth', {
  state: () => ({ ... }),
  getters: { ... },
  actions: { ... }
})

// Composition API风格 - 类似setup函数
defineStore('auth', () => {
  const state = ref(...)
  const getter = computed(...)
  const action = () => { ... }
  return { state, getter, action }
})

1.2 为什么会有两种方式?

这两种方式的存在反映了Vue 3生态系统的演进:

  • Options API风格:保持与Vuex的一致性,便于迁移
  • Composition API风格:拥抱Vue 3的新特性,提供更好的类型推断和逻辑复用

二、Options API风格详解

2.1 基本结构

Options API风格的Store结构清晰,严格分离关注点:

javascript 复制代码
// stores/auth.js
import { defineStore } from 'pinia'

export const useAuthStore = defineStore('auth', {
  // 1. 状态定义 - 相当于组件的data
  state: () => ({
    user: null,
    token: null,
    rememberedEmail: '',
    isLoading: false,
    loginAttempts: 0
  }),

  // 2. 计算属性 - 相当于组件的computed
  getters: {
    // 简单的getter
    isLoggedIn: (state) => !!state.token,
    
    // 依赖其他getter的getter
    canLogin: (state) => state.loginAttempts < 5,
    
    // 返回函数的getter(支持传参)
    getUserDisplayName: (state) => (defaultName = '游客') => {
      return state.user?.name || defaultName
    },
    
    // 使用this访问其他getter
    welcomeMessage() {
      return `欢迎回来,${this.getUserDisplayName('用户')}!`
    }
  },

  // 3. 方法定义 - 相当于组件的methods
  actions: {
    // 同步action
    setRememberedEmail(email) {
      this.rememberedEmail = email
      if (email) {
        localStorage.setItem('rememberedEmail', email)
      } else {
        localStorage.removeItem('rememberedEmail')
      }
    },

    // 异步action
    async login(credentials) {
      const { email, password, remember } = credentials
      
      // 检查登录次数限制
      if (!this.canLogin) {
        throw new Error('登录尝试次数过多,请稍后再试')
      }

      this.isLoading = true
      this.loginAttempts++

      try {
        // 模拟API调用
        const response = await fetch('/api/login', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ email, password })
        })

        if (!response.ok) {
          const error = await response.json()
          throw new Error(error.message || '登录失败')
        }

        const data = await response.json()

        // 更新状态
        this.user = data.user
        this.token = data.token
        this.loginAttempts = 0 // 重置登录尝试次数

        // 处理"记住我"功能
        if (remember) {
          this.setRememberedEmail(email)
        } else {
          this.setRememberedEmail('')
        }

        // 持久化token
        localStorage.setItem('authToken', data.token)
        
        return data
      } catch (error) {
        console.error('登录失败:', error)
        throw error
      } finally {
        this.isLoading = false
      }
    },

    logout() {
      this.user = null
      this.token = null
      this.loginAttempts = 0
      localStorage.removeItem('authToken')
    },

    loadRememberedEmail() {
      const email = localStorage.getItem('rememberedEmail')
      if (email) {
        this.rememberedEmail = email
      }
    },

    // 调用其他action
    async initializeAuth() {
      // 加载记住的邮箱
      this.loadRememberedEmail()
      
      // 检查是否有保存的token
      const token = localStorage.getItem('authToken')
      if (token) {
        this.token = token
        try {
          await this.validateToken()
        } catch (error) {
          console.warn('Token验证失败:', error)
          this.logout()
        }
      }
    },

    async validateToken() {
      const response = await fetch('/api/validate-token', {
        headers: { Authorization: `Bearer ${this.token}` }
      })
      
      if (response.ok) {
        this.user = await response.json()
      } else {
        throw new Error('Token无效')
      }
    }
  }
})

2.2 Options API风格的特点

优点:

  • ✅ 结构化清晰 :state、getters、actions严格分离
  • ✅ Vuex用户友好:API设计与Vuex高度相似,迁移成本低
  • ✅ 团队协作友好:代码风格统一,容易制定规范
  • ✅ 学习成本低:概念简单,容易理解

缺点:

  • ❌ TypeScript支持有限:类型推断不够准确
  • ❌ 逻辑复用困难:难以提取和复用公共逻辑
  • ❌ this指向问题:在某些情况下this的指向可能不明确

三、Composition API风格详解

3.1 基本结构

Composition API风格更加灵活,可以自由组合逻辑:

javascript 复制代码
// stores/auth.js
import { defineStore } from 'pinia'
import { ref, computed, watch } from 'vue'

export const useAuthStore = defineStore('auth', () => {
  // 1. 状态定义 - 使用ref/reactive
  const user = ref(null)
  const token = ref(null)
  const rememberedEmail = ref('')
  const isLoading = ref(false)
  const loginAttempts = ref(0)

  // 2. 计算属性 - 使用computed
  const isLoggedIn = computed(() => !!token.value)
  const canLogin = computed(() => loginAttempts.value < 5)
  
  // 支持传参的计算属性
  const getUserDisplayName = computed(() => {
    return (defaultName = '游客') => {
      return user.value?.name || defaultName
    }
  })
  
  const welcomeMessage = computed(() => {
    return `欢迎回来,${getUserDisplayName.value('用户')}!`
  })

  // 3. 监听器 - 使用watch(Options API风格不直接支持)
  watch(rememberedEmail, (newEmail) => {
    if (newEmail) {
      localStorage.setItem('rememberedEmail', newEmail)
    } else {
      localStorage.removeItem('rememberedEmail')
    }
  })

  // 监听登录状态变化
  watch(isLoggedIn, (loggedIn) => {
    if (loggedIn) {
      console.log('用户已登录')
    } else {
      console.log('用户已退出')
    }
  })

  // 4. 方法定义 - 直接定义函数
  const setRememberedEmail = (email) => {
    rememberedEmail.value = email
  }

  const login = async (credentials) => {
    const { email, password, remember } = credentials
    
    if (!canLogin.value) {
      throw new Error('登录尝试次数过多,请稍后再试')
    }

    isLoading.value = true
    loginAttempts.value++

    try {
      const response = await fetch('/api/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ email, password })
      })

      if (!response.ok) {
        const error = await response.json()
        throw new Error(error.message || '登录失败')
      }

      const data = await response.json()

      user.value = data.user
      token.value = data.token
      loginAttempts.value = 0

      if (remember) {
        setRememberedEmail(email)
      } else {
        setRememberedEmail('')
      }

      localStorage.setItem('authToken', data.token)
      
      return data
    } catch (error) {
      console.error('登录失败:', error)
      throw error
    } finally {
      isLoading.value = false
    }
  }

  const logout = () => {
    user.value = null
    token.value = null
    loginAttempts.value = 0
    localStorage.removeItem('authToken')
  }

  const loadRememberedEmail = () => {
    const email = localStorage.getItem('rememberedEmail')
    if (email) {
      rememberedEmail.value = email
    }
  }

  const validateToken = async () => {
    const response = await fetch('/api/validate-token', {
      headers: { Authorization: `Bearer ${token.value}` }
    })
    
    if (response.ok) {
      user.value = await response.json()
    } else {
      throw new Error('Token无效')
    }
  }

  const initializeAuth = async () => {
    loadRememberedEmail()
    
    const savedToken = localStorage.getItem('authToken')
    if (savedToken) {
      token.value = savedToken
      try {
        await validateToken()
      } catch (error) {
        console.warn('Token验证失败:', error)
        logout()
      }
    }
  }

  // 5. 逻辑复用 - 可以提取公共逻辑
  const useRetry = (fn, maxAttempts = 3) => {
    return async (...args) => {
      let lastError
      for (let i = 0; i < maxAttempts; i++) {
        try {
          return await fn(...args)
        } catch (error) {
          lastError = error
          if (i < maxAttempts - 1) {
            await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)))
          }
        }
      }
      throw lastError
    }
  }

  // 使用重试逻辑包装登录方法
  const loginWithRetry = useRetry(login, 2)

  // 6. 返回要暴露的状态和方法
  return {
    // 状态
    user,
    token,
    rememberedEmail,
    isLoading,
    loginAttempts,
    
    // 计算属性
    isLoggedIn,
    canLogin,
    getUserDisplayName,
    welcomeMessage,
    
    // 方法
    login,
    loginWithRetry,
    logout,
    setRememberedEmail,
    loadRememberedEmail,
    initializeAuth,
    validateToken
  }
})

3.2 Composition API风格的特点

优点:

  • ✅ TypeScript支持优秀:类型推断准确,IDE支持更好
  • ✅ 逻辑复用容易:可以提取公共组合函数
  • ✅ 更加灵活:可以使用watch、生命周期等Vue 3特性
  • ✅ 性能更好:更好的tree-shaking支持
  • ✅ 调试体验更好:DevTools支持更完善

缺点:

  • ❌ 学习成本高:需要理解ref、computed、watch等概念
  • ❌ 代码结构松散:没有强制的结构约束
  • ❌ 容易出错 :忘记.value是常见问题

四、两种方式的详细对比

4.1 语法对比表

功能 Options API风格 Composition API风格
状态定义 state: () => ({ count: 0 }) const count = ref(0)
计算属性 getters: { double: (state) => state.count * 2 } const double = computed(() => count.value * 2)
方法定义 actions: { increment() { this.count++ } } const increment = () => count.value++
访问状态 this.count count.value
访问其他getter this.double double.value
调用其他action this.otherAction() otherAction()

4.2 使用场景对比

场景1:简单的状态管理

javascript 复制代码
// Options API - 更适合
defineStore('counter', {
  state: () => ({ count: 0 }),
  getters: { double: (state) => state.count * 2 },
  actions: { increment() { this.count++ } }
})

// Composition API - 略显复杂
defineStore('counter', () => {
  const count = ref(0)
  const double = computed(() => count.value * 2)
  const increment = () => count.value++
  return { count, double, increment }
})

场景2:复杂的业务逻辑

javascript 复制代码
// Options API - 结构混乱
defineStore('complex', {
  state: () => ({ 
    users: [], 
    currentUser: null, 
    permissions: [], 
    settings: {} 
  }),
  getters: { 
    // 大量getter混杂在一起
  },
  actions: { 
    // 大量action混杂在一起
  }
})

// Composition API - 可以按功能分组
defineStore('complex', () => {
  // 用户相关逻辑
  const users = ref([])
  const currentUser = ref(null)
  const loadUsers = async () => { /* ... */ }
  
  // 权限相关逻辑
  const permissions = ref([])
  const checkPermission = (perm) => { /* ... */ }
  
  // 设置相关逻辑
  const settings = ref({})
  const updateSettings = (newSettings) => { /* ... */ }
  
  return { 
    users, currentUser, loadUsers,
    permissions, checkPermission,
    settings, updateSettings
  }
})

4.3 TypeScript支持对比

javascript 复制代码
// Options API - 类型推断有限
interface User {
  id: number
  name: string
  email: string
}

export const useAuthStore = defineStore('auth', {
  state: (): { user: User | null } => ({
    user: null
  }),
  getters: {
    // 需要手动类型注解
    userName: (state): string => state.user?.name || ''
  },
  actions: {
    setUser(user: User) {
      this.user = user // this的类型推断不完美
    }
  }
})

// Composition API - 类型推断优秀
export const useAuthStore = defineStore('auth', () => {
  const user = ref<User | null>(null) // 类型推断准确
  
  const userName = computed(() => user.value?.name || '') // 自动推断返回类型
  
  const setUser = (newUser: User) => {
    user.value = newUser // 完美的类型检查
  }
  
  return { user, userName, setUser }
})

五、持久化插件的使用

5.1 安装和配置持久化插件

bash 复制代码
npm install pinia-plugin-persistedstate
javascript 复制代码
// main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'

const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)

const app = createApp(App)
app.use(pinia)
app.mount('#app')

5.2 Options API风格 + 持久化

javascript 复制代码
export const useAuthStore = defineStore('auth', {
  state: () => ({
    user: null,
    token: null,
    rememberedEmail: '',
    preferences: {
      theme: 'light',
      language: 'zh-CN'
    }
  }),

  getters: {
    isLoggedIn: (state) => !!state.token
  },

  actions: {
    login(credentials) {
      // 登录逻辑
    },
    
    logout() {
      // 退出时清除敏感信息,但保留偏好设置
      this.user = null
      this.token = null
      // rememberedEmail 和 preferences 会被持久化保留
    }
  },

  // 持久化配置
  persist: {
    key: 'auth', // 存储的key
    storage: localStorage, // 存储方式
    paths: ['rememberedEmail', 'preferences'], // 只持久化指定字段
    
    // 高级配置
    beforeRestore: (context) => {
      console.log('即将恢复状态:', context)
    },
    afterRestore: (context) => {
      console.log('状态已恢复:', context)
    }
  }
})

5.3 Composition API风格 + 持久化

javascript 复制代码
export const useAuthStore = defineStore('auth', 
  () => {
    const user = ref(null)
    const token = ref(null)
    const rememberedEmail = ref('')
    const preferences = ref({
      theme: 'light',
      language: 'zh-CN'
    })

    const isLoggedIn = computed(() => !!token.value)

    const login = async (credentials) => {
      // 登录逻辑
    }

    const logout = () => {
      user.value = null
      token.value = null
      // rememberedEmail 和 preferences 保持不变
    }

    return {
      user,
      token,
      rememberedEmail,
      preferences,
      isLoggedIn,
      login,
      logout
    }
  },
  {
    // 持久化配置
    persist: {
      key: 'auth-composition',
      storage: localStorage,
      paths: ['rememberedEmail', 'preferences'],
      
      // 自定义序列化
      serializer: {
        serialize: JSON.stringify,
        deserialize: JSON.parse
      }
    }
  }
)

5.4 多存储配置

javascript 复制代码
export const useAuthStore = defineStore('auth', {
  state: () => ({
    user: null,
    token: null,
    rememberedEmail: '',
    preferences: {
      theme: 'light',
      language: 'zh-CN'
    }
  }),

  // 多个持久化配置
  persist: [
    {
      key: 'auth-session',
      storage: sessionStorage,
      paths: ['user', 'token'] // 会话级别的数据
    },
    {
      key: 'auth-local',
      storage: localStorage,
      paths: ['rememberedEmail', 'preferences'] // 长期保存的数据
    }
  ]
})

六、实战案例:登录"记住我"功能完整实现

6.1 登录组件实现

html 复制代码
<!-- LoginForm.vue -->
<template>
  <div class="login-container">
    <form @submit.prevent="handleLogin" class="login-form">
      <h2>用户登录</h2>
      
      <div class="form-group">
        <label for="email">邮箱地址</label>
        <input
          id="email"
          v-model="loginForm.email"
          type="email"
          placeholder="请输入邮箱地址"
          required
          :disabled="authStore.isLoading"
        />
      </div>

      <div class="form-group">
        <label for="password">登录密码</label>
        <input
          id="password"
          v-model="loginForm.password"
          type="password"
          placeholder="请输入登录密码"
          required
          :disabled="authStore.isLoading"
        />
      </div>

      <div class="form-group checkbox-group">
        <label class="checkbox-label">
          <input
            v-model="loginForm.remember"
            type="checkbox"
            :disabled="authStore.isLoading"
          />
          <span class="checkmark"></span>
          记住我的邮箱
        </label>
      </div>

      <div class="form-group">
        <button 
          type="submit" 
          class="login-btn"
          :disabled="authStore.isLoading || !authStore.canLogin"
        >
          <span v-if="authStore.isLoading">登录中...</span>
          <span v-else>立即登录</span>
        </button>
      </div>

      <div v-if="loginAttempts > 0" class="login-attempts">
        登录尝试次数: {{ authStore.loginAttempts }}/5
      </div>
    </form>

    <!-- 登录成功后的用户信息 -->
    <div v-if="authStore.isLoggedIn" class="user-info">
      <h3>{{ authStore.welcomeMessage }}</h3>
      <div class="user-details">
        <p>用户名: {{ authStore.user?.name }}</p>
        <p>邮箱: {{ authStore.user?.email }}</p>
        <p>登录时间: {{ loginTime }}</p>
      </div>
      <button @click="handleLogout" class="logout-btn">退出登录</button>
    </div>
  </div>
</template>

<script setup>
import { reactive, ref, onMounted } from 'vue'
import { useAuthStore } from '@/stores/auth'

const authStore = useAuthStore()

const loginForm = reactive({
  email: '',
  password: '',
  remember: false
})

const loginTime = ref('')
const errorMessage = ref('')

// 初始化
onMounted(async () => {
  await authStore.initializeAuth()
  
  // 如果有记住的邮箱,自动填充
  if (authStore.rememberedEmail) {
    loginForm.email = authStore.rememberedEmail
    loginForm.remember = true
  }
})

const handleLogin = async () => {
  try {
    errorMessage.value = ''
    
    await authStore.login({
      email: loginForm.email,
      password: loginForm.password,
      remember: loginForm.remember
    })
    
    loginTime.value = new Date().toLocaleString()
    
    // 清空密码
    loginForm.password = ''
    
  } catch (error) {
    errorMessage.value = error.message
    console.error('登录失败:', error)
  }
}

const handleLogout = () => {
  authStore.logout()
  loginTime.value = ''
  
  // 如果没有记住邮箱,清空表单
  if (!authStore.rememberedEmail) {
    loginForm.email = ''
  }
  loginForm.password = ''
}
</script>

<style scoped>
.login-container {
  max-width: 400px;
  margin: 50px auto;
  padding: 20px;
}

.login-form {
  background: #fff;
  padding: 30px;
  border-radius: 8px;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}

.form-group {
  margin-bottom: 20px;
}

label {
  display: block;
  margin-bottom: 5px;
  font-weight: 500;
  color: #333;
}

input[type="email"],
input[type="password"] {
  width: 100%;
  padding: 12px;
  border: 1px solid #ddd;
  border-radius: 4px;
  font-size: 16px;
}

input[type="email"]:focus,
input[type="password"]:focus {
  outline: none;
  border-color: #007bff;
  box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
}

.checkbox-group {
  display: flex;
  align-items: center;
}

.checkbox-label {
  display: flex;
  align-items: center;
  cursor: pointer;
  font-weight: normal;
}

.checkbox-label input[type="checkbox"] {
  margin-right: 8px;
}

.login-btn {
  width: 100%;
  padding: 12px;
  background: #007bff;
  color: white;
  border: none;
  border-radius: 4px;
  font-size: 16px;
  cursor: pointer;
  transition: background-color 0.3s;
}

.login-btn:hover:not(:disabled) {
  background: #0056b3;
}

.login-btn:disabled {
  background: #ccc;
  cursor: not-allowed;
}

.login-attempts {
  text-align: center;
  color: #ffc107;
  font-size: 14px;
  margin-top: 10px;
}

.user-info {
  background: #f8f9fa;
  padding: 20px;
  border-radius: 8px;
  margin-top: 20px;
}

.user-details {
  margin: 15px 0;
}

.user-details p {
  margin: 5px 0;
  color: #666;
}

.logout-btn {
  background: #dc3545;
  color: white;
  border: none;
  padding: 8px 16px;
  border-radius: 4px;
  cursor: pointer;
}

.logout-btn:hover {
  background: #c82333;
}

.error-message {
  background: #f8d7da;
  color: #721c24;
  padding: 10px;
  border-radius: 4px;
  margin-top: 10px;
}
</style>

6.2 Store的最佳实践配置

根据项目需求选择合适的Store风格:

javascript 复制代码
// 推荐:Composition API风格 + 持久化
export const useAuthStore = defineStore('auth', 
  () => {
    // 状态定义
    const user = ref(null)
    const token = ref(null)
    const rememberedEmail = ref('')
    const isLoading = ref(false)
    const loginAttempts = ref(0)
    const lastLoginTime = ref(null)

    // 计算属性
    const isLoggedIn = computed(() => !!token.value)
    const canLogin = computed(() => loginAttempts.value < 5)
    
    const welcomeMessage = computed(() => {
      if (!user.value) return ''
      const hour = new Date().getHours()
      const greeting = hour < 12 ? '早上好' : hour < 18 ? '下午好' : '晚上好'
      return `${greeting},${user.value.name}!`
    })

    // 监听器
    watch(isLoggedIn, (loggedIn) => {
      if (loggedIn) {
        lastLoginTime.value = new Date().toISOString()
      }
    })

    // 方法
    const login = async (credentials) => {
      // ... 登录逻辑
    }

    const logout = () => {
      user.value = null
      token.value = null
      loginAttempts.value = 0
      lastLoginTime.value = null
    }

    const initializeAuth = async () => {
      // ... 初始化逻辑
    }

    return {
      user, token, rememberedEmail, isLoading, loginAttempts, lastLoginTime,
      isLoggedIn, canLogin, welcomeMessage,
      login, logout, initializeAuth
    }
  },
  {
    persist: {
      key: 'auth-state',
      storage: localStorage,
      paths: ['rememberedEmail', 'lastLoginTime'],
      beforeRestore: (context) => {
        console.log('恢复认证状态...')
      }
    }
  }
)
相关推荐
Dream-Y.ocean2 小时前
鸿蒙PC平台宅匠 3D 室内设计适配实战:基于 Electron 壳方案的 Three.js 双渲染引擎与参数化户型编辑器开发
javascript·3d·harmonyos
Csvn3 小时前
性能监控
前端
Highcharts.js3 小时前
Highcharts Gantt使用符号的自定义数据标签开发甘特图示例
javascript·甘特图·highcharts·图表开发·实战代码·响应式图表
Csvn3 小时前
构建工具
前端
FRing034 小时前
vue2干货总结3(computed计算属性、watch监听器)
开发语言·前端·vue.js·vue2
Java小卷4 小时前
JuanForm vs FormCreate!Vue3 表单引擎联动配置 PK!
vue.js·低代码
kyriewen6 小时前
Skills 榜前 10 我全装了:只有 3 个能直接用
前端·javascript·程序员
IT_陈寒6 小时前
当Java的hashCode遇上Lombok,我的缓存被全清了
前端·人工智能·后端
工控丫丫7 小时前
帆软看板中经常使用的公式函数及经常搭的组合
前端·javascript·数据库
jerrywus7 小时前
我用 GPT-6.1-sol 做了台会讲解的 3D 售货机:60 罐饮料,一口没喝上
前端·agent·claude