Vue状态管理扫盲篇:在组件中优雅地使用 Pinia | 类型提示、解构、持久化

8 阅读6分钟

同学们好,我是 Eugene(尤金),一个拥有多年中后台开发经验的前端工程师~

(Eugene 发音很简单,/juːˈdʒiːn/,大家怎么顺口怎么叫就好)

你是否也有过:明明学过很多技术,一到关键时候却讲不出来、甚至写不出来?

你是否也曾怀疑自己,是不是太笨了,明明感觉会,却总差一口气?

就算想沉下心从头梳理,可工作那么忙,回家还要陪伴家人。

一天只有24小时,时间永远不够用,常常感到力不从心。

技术行业,本就是逆水行舟,不进则退。

如果你也有同样的困扰,别慌。

从现在开始,跟着我一起心态归零,利用碎片时间,来一次彻彻底底的基础扫盲

这一次,我们一起慢慢来,扎扎实实变强。

不搞花里胡哨的理论堆砌,只分享看得懂、用得上的前端干货,

咱们一起稳步积累,真正摆脱“面向搜索引擎写代码”的尴尬。

面向会写 Vue/JS 但概念略混、或想把基础打牢的开发者,讲「怎么选、为什么选、踩坑在哪」。

一、先搞清楚:Pinia 是什么,为什么要用?

Pinia 是 Vue 3 官方推荐的状态管理库,取代 Vuex。它的作用是:

  • 多个组件共享同一份数据
  • 数据更新后,所有用到的地方都能自动更新

如果只是父子组件传值,用 propsemit 就够了;当多个不相关的组件需要同一份数据时,就需要 Pinia 这类全局状态管理。

二、安装与基础配置

npm install pinia

main.jsmain.ts 中:

import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'

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

三、定义 Store:从简单到规范

3.1 先看一个最基础的 Store

// stores/counter.js
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
    name: '计数器'
  }),
  getters: {
    doubleCount: (state) => state.count * 2
  },
  actions: {
    increment() {
      this.count++
    }
  }
})

3.2 加上 TypeScript 类型(推荐做法)

很多项目是 .ts,或者希望有更好的提示,建议一开始就加上类型:

// stores/counter.ts
import { defineStore } from 'pinia'

// 1. 先定义 state 的类型
interface CounterState {
  count: number
  name: string
}

export const useCounterStore = defineStore('counter', {
  state: (): CounterState => ({
    count: 0,
    name: '计数器'
  }),
  getters: {
    doubleCount: (state): number => state.count * 2,
    // 带参数的 getter
    getGreeting: (state) => (prefix: string) => `${prefix}, ${state.name}`
  },
  actions: {
    increment() {
      this.count++  // 这里 this 会自动推断类型
    },
    async fetchCount() {
      const res = await fetch('/api/count')
      const data = await res.json()
      this.count = data.count
    }
  }
})

为什么要单独定义 CounterState

  • 便于在其它文件中复用类型
  • state 结构一变,IDE 会立刻提示哪里需要改
  • 更符合多人协作和长期维护习惯

四、在组件中使用:三种方式对比

4.1 方式一:直接使用 store(适合少量字段)

<template>
  <div>
    <p>{{ store.count }}</p>
    <p>{{ store.doubleCount }}</p>
    <button @click="store.increment">+1</button>
  </div>
</template>

<script setup lang="ts">
import { useCounterStore } from '@/stores/counter'

const store = useCounterStore()
</script>
  • 优点:写法简单
  • 缺点:每次访问都要 store.xxx,模板和逻辑都略啰嗦

4.2 方式二:直接解构(容易踩坑)

<script setup lang="ts">
import { useCounterStore } from '@/stores/counter'

const { count, doubleCount, increment } = useCounterStore()
</script>

<template>
  <div>
    <p>{{ count }}</p>  <!-- 第一次有值,之后不会更新! -->
    <button @click="increment">+1</button>
  </div>
</template>

问题:countdoubleCount 会失去响应式。点击按钮后,store 变了,但模板里的 count 不会更新。
原因:解构得到的是普通值,不是响应式引用,所以响应式链路断了。


4.3 方式三:用 storeToRefs 解构(推荐)

<template>
  <div>
    <p>{{ count }}</p>
    <p>{{ doubleCount }}</p>
    <button @click="increment">+1</button>
  </div>
</template>

<script setup lang="ts">
import { storeToRefs } from 'pinia'
import { useCounterStore } from '@/stores/counter'

const store = useCounterStore()

// state 和 getters:必须用 storeToRefs,否则会丢失响应式
const { count, doubleCount } = storeToRefs(store)

// actions:直接从 store 解构即可,它们本来就是普通函数
const { increment } = store
</script>

记忆口诀:

  • state + getters:用 storeToRefs 解构
  • actions:直接从 store 解构

五、storeToRefs 细节说明

5.1 为什么 actions 不需要 storeToRefs?

  • state 和 getters 是「数据」,需要在模板/计算属性里被追踪变化
  • actions 是函数,不参与响应式,直接解构不会影响功能

5.2 常见写法对比

// ✅ 推荐:清晰、有类型、响应式正确
const store = useCounterStore()
const { count, doubleCount } = storeToRefs(store)
const { increment, fetchCount } = store

// ❌ 错误:count 和 doubleCount 不再响应
const { count, doubleCount, increment } = useCounterStore()

// ❌ 错误:不要把 actions 放进 storeToRefs
const { count, increment } = storeToRefs(store)  // increment 会变成 ref,调用要 .value

六、数据持久化:刷新页面不丢数据

场景:用户登录信息、主题、语言等,希望刷新后仍然保留。

6.1 插件选择

插件维护状态推荐度
pinia-plugin-persistedstate社区主流⭐⭐⭐ 首选
pinia-plugin-persistedstate-2活跃维护的 fork⭐⭐ 备选
pinia-plugin-persist多年未更新⚠️ 不推荐新项目使用

建议新项目用 pinia-plugin-persistedstate

6.2 安装与全局注册

npm install pinia-plugin-persistedstate
// main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import App from './App.vue'

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

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

6.3 在 Store 中开启持久化

// stores/user.ts
import { defineStore } from 'pinia'

interface UserState {
  token: string
  userInfo: { id: number; name: string } | null
}

export const useUserStore = defineStore('user', {
  state: (): UserState => ({
    token: '',
    userInfo: null
  }),
  persist: true  // 最简配置:用 store id 作为 key,存到 localStorage
})

6.4 常用持久化配置

export const useUserStore = defineStore('user', {
  state: (): UserState => ({
    token: '',
    userInfo: null
  }),
  persist: {
    key: 'my-user',           // 自定义 localStorage 的 key
    storage: sessionStorage,  // 改用 sessionStorage
    pick: ['token'],          // 只持久化 token,不存 userInfo
    // omit: ['userInfo']     // 或排除某些字段
  }
})

6.5 多个存储策略示例

persist: [
  { pick: ['token'], storage: localStorage, key: 'auth-token' },
  { pick: ['theme', 'language'], storage: sessionStorage, key: 'ui-prefs' }
]

七、完整示例:用户 Store + 持久化 + 类型

// stores/user.ts
import { defineStore } from 'pinia'

interface UserInfo {
  id: number
  name: string
  avatar: string
}

interface UserState {
  token: string
  userInfo: UserInfo | null
}

export const useUserStore = defineStore('user', {
  state: (): UserState => ({
    token: '',
    userInfo: null
  }),
  getters: {
    isLoggedIn: (state) => !!state.token,
    userName: (state) => state.userInfo?.name ?? '游客'
  },
  actions: {
    setToken(token: string) {
      this.token = token
    },
    setUserInfo(info: UserInfo | null) {
      this.userInfo = info
    },
    logout() {
      this.token = ''
      this.userInfo = null
    }
  },
  persist: {
    key: 'user-store',
    pick: ['token', 'userInfo'],
    storage: localStorage
  }
})
<!-- 组件中使用 -->
<template>
  <div v-if="isLoggedIn">
    <span>欢迎,{{ userName }}</span>
    <button @click="logout">退出</button>
  </div>
</template>

<script setup lang="ts">
import { storeToRefs } from 'pinia'
import { useUserStore } from '@/stores/user'

const userStore = useUserStore()
const { isLoggedIn, userName } = storeToRefs(userStore)
const { logout } = userStore
</script>

八、常见踩坑小结

坑点现象正确做法
直接解构 state/getters数据不更新storeToRefs
actions 放进 storeToRefs调用时要 .value,类型不自然actions 直接从 store 解构
持久化存敏感数据被攻击者读取pick 只存必要字段,或考虑加密
在 setup 外调用 store报错或行为异常必须在 setup 或其它生命周期里调用
持久化插件选错功能残缺、兼容性问题pinia-plugin-persistedstate

九、小结

  1. 类型:给 state 定义接口,有利于维护和类型提示。
  2. 解构:state/getters 用 storeToRefs,actions 直接从 store 解构。
  3. 持久化:用 pinia-plugin-persistedstate,通过 persist 按需配置。

按上面三步做,在组件里使用 Pinia 会清晰、稳定,也更容易排查问题。

🔍 本系列专栏导航

一、《Vue状态管理扫盲篇:Vuex 到 Pinia | 为什么大家都在迁移?核心用法对比》

二、《Vue状态管理扫盲篇:如何设计一个合理的全局状态树 | 用户、权限、字典、布局配置》

三、《Vue状态管理扫盲篇:在组件中优雅地使用 Pinia | 类型提示、解构、持久化》

四、《Vue状态管理扫盲篇:状态管理中的常见坑 | 循环依赖、状态污染与调试技巧》

👉 跟着系列慢慢学,把技术功底扎扎实实地打牢~


学习本就是一场持久战,不需要急着一口吃成胖子。哪怕今天你只记住了一点点,这都是实打实的进步。

后续我还会继续用这种大白话、讲实战方式,带大家扫盲更多前端基础。

关注我,不迷路,咱们把那些曾经模糊的知识点,一个个彻底搞清楚。

如果你觉得这篇内容对你有帮助,不妨点赞+收藏,下次写代码卡壳时,拿出来翻一翻,比搜引擎更靠谱。

我是 Eugene,你的电子学友,我们下一篇干货见~