她问我Pinia两种Store定义方式,到底选哪种写法,我说我也不知道...

凌晨1点,办公室里只剩下我和橙子还在对着屏幕发呆。生产环境的用户登录模块又出问题了,这次不是服务器炸了,而是"记住我"功能莫名其妙失效了。更要命的是,我们的Pinia Store代码写得乱七八糟,一会儿用stateactions,一会儿用refcomputed,连我们自己都搞不清楚到底哪种写法更好...

🔥 又是一个不眠夜

"豆子,你快看看这个登录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的好处很明显------结构化。你看,stategettersactions三部分泾渭分明,就像我们设计系统架构一样,每个模块职责清晰。"

"是的,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点,我们终于把所有测试都跑完了。

我长舒一口气:"原来两种写法各有优势啊。现在我明白了"

  1. 如果团队刚从Vuex迁移,或者项目比较简单,Options API更合适
  2. 如果是新项目,特别是TypeScript项目,Composition API体验更好
  3. 复杂的业务逻辑用Composition API更容易管理

"豆子,你觉得我们应该选哪种?"橙子问我。

我想了想:"其实都挺好的,但考虑到我们团队的情况..."

"考虑到我们现在的项目已经用了TypeScript,而且后续会有很多复杂的业务逻辑,我倾向于Composition API。"我说。

当我关掉了最后一个文件,靠在椅子上,对橙子说:"你说我们程序员是不是都有点强迫症?"

"哈哈,可能吧。"橙子笑着说:"想想刚开始写代码的时候,能跑就行。现在不仅要跑得对,还要跑得美。"


技术选型没有绝对的对错,只有适合与不适合。Pinia的两种写法各有优劣,关键是要根据项目实际情况和团队特点来选择。不管选择哪种,保持团队代码风格的一致性才是最重要的

毕竟,代码是写给人看的,不是写给机器看的。

相关推荐
程序员啊楠3 分钟前
Flutter 开发APP左滑返回到上一页
前端·flutter
程序员王天14 分钟前
Git Push 报错图解:从分支分叉到代码恢复
前端·git
Hilaku23 分钟前
前端权限系统怎么做才不会写吐?我们项目踩过的 3 套失败方案总结
前端·javascript·vue.js
nbsaas-boot28 分钟前
Vue 组件数据流与状态控制最佳实践规范
前端·javascript·vue.js
鹏多多.1 小时前
详解vue渲染函数render的使用
前端·javascript·vue.js·前端框架
初心w50t21 小时前
el-tree的属性render-content自定义样式不生效
前端·javascript·vue.js
19组清风1 小时前
深入解析 Vite 代码分割原理:从依赖入口点算法到动态导入优化
前端·vite·rollup.js
Luffe船长1 小时前
vue+elementUI实现固定table超过设定高度显示下拉条
前端·elementui·vue
网络点点滴1 小时前
什么是Vue.js
前端·javascript·vue.js
非优秀程序员1 小时前
10 个最佳开源 ChatGPT 替代方案,100% 本地运行
前端·人工智能·后端