同学们好,我是 Eugene(尤金),一个拥有多年中后台开发经验的前端工程师~
(Eugene 发音很简单,/juːˈdʒiːn/,大家怎么顺口怎么叫就好)
你是否也有过:明明学过很多技术,一到关键时候却讲不出来、甚至写不出来?
你是否也曾怀疑自己,是不是太笨了,明明感觉会,却总差一口气?
就算想沉下心从头梳理,可工作那么忙,回家还要陪伴家人。
一天只有24小时,时间永远不够用,常常感到力不从心。
技术行业,本就是逆水行舟,不进则退。
如果你也有同样的困扰,别慌。
从现在开始,跟着我一起心态归零,利用碎片时间,来一次彻彻底底的基础扫盲。
这一次,我们一起慢慢来,扎扎实实变强。
不搞花里胡哨的理论堆砌,只分享看得懂、用得上的前端干货,
咱们一起稳步积累,真正摆脱“面向搜索引擎写代码”的尴尬。
面向会写 Vue/JS 但概念略混、或想把基础打牢的开发者,讲「怎么选、为什么选、踩坑在哪」。
一、先搞清楚:Pinia 是什么,为什么要用?
Pinia 是 Vue 3 官方推荐的状态管理库,取代 Vuex。它的作用是:
- 多个组件共享同一份数据
- 数据更新后,所有用到的地方都能自动更新
如果只是父子组件传值,用 props 和 emit 就够了;当多个不相关的组件需要同一份数据时,就需要 Pinia 这类全局状态管理。
二、安装与基础配置
npm install pinia
在 main.js 或 main.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>
问题:count 和 doubleCount 会失去响应式。点击按钮后,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 |
九、小结
- 类型:给 state 定义接口,有利于维护和类型提示。
- 解构:state/getters 用
storeToRefs,actions 直接从 store 解构。 - 持久化:用
pinia-plugin-persistedstate,通过persist按需配置。
按上面三步做,在组件里使用 Pinia 会清晰、稳定,也更容易排查问题。
🔍 本系列专栏导航
一、《Vue状态管理扫盲篇:Vuex 到 Pinia | 为什么大家都在迁移?核心用法对比》
二、《Vue状态管理扫盲篇:如何设计一个合理的全局状态树 | 用户、权限、字典、布局配置》
三、《Vue状态管理扫盲篇:在组件中优雅地使用 Pinia | 类型提示、解构、持久化》
四、《Vue状态管理扫盲篇:状态管理中的常见坑 | 循环依赖、状态污染与调试技巧》
👉 跟着系列慢慢学,把技术功底扎扎实实地打牢~
学习本就是一场持久战,不需要急着一口吃成胖子。哪怕今天你只记住了一点点,这都是实打实的进步。
后续我还会继续用这种大白话、讲实战方式,带大家扫盲更多前端基础。
关注我,不迷路,咱们把那些曾经模糊的知识点,一个个彻底搞清楚。
如果你觉得这篇内容对你有帮助,不妨点赞+收藏,下次写代码卡壳时,拿出来翻一翻,比搜引擎更靠谱。
我是 Eugene,你的电子学友,我们下一篇干货见~