凌晨1点,办公室里只剩下我和橙子还在对着屏幕发呆。生产环境的用户登录模块又出问题了,这次不是服务器炸了,而是"记住我"功能莫名其妙失效了。更要命的是,我们的Pinia Store代码写得乱七八糟,一会儿用
state
、actions
,一会儿用ref
、computed
,连我们自己都搞不清楚到底哪种写法更好...
🔥 又是一个不眠夜
"豆子,你快看看这个登录Store,我都要疯了!"橙子指着屏幕上的代码,额头上都冒汗了。
我凑过去一看,好家伙,这代码写得确实...让人头大:
javascript
// 这是什么鬼混合写法?
export const useAuthStore = defineStore('auth', {
state: () => ({ user: null }),
actions: {
login() { /* ... */ }
}
})
// 另一个文件里又是这样写的
export const useUserStore = defineStore('user', () => {
const profile = ref(null)
const updateProfile = () => { /* ... */ }
return { profile, updateProfile }
})
"橙子,这是谁写的?"我问道。
"你写的一半,我写的一半..."她有些尴尬,"当时赶项目,也没统一规范,现在维护起来真的很痛苦。"
确实,我想起来了。上个月接手用户模块的时候,我习惯性地用了Options API的写法,因为之前用Vuex用习惯了。而橙子作为前端架构师,更喜欢用Composition API,说是类型提示更好。
结果就是现在这个四不像的代码库。
🎯 问题的起源:两种写法的困惑
"豆子,你老实说,你真的知道这两种写法的区别吗?"橙子直接灵魂拷问。
我挠挠头:"嗯...一个像Vuex,一个像setup函数?"
"就这?"
"还有...一个用this,一个用.value?"
橙子翻了个白眼:"算了,我们今晚就把这个问题彻底搞清楚,不然明天轩哥他们又要吐槽我们的代码了。"
说到轩哥,我就想起他上次Code Review时那个嫌弃的表情。作为后端架构师,他最受不了的就是前端代码不规范。
先说Options API风格
橙子打开了一个新文件:"我们先来看看Options API风格,这个你应该熟悉。"
javascript
// stores/auth.js
import { defineStore } from 'pinia'
export const useAuthStore = defineStore('auth', {
// 1. 状态就像Vue组件的data
state: () => ({
user: null,
token: null,
rememberedEmail: '',
isLoading: false,
loginAttempts: 0
}),
// 2. 计算属性就像computed
getters: {
// 简单的getter
isLoggedIn: (state) => !!state.token,
// 这个可以传参数,很实用
getUserDisplayName: (state) => (defaultName = '游客') => {
return state.user?.name || defaultName
},
// 可以用this访问其他getter
welcomeMessage() {
return `欢迎回来,${this.getUserDisplayName('用户')}!`
},
// 业务逻辑判断
canLogin: (state) => state.loginAttempts < 5
},
// 3. 方法就像methods
actions: {
// 同步操作
setRememberedEmail(email) {
this.rememberedEmail = email
if (email) {
localStorage.setItem('rememberedEmail', email)
} else {
localStorage.removeItem('rememberedEmail')
}
},
// 异步操作 - 核心登录逻辑
async login(credentials) {
const { email, password, remember } = credentials
if (!this.canLogin) {
throw new Error('登录尝试次数过多,请稍后再试')
}
this.isLoading = true
this.loginAttempts++
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()
// 更新状态
this.user = data.user
this.token = data.token
this.loginAttempts = 0
// 处理"记住我"功能
if (remember) {
this.setRememberedEmail(email)
} else {
this.setRememberedEmail('')
}
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')
}
}
})
"看起来还挺清晰的嘛。"我点点头,接着说:"Options API的好处很明显------结构化。你看,state
、getters
、actions
三部分泾渭分明,就像我们设计系统架构一样,每个模块职责清晰。"
"是的,Options API的优点很明显:结构化、好理解、团队容易统一规范。这样写的好处是:团队里的任何人都能快速理解代码结构,维护起来也很方便。特别是从Vuex迁移过来的项目,几乎不需要学习成本。"慕橙说,"但是它也有问题。"
再看Composition API风格
"来,我们再看看Composition API风格。"橙子新开了一个文件:
javascript
// stores/auth-composition.js
import { defineStore } from 'pinia'
import { ref, computed, watch } from 'vue'
export const useAuthStore = defineStore('auth', () => {
// 1. 状态定义 - 用ref包装
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. 监听器 - 这个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')
}
// 5. 逻辑复用 - 这也是Options API做不到的
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
}
})
"哇,这个写法确实更灵活!"我惊叹道,"特别是那个watch监听器和逻辑复用,Options API确实做不到。"
"对吧!"橙子眼睛亮了,"而且TypeScript的类型推断也更准确。不过你有没有发现,代码量明显增加了?"
确实,Composition API的写法虽然灵活,可以使用watch
来监听状态变化,还可以提取公共逻辑。最关键的是,TypeScript的类型推断非常准确。但代码看起来更冗长一些。
"你看,"橙子说,"在组件层面,两种Store写法的使用体验几乎一样。关键差异在于Store内部的开发体验。"
实战对比:哪种更适合我们的场景?
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 }
})
"看到了吗?Composition API的类型推断明显更准确。"橙子指着屏幕说。
"确实,IDE的提示也更友好。"我点头赞同。
语法对比表
这时候,我突然想起八哥总是喜欢做表格对比,于是我们也整理了一个:
功能 | 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 |
类型推断 | 一般 | 优秀 |
逻辑复用 | 困难 | 容易 |
加入持久化:让"记住我"功能完美运行
"豆子,现在我们来解决最初的问题------'记住我'功能。"橙子说,"我们需要用到pinia-plugin-persistedstate插件。"
安装配置
bash
npm install pinia-plugin-persistedstate
橙子接着说:"我们需要在main.js
文件中引入这个插件"
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')
Options API + 持久化
橙子打开一个js文件,指着文件说:"如果是使用 Options API
的话,只需要增加一个配置项persist
,在这一项中增加持久化配置即可。"
javascript
export const useAuthStore = defineStore('auth', {
state: () => ({
user: null,
token: null,
rememberedEmail: '',
}),
getters: {
isLoggedIn: (state) => !!state.token
},
actions: {
login(credentials) { /* 登录逻辑 */ },
logout() {
// 退出时清除敏感信息,但保留偏好设置
this.user = null
this.token = null
// rememberedEmail 和 preferences 会被持久化保留
}
},
// 持久化配置
persist: {
key: 'auth-options',
storage: localStorage,
paths: ['rememberedEmail', 'preferences'], // 只持久化这些字段
beforeRestore: (context) => {
console.log('即将恢复状态:', context)
},
afterRestore: (context) => {
console.log('状态已恢复:', context)
}
}
})
我问道:"如果是使用Composition API,那该怎么配置呢?"
Composition API + 持久化
橙子这时候打开另外一个文件,对我说:"Pinia 的 defineStore
的持久化配置(使用 pinia-plugin-persistedstate
插件时)需要作为第三个参数传入,插件会在 store 初始化时读取第三个参数的配置"
js
export const useAuthStore = defineStore('auth',
() => {
const user = ref(null)
const token = ref(null)
const rememberedEmail = ref('')
const isLoggedIn = computed(() => !!token.value)
const login = async (credentials) => { /* 登录逻辑 */ }
const logout = () => {
user.value = null
token.value = null
}
return {
user, token, rememberedEmail, preferences,
isLoggedIn, login, logout
}
},
{
persist: {
key: 'auth-composition',
storage: localStorage,
paths: ['rememberedEmail', 'preferences']
}
}
)
🌙 团队决策:我们的最终选择
凌晨3点,我们终于把所有测试都跑完了。
我长舒一口气:"原来两种写法各有优势啊。现在我明白了"
- 如果团队刚从Vuex迁移,或者项目比较简单,Options API更合适
- 如果是新项目,特别是TypeScript项目,Composition API体验更好
- 复杂的业务逻辑用Composition API更容易管理
"豆子,你觉得我们应该选哪种?"橙子问我。
我想了想:"其实都挺好的,但考虑到我们团队的情况..."
"考虑到我们现在的项目已经用了TypeScript,而且后续会有很多复杂的业务逻辑,我倾向于Composition API。"我说。
当我关掉了最后一个文件,靠在椅子上,对橙子说:"你说我们程序员是不是都有点强迫症?"
"哈哈,可能吧。"橙子笑着说:"想想刚开始写代码的时候,能跑就行。现在不仅要跑得对,还要跑得美。"
技术选型没有绝对的对错,只有适合与不适合。Pinia的两种写法各有优劣,关键是要根据项目实际情况和团队特点来选择。不管选择哪种,保持团队代码风格的一致性才是最重要的。
毕竟,代码是写给人看的,不是写给机器看的。