1.前言
vue3设计的状态管理库,管理应用中的状态。
核心概念
Store(存储):类似于Vuex中的模块,更轻量不强制要求单一状态树,可创建多个Store。
**State(状态):**定义应用状态数据,与Vuex不同的是,Pinia的state可以是一个函数返回对象。
**Getters(获取器):**从state中派生出一些状态,可以看作store中的计算属性,具有缓存功能。
**Actions(动作):**执行异步操作或修改状态。与Vuex不同的是Pinia的actions允许直接访问和修改state,而不是像Vuex需要通过mutations.
2.基础使用示例
安装Pinia
javascript
# npm
npm install pinia
# yarn
yarn add pinia
# pnpm
pnpm add pinia
创建并注册Pinia
在main.js(main.ts)中创建Pinia实例,并通过app.use()注册
javascript
// main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia' // 引入 createPinia
import App from './App.vue'
const app = createApp(App)
const pinia = createPinia() // 创建 Pinia 实例
app.use(pinia) // 注册到 Vue 应用
app.mount('#app')
定义store
通常在src/stores/目录下创建Store文件,counter.ts
javascript
// src/stores/counter.ts
import { defineStore } from 'pinia'
// 使用 defineStore 定义一个 Store
// 第一个参数是 Store 的唯一 ID(用于 DevTools 和持久化)
export const useCounterStore = defineStore('counter', {
// 🔹 state:必须是一个函数,返回初始状态对象
state: () => ({
count: 0,
name: 'Alice'
}),
// 🔹 getters:类似 computed,用于派生状态(可选)
getters: {
// getter 接收 state 作为参数
doubleCount: (state) => state.count * 2,
// getter 也可以使用其他 getter(通过 this)
doubleCountPlusOne(): number {
return this.doubleCount + 1
}
},
// 🔹 actions:用于修改状态或执行异步操作(可选)
actions: {
// 同步 action
increment() {
this.count++ // 直接修改 state(无需 mutations!)
},
// 带参数的 action
incrementBy(amount: number) {
this.count += amount
},
// 异步 action
async fetchData() {
try {
const res = await fetch('/api/data')
const data = await res.json()
this.name = data.name // 异步中也可直接修改 state
} catch (error) {
console.error('Failed to fetch data:', error)
}
}
}
})
在组件中使用Store
组件中调用Store并使用其状态/方法
javascript
<!-- src/components/Counter.vue -->
<template>
<div>
<p>Count: {{ counterStore.count }}</p>
<p>Double: {{ counterStore.doubleCount }}</p>
<p>Name: {{ counterStore.name }}</p>
<!-- 调用 actions -->
<button @click="counterStore.increment">+1</button>
<button @click="incrementBy5">+5</button>
<button @click="loadData">加载数据</button>
</div>
</template>
<script setup lang="ts">
// 导入定义好的 Store
import { useCounterStore } from '@/stores/counter'
// 调用 Store 函数,获取响应式实例
const counterStore = useCounterStore()
// 自定义方法(可选)
function incrementBy5() {
counterStore.incrementBy(5)
}
async function loadData() {
await counterStore.fetchData()
}
</script>
3.启用持久化(保存到localStorage)
刷新页面后,状态不丢失,可使用pinia-plugin-persistedstate插件
安装插件
javascript
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 app = createApp(App)
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate) // 注册插件
app.use(pinia)
app.mount('#app')
在store中启用持久化
javascript
// stores/counter.ts
export const useCounterStore = defineStore('counter', {
state: () => ({ count: 0, name: 'Alice' }),
// ...getters, actions...
// 启用持久化
persist: true // 默认保存全部 state 到 localStorage
// 或精细控制:
// persist: {
// key: 'my-counter',
// paths: ['count'] // 只持久化 count 字段
// }
})
常用操作
| 操作 | 代码示例 |
|---|---|
| 批量更新 state | store.$patch({ count: 10, name: 'Bob' }) |
| 函数式批量更新 | store.$patch(state => { state.count++; }) |
| 重置为初始状态 | store.$reset() |
| 监听 store 变化 | watch(() => store.count, (newVal) => { ... }) |
| 在非 setup 中使用 | const store = useCounterStore()(需在 setup 或生命周期内调用) |
4.应用场景
1.大屏状态集中管理(面板折叠,主题切换)
具体开发可视化大屏项目中:控制左右侧面板的展开折叠,切换深色与浅色主题,可持久化刷新后不丢失

状态Store
javascript
// src/stores/ui.ts
import { defineStore } from 'pinia'
/**
* 定义 UI 全局状态 Store
* 用于管理:主题、布局、面板折叠等 UI 行为
*/
export const useUiStore = defineStore('ui', {
/**
* 初始状态
* 注意:state 必须是一个函数,返回对象
*/
state: () => ({
// 左侧面板(如侧边导航)是否折叠
sidebarCollapsed: false,
// 右侧面板(如属性配置区)是否折叠
rightPanelCollapsed: false,
// 是否启用深色模式(dark mode)
isDark: false,
// 布局模式:'default' | 'compact' | 'fullscreen'
layoutMode: 'default' as 'default' | 'compact' | 'fullscreen',
// 是否显示全局加载遮罩
globalLoading: false
}),
/**
* 计算属性(可选)
* 派生状态,具有缓存特性
*/
getters: {
// 返回侧边栏的实际宽度(用于动态计算布局)
sidebarWidth(): string {
return this.sidebarCollapsed ? '60px' : '240px'
},
// 返回当前主题类名
themeClass(): string {
return this.isDark ? 'dark' : 'light'
}
},
/**
* 操作方法(同步或异步)
*/
actions: {
/**
* 切换左侧面板折叠状态
*/
toggleSidebar() {
this.sidebarCollapsed = !this.sidebarCollapsed
},
/**
* 切换右侧面板折叠状态
*/
toggleRightPanel() {
this.rightPanelCollapsed = !this.rightPanelCollapsed
},
/**
* 切换深色/浅色主题
* 同时操作 <html> 标签的 class,使 CSS 生效
*/
toggleTheme() {
this.isDark = !this.isDark
// 动态添加/移除 'dark' 类到 html 根元素
// 配合 Tailwind CSS 或自定义 CSS 实现主题切换
if (this.isDark) {
document.documentElement.classList.add('dark')
} else {
document.documentElement.classList.remove('dark')
}
// 可选:触发自定义事件供第三方库监听
// window.dispatchEvent(new CustomEvent('theme-change', { detail: this.isDark }))
},
/**
* 设置布局模式
*/
setLayoutMode(mode: 'default' | 'compact' | 'fullscreen') {
this.layoutMode = mode
},
/**
* 显示/隐藏全局加载状态
*/
setGlobalLoading(loading: boolean) {
this.globalLoading = loading
}
}
})
布局容器
javascript
<!-- src/components/Layout.vue -->
<template>
<div class="app-layout">
<!-- 顶部栏 -->
<Header />
<div class="main-content">
<!-- 左侧面板(侧边栏) -->
<Sidebar
:collapsed="uiStore.sidebarCollapsed"
:width="uiStore.sidebarWidth"
/>
<!-- 主内容区 -->
<main
class="content-area"
:class="{ 'full-width': uiStore.layoutMode === 'fullscreen' }"
>
<slot />
</main>
<!-- 右侧面板(可选) -->
<div
v-if="!uiStore.rightPanelCollapsed"
class="right-panel"
>
<h3>属性面板</h3>
<!-- 配置项内容 -->
</div>
</div>
<!-- 全局加载遮罩 -->
<div v-if="uiStore.globalLoading" class="global-loading-mask">
加载中...
</div>
</div>
</template>
<script setup lang="ts">
import { useUiStore } from '@/stores/ui'
import Header from './Header.vue'
import Sidebar from './Sidebar.vue'
// 获取 UI 状态实例
const uiStore = useUiStore()
</script>
<style scoped>
.app-layout {
display: flex;
flex-direction: column;
height: 100vh;
}
.main-content {
display: flex;
flex: 1;
overflow: hidden;
}
.content-area {
flex: 1;
padding: 16px;
transition: margin 0.3s ease;
}
.content-area.full-width {
margin-left: 0 !important;
}
.right-panel {
width: 300px;
background: #f5f5f5;
border-left: 1px solid #ddd;
padding: 16px;
}
.global-loading-mask {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.5);
color: white;
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
}
</style>
顶部组件
javascript
<!-- src/components/Header.vue -->
<template>
<header class="app-header">
<!-- 折叠侧边栏按钮 -->
<button @click="uiStore.toggleSidebar" class="icon-btn">
☰
</button>
<!-- 切换右侧面板 -->
<button @click="uiStore.toggleRightPanel" class="icon-btn">
右侧 {{ uiStore.rightPanelCollapsed ? '展开' : '收起' }}
</button>
<!-- 主题切换按钮 -->
<button @click="uiStore.toggleTheme" class="theme-toggle">
{{ uiStore.isDark ? '☀️ 浅色' : '🌙 深色' }}
</button>
</header>
</template>
<script setup lang="ts">
import { useUiStore } from '@/stores/ui'
const uiStore = useUiStore()
</script>
<style scoped>
.app-header {
height: 60px;
background: #fff;
border-bottom: 1px solid #eee;
display: flex;
align-items: center;
padding: 0 16px;
}
.icon-btn, .theme-toggle {
margin-right: 12px;
padding: 6px 12px;
border: 1px solid #ccc;
border-radius: 4px;
cursor: pointer;
}
/* 深色模式下的样式 */
.dark .app-header {
background: #1f2937;
color: white;
border-color: #4b5563;
}
</style>
侧边栏组件
javascript
<!-- src/components/Sidebar.vue -->
<template>
<aside
class="sidebar"
:style="{ width: width }"
:class="{ collapsed: collapsed }"
>
<nav>
<ul>
<li>首页</li>
<li>数据看板</li>
<li>系统设置</li>
</ul>
</nav>
</aside>
</template>
<script setup lang="ts">
defineProps<{
collapsed: boolean
width: string
}>()
</script>
<style scoped>
.sidebar {
height: calc(100vh - 60px);
background: #2d3748;
color: white;
transition: width 0.3s ease;
overflow: hidden;
}
.sidebar.collapsed {
width: 60px;
}
.sidebar ul {
list-style: none;
padding: 0;
}
.sidebar li {
padding: 12px 16px;
cursor: pointer;
}
.sidebar li:hover {
background: #4a5568;
}
</style>
持久化引入
安装pinia-plugin-persistedstate插件,main.js(main.ts)中注册
在状态Store中进行启用
javascript
export const useUiStore = defineStore('ui', {
// ...state, getters, actions...
persist: {
key: 'ui-preferences',
paths: ['sidebarCollapsed', 'isDark', 'layoutMode'] // 只保存这些字段
}
})
2.跨组件共享状态(登录用户信息与登录状态)
用户登录后,header组件显示用户名,sidebar显示头像,profile页面显示详细信息;
登出后,所有组件自动清空用户数据并跳转到登录页;
刷新页面后自动恢复登录状态(通过token)
对应文件结构

用户状态store
javascript
// src/stores/user.ts
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { useRouter } from 'vue-router'
// 定义用户数据结构(TypeScript 接口)
export interface User {
id: string
name: string
email: string
avatar: string
role: 'admin' | 'user'
}
/**
* 用户状态管理 Store
* 负责:登录、登出、用户信息缓存、自动恢复登录态
*/
export const useUserStore = defineStore('user', {
/**
* 初始状态
* 注意:state 必须是函数,确保多实例隔离
*/
state: () => ({
// 当前用户信息(null 表示未登录)
userInfo: null as User | null,
// 登录状态标志(可由 userInfo 推导,但显式声明更清晰)
isLoggedIn: false,
// 登录加载状态(用于按钮禁用)
loggingIn: false
}),
/**
* 派生状态(计算属性)
*/
getters: {
// 获取用户姓名(安全访问)
userName: (state) => state.userInfo?.name || '游客',
// 判断是否为管理员
isAdmin: (state) => state.userInfo?.role === 'admin',
// 获取头像 URL(提供默认图)
avatarUrl: (state) =>
state.userInfo?.avatar || 'https://via.placeholder.com/40'
},
/**
* 业务操作方法
*/
actions: {
/**
* 用户登录
* @param credentials - { username, password }
*/
async login(credentials: { username: string; password: string }) {
this.loggingIn = true
try {
// 👇 实际项目中替换为真实 API 调用
// const response = await api.post('/auth/login', credentials)
// const { user, token } = response.data
// 模拟 API 响应(开发阶段)
await new Promise((resolve) => setTimeout(resolve, 800))
const mockUser: User = {
id: '1',
name: '张三',
email: 'zhangsan@example.com',
avatar: '/avatars/zhangsan.jpg',
role: 'admin'
}
const mockToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'
// 保存用户信息到状态
this.userInfo = mockUser
this.isLoggedIn = true
// 保存 token 到 localStorage(用于刷新后自动登录)
localStorage.setItem('auth-token', mockToken)
// 可选:通知其他系统(如 Sentry 设置用户上下文)
// Sentry.setUser({ id: mockUser.id, email: mockUser.email })
} catch (error) {
console.error('登录失败:', error)
throw error // 向上抛出错误,供组件处理
} finally {
this.loggingIn = false
}
},
/**
* 用户登出
* 清空状态 + 移除 token + 跳转到登录页
*/
logout() {
// 清空用户状态
this.userInfo = null
this.isLoggedIn = false
// 移除本地存储的 token
localStorage.removeItem('auth-token')
// 跳转到登录页(使用 Vue Router)
const router = useRouter()
router.push('/login')
},
/**
* 尝试从 localStorage 恢复登录状态(页面初始化时调用)
* 通常在 App.vue 或路由守卫中调用
*/
async restoreAuth() {
const token = localStorage.getItem('auth-token')
if (!token) return
try {
// 验证 token 是否有效(可选:调用 /auth/me 接口)
// const response = await api.get('/auth/me', { headers: { Authorization: `Bearer ${token}` } })
// this.userInfo = response.data.user
// this.isLoggedIn = true
// 模拟:假设 token 有效,恢复用户信息
this.userInfo = {
id: '1',
name: '张三',
email: 'zhangsan@example.com',
avatar: '/avatars/zhangsan.jpg',
role: 'admin'
}
this.isLoggedIn = true
} catch (error) {
// token 无效,清除并重置
console.warn('Token 无效,清除登录状态')
localStorage.removeItem('auth-token')
this.userInfo = null
this.isLoggedIn = false
}
}
}
})
顶部组件显示用户名与登出
javascript
<!-- src/components/Header.vue -->
<template>
<header class="app-header">
<div class="user-info" v-if="userStore.isLoggedIn">
<img :src="userStore.avatarUrl" alt="头像" class="avatar" />
<span>欢迎,{{ userStore.userName }}</span>
<button @click="handleLogout" :disabled="logoutLoading">
{{ logoutLoading ? '退出中...' : '登出' }}
</button>
</div>
<div v-else>
<router-link to="/login">请登录</router-link>
</div>
</header>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
const logoutLoading = ref(false)
async function handleLogout() {
logoutLoading.value = true
try {
await userStore.logout() // logout 内部会跳转
} finally {
logoutLoading.value = false
}
}
</script>
<style scoped>
.app-header {
height: 60px;
background: #fff;
border-bottom: 1px solid #eee;
display: flex;
align-items: center;
padding: 0 20px;
}
.user-info {
display: flex;
align-items: center;
gap: 12px;
}
.avatar {
width: 32px;
height: 32px;
border-radius: 50%;
object-fit: cover;
}
</style>
侧边组件显示头像与角色
javascript
<!-- src/components/Sidebar.vue -->
<template>
<aside class="sidebar">
<div class="user-card" v-if="userStore.isLoggedIn">
<img :src="userStore.avatarUrl" alt="头像" />
<div>
<p>{{ userStore.userName }}</p>
<small>{{ userStore.isAdmin ? '管理员' : '普通用户' }}</small>
</div>
</div>
<nav v-if="userStore.isLoggedIn">
<ul>
<li><router-link to="/dashboard">仪表盘</router-link></li>
<li><router-link to="/profile">个人资料</router-link></li>
<li v-if="userStore.isAdmin">
<router-link to="/admin">系统管理</router-link>
</li>
</ul>
</nav>
</aside>
</template>
<script setup lang="ts">
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
</script>
登录组件
javascript
<!-- src/views/Login.vue -->
<template>
<div class="login-page">
<form @submit.prevent="handleSubmit">
<h2>用户登录</h2>
<input v-model="form.username" placeholder="用户名" required />
<input v-model="form.password" type="password" placeholder="密码" required />
<button type="submit" :disabled="userStore.loggingIn">
{{ userStore.loggingIn ? '登录中...' : '登录' }}
</button>
<p v-if="error" class="error">{{ error }}</p>
</form>
</div>
</template>
<script setup lang="ts">
import { reactive, ref } from 'vue'
import { useUserStore } from '@/stores/user'
import { useRouter } from 'vue-router'
const userStore = useUserStore()
const router = useRouter()
const form = reactive({ username: '', password: '' })
const error = ref('')
async function handleSubmit() {
error.value = ''
try {
await userStore.login(form)
// 登录成功后,userStore 会自动更新状态
// 跳转已在 login action 中处理,或可在此处跳转
router.push('/dashboard')
} catch (err) {
error.value = '用户名或密码错误'
}
}
</script>
应用根组件(初始化恢复登录状态)
javascript
<!-- src/App.vue -->
<template>
<div id="app">
<Header v-if="!isLoginPage" />
<router-view />
</div>
</template>
<script setup lang="ts">
import { onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { useUserStore } from '@/stores/user'
import Header from '@/components/Header.vue'
const route = useRoute()
const userStore = useUserStore()
// 判断当前是否为登录页
const isLoginPage = computed(() => route.path === '/login')
// 页面加载时尝试恢复登录状态
onMounted(() => {
if (!isLoginPage.value) {
userStore.restoreAuth()
}
})
</script>
5.Pinia与Vuex明显的区别
Vuex必须通过mutations同步,在actions中commit进行修改。
javascript
// store.js
mutations: {
SET_COUNT(state, value) {
state.count = value
}
},
actions: {
increment({ commit }) {
commit('SET_COUNT', this.state.count + 1) // 必须 commit
}
}
Pinia直接在actions中进行状态修改
javascript
// stores/counter.ts
actions: {
increment() {
this.count++
}
}