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

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

本文将通过实战案例------用户登录的"记住我"功能,深入对比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风格的特点

优点:

  • 结构化清晰stategettersactions严格分离
  • 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('恢复认证状态...')
      }
    }
  }
)
相关推荐
OpenTiny社区2 分钟前
盘点字体性能优化方案
前端·javascript
FogLetter6 分钟前
深入浅出React Hooks:useEffect那些事儿
前端·javascript
Savior`L7 分钟前
CSS知识复习4
前端·css
0wioiw022 分钟前
Flutter基础(前端教程④-组件拼接)
前端·flutter
花生侠1 小时前
记录:前端项目使用pnpm+husky(v9)+commitlint,提交代码格式化校验
前端
猿榜1 小时前
魔改编译-永久解决selenium痕迹(二)
javascript·python
阿幸软件杂货间1 小时前
阿幸课堂随机点名
android·开发语言·javascript
一涯1 小时前
Cursor操作面板改为垂直
前端
我要让全世界知道我很低调1 小时前
记一次 Vite 下的白屏优化
前端·css