源本科技 | 码上会

Vue3 Pinia Store

2026/04/25
30
0

引言

什么是 Pinia Store

Store 是 Vue3 应用的 集中式状态容器,用来管理全局共享的数据(比如用户信息、计数、购物车)。

  • 独立:每个 Store 都是独立模块,互不干扰

  • 三要素:

    • State:存储数据(响应式)

    • Getters:计算属性(基于 State 派生数据,缓存结果)

    • Actions:修改 State 的方法(支持同步 / 异步)

核心 API

defineStore:Pinia 提供的唯一创建 Store 的方法


定义 Pinia Store

创建状态管理容器:src/store/counter.ts

方式1:选项式

更直观,类似 Vuex

import { defineStore } from 'pinia';

// 1. defineStore(唯一ID, 配置对象)
// 唯一ID:'counter' → 开发者工具中识别 Store 的标识
export const useCounterStore = defineStore('counter', {
    // 2. State:必须是函数!返回响应式数据(避免多实例数据污染)
    state: () => ({ 
        count: 0  // 初始状态:计数
    }),
    
    // 3. Getters:计算属性,基于 state 派生数据(自带缓存)
    getters: {
        doubleCount: (state) => state.count * 2
    },
    
    // 4. Actions:修改 state 的方法(可写业务逻辑)
    actions: {
        increment() {
            // this 指向当前 store 实例
            this.count++;
        }
    }
});

方式2:组合式

Vue3 推荐的方式;新手建议先用上面的选项式,代码看起来更直观

import { defineStore } from 'pinia';
// 导入 Vue 响应式 API
import { computed, ref } from 'vue';

export const useCounterStore = defineStore('counter', () => {
  // 1. 用 ref 定义 State(响应式数据)
  const count = ref(0);
  
  // 2. 用 computed 定义 Getters
  const doubleCount = computed(() => count.value * 2);
  
  // 3. 用普通函数定义 Actions
  const increment = () => {
    count.value++;
  };

  // 必须返回!外部组件才能访问这些数据/方法
  return { count, doubleCount, increment };
});

两种方式完全等价,组件中使用方式一致。


配置路由

创建路由文件,用于切换不同功能页面,文件:src/router/index.ts

import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router';

// 路由规则数组
const routes: Array<RouteRecordRaw> = [
    // 首页
    {
        path: '/',
        component: () => import('@/views/home/home.vue'),
        name: 'home'
    },
    // 基础使用 Store
    {
        path: '/store/use',
        component: () => import('@/views/store/use.vue'),
        name: 'use-store'
    },
    // Store 解构
    {
        path: '/store/deconstruction',
        component: () => import('@/views/store/deconstruction.vue'),
        name: 'deconstruction'
    },
    // 监听 State
    {
        path: '/store/listening',
        component: () => import('@/views/store/listening.vue'),
        name: 'listening'
    },
    // 修改 State
    {
        path: '/store/update',
        component: () => import('@/views/store/update.vue'),
        name: 'update'
    }
];

// 创建路由实例
const router = createRouter({
    history: createWebHistory(), // history 模式
    routes
});

export default router;

解释

  • 懒加载组件:() => import('...') → 优化页面性能

  • 路由对应我们后续要创建的 4 个功能页面


搭建全局布局

创建公共布局组件,包含顶部导航菜单,文件:src/components/AppLayout.vue

<template>
  <div class="app-layout">
    <!-- Element Plus 导航菜单 -->
    <el-menu :default-active="activeIndex" mode="horizontal" @select="handleSelect">
      <el-menu-item index="/">首页</el-menu-item>
      <!-- 子菜单:状态管理 -->
      <el-sub-menu index="/store">
        <template #title>状态管理</template>
        <el-menu-item index="/store/use">使用 Store</el-menu-item>
        <el-menu-item index="/store/deconstruction">解构 Store</el-menu-item>
        <el-menu-item index="/store/listening">监听 State</el-menu-item>
        <el-menu-item index="/store/update">修改 State</el-menu-item>
      </el-sub-menu>
    </el-menu>
    
    <!-- 页面内容容器:路由匹配的组件渲染在这里 -->
    <div class="content">
      <RouterView />
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { RouterView, useRouter } from 'vue-router'

const router = useRouter()
// 默认激活的菜单
const activeIndex = ref('/store')

// 菜单点击事件:跳转到对应路由
const handleSelect = (key: string) => {
  if (key !== '/store') {
    router.push(key)
  }
}
</script>

<style scoped>
.app-layout {
  width: 100%;
  min-height: 100vh;
}
.el-menu-demo {
  margin-bottom: 20px;
}
.content {
  padding: 20px;
}
</style>

解释

  • RouterView:路由组件出口,匹配的页面会渲染在这里

  • 菜单 index 对应路由的 path,实现点击跳转


修改根组件

使用我们创建的布局组件,文件:src/App.vue

<script setup lang="ts">
import AppLayout from './components/AppLayout.vue'
</script>

<template>
  <!-- 全局布局 -->
  <AppLayout />
</template>

解释:整个应用的根组件,只引入布局,实现统一页面结构。


创建首页组件

文件:src/views/home/home.vue

<template>
  <div class="home-page">
    <h1>欢迎使用 Pinia 状态管理示例</h1>
    <p>请从上方菜单选择要查看的功能</p>
  </div>
</template>

<style scoped>
.home-page { padding: 20px; }
</style>

基础使用 Store

第一个功能页面:直接使用 Store,文件:src/views/store/use.vue

<template>
  <h2>使用 Store</h2>
  <!-- 1. 直接在模板中使用 Store 的数据和计算属性 -->
  <div>原始计数:{{ counterStore.count }}</div>
  <div>双倍计数:{{ counterStore.doubleCount }}</div>
  <!-- 2. 调用 Store 的方法 -->
  <button @click="counterStore.increment">Increment</button>
</template>

<script setup lang="ts">
// 1. 导入定义好的 Store
import { useCounterStore } from '@/store/counter';

// 2. 创建 Store 实例(必须调用函数)
const counterStore = useCounterStore();
</script>

<style scoped>
button { margin: 10px; padding: 10px; background: #4CAF50; color: white; }
</style>

核心知识点

  • 组件中使用 Store:先导入 → 调用函数获取实例 → 直接使用

  • 模板中可直接访问 state/getters,调用 actions

  • 数据是响应式的,修改后页面自动更新


Store 解构

解决响应式丢失

问题

Store 是 reactive 包裹的对象,直接 ES6 解构会失去响应性

// ❌ 错误写法:解构后 count 不是响应式
const { count } = counterStore

storeToRefs

文件:src/views/store/deconstruction.vue

<template>
  <h2>解构 Store</h2>
  <!-- 解构后直接使用变量,更简洁 -->
  <div>{{ count }}</div>
  <div>{{ doubleCount }}</div>
  <button @click="increment">Increment</button>
</template>

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

const counterStore = useCounterStore();

// ✅ 正确:storeToRefs 把 state/getters 转为响应式 ref
const { count, doubleCount } = storeToRefs(counterStore);

// ⚠️ 注意:actions 不需要解构,直接调用
const increment = () => {
  counterStore.increment();
};
</script>

解释

  • storeToRefs:专门用于解构 Store,保留响应式

  • 只解构 state/gettersactions 直接用原实例调用


批量修改 State

修改 State 有 3 种方式,$patch 批量修改性能最高

文件:src/views/store/update.vue

<template>
  <div>{{ counterStore.count }}</div>
  <button @click="updateState">Update State</button>
</template>

<script setup lang="ts">
import { useCounterStore } from '@/store/counter';
const counterStore = useCounterStore();

const updateState = () => {
  // 方式1:$patch 传入对象(批量修改简单数据)
  counterStore.$patch({
    count: counterStore.count + 1
  });

  // 方式2:$patch 传入函数(推荐!修改数组/对象等高代价操作)
  // counterStore.$patch((state) => {
  //   state.count++;
  // });
};
</script>

对比

  1. 直接修改:counterStore.count++ → 简单但多次修改性能差

  2. $patch 对象:适合修改基础类型

  3. $patch 函数:适合修改数组 / 对象(无需创建新集合,性能最优)


监听 Store 状态变化

使用 $subscribe 监听 State 变化,比 watch 更高效(多状态修改只触发一次)。

文件:src/views/store/listening.vue

<template>
  <div>{{ counterStore.count }}</div>
  <button @click="counterStore.increment">Increment</button>
</template>

<script setup lang="ts">
import { useCounterStore } from '@/store/counter';
const counterStore = useCounterStore();

// 监听当前 Store 的所有状态变化
counterStore.$subscribe((mutation, state) => {
  // mutation:变化信息(类型、修改内容)
  console.log('变化类型:', mutation.type);
  // state:最新的状态数据
  console.log('最新状态:', state);
});
</script>

解释

  • mutation.typedirect / patch / action

  • 适合日志记录、状态同步等场景


全局监听 + 状态持久化

main.ts监听所有 Store,并把状态存到本地存储(刷新页面不丢失)。

// 在 main.ts 原有代码基础上添加
import { watch } from 'vue';

// 监听 Pinia 所有状态变化
watch(
    pinia.state, // 监听根状态
    (state) => {
      // 持久化到 localStorage
      localStorage.setItem('piniaState', JSON.stringify(state));
    },
    { deep: true } // 深度监听
);

补充:ES6 解构语法

因为 Store 解构用到了 ES6 对象解构,这里补充核心用法:

对象解构(最常用)

const obj = { name: '张三', age: 18 };
// 从对象中提取属性赋值给变量
const { name, age } = obj; 

重命名解构

const { name: userName } = obj; 
// userName = '张三'

函数参数解构

function fn({ count }) { console.log(count) }
fn(counterStore)

顺序总结

  1. 安装并挂载 Pinia(入口文件)

  2. 定义 Store(State/Getters/Actions)

  3. 配置路由 + 布局

  4. 基础使用 Store(组件中直接调用)

  5. 解构 Store(storeToRefs 保留响应式)

  6. 批量修改 State($patch 优化性能)

  7. 监听 State($subscribe / 全局监听)

  8. 状态持久化