vue3-Pinia Vue3状态管理库

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++ 
  }
}
相关推荐
be or not to be19 小时前
深入理解 CSS 浮动布局(float)
前端·css
LYFlied19 小时前
【每日算法】LeetCode 1143. 最长公共子序列
前端·算法·leetcode·职场和发展·动态规划
老华带你飞20 小时前
农产品销售管理|基于java + vue农产品销售管理系统(源码+数据库+文档)
java·开发语言·前端·数据库·vue.js·spring boot·后端
小徐_233320 小时前
2025 前端开源三年,npm 发包卡我半天
前端·npm·github
C_心欲无痕20 小时前
vue3 - 类与样式的绑定
javascript·vue.js·vue3
GIS之路20 小时前
GIS 数据转换:使用 GDAL 将 Shp 转换为 GeoJSON 数据
前端
JIngJaneIL21 小时前
基于springboot + vue房屋租赁管理系统(源码+数据库+文档)
java·开发语言·前端·数据库·vue.js·spring boot·后端
天天扭码21 小时前
以浏览器多进程的角度解构页面渲染的整个流程
前端·面试·浏览器
你们瞎搞21 小时前
Cesium加载20GB航测影像.tif
前端·cesium·gdal·地图切片
南山安21 小时前
Tailwind CSS:顺风CSS
javascript·css·react.js