当我们从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('恢复认证状态...')
}
}
}
)