🎯 你正在阅读「【专栏名字】」系列文章 🎯
🔥 弹简特 个人主页
❄️ 个人专栏直通车:
- 🔌 接口测试从入门到跑路
- ☕ 一个后端的 JavaEE 续命指南
- 🛜 网络原理续命手册
- ☕ Java项目-轻聊
- ☕ Java项目-企悦抽
✨ 靠热爱去书写自己,靠勇敢去书写生活!
🌟 博主简介:

文章目录:
- 一、前言
- 二、案例
- 三、详细解释
-
- [0. 咱要把什么做出来?](#0. 咱要把什么做出来?)
-
-
- [📖 路线图说明](#📖 路线图说明)
-
- [第 0 步:先搞懂「为什么要 Pinia」](#第 0 步:先搞懂「为什么要 Pinia」)
-
-
- [📖 说明](#📖 说明)
-
- [第 1 步:搭项目 + 装依赖](#第 1 步:搭项目 + 装依赖)
-
- 这步做什么?
- [写什么代码 / 命令?](#写什么代码 / 命令?)
- 写了就有了什么功能?
- [第 2 步:在 main.js 挂载 Pinia(应用才"开仓库大门")](#第 2 步:在 main.js 挂载 Pinia(应用才“开仓库大门”))
-
- 这步做什么?
- 写什么代码?
- 写了就有了什么功能?
-
- [📖 说明](#📖 说明)
- [第 3 步:定义 Store(中央仓库长什么样)](#第 3 步:定义 Store(中央仓库长什么样))
-
- 这步做什么?
- 类比
- 写什么代码?
- [① state → 公共数据源(相当于组件`data()`)](#① state → 公共数据源(相当于组件
data())) - [② getters → 计算属性(相当于组件`computed`)](#② getters → 计算属性(相当于组件
computed)) - [③ actions → 业务方法(相当于组件`methods`)](#③ actions → 业务方法(相当于组件
methods)) - 写了就有了什么功能?
-
- [📖 说明](#📖 说明)
- [第 4 步:写 Operate.vue(操作仓库)](#第 4 步:写 Operate.vue(操作仓库))
- [第 5 步:写 List.vue(展示同一份仓库)](#第 5 步:写 List.vue(展示同一份仓库))
-
- 这步做什么?
- 写什么代码?
- 写了就有了什么功能?
-
- [📖 说明](#📖 说明)
- [第 6 步:配路由,让两个页能切换](#第 6 步:配路由,让两个页能切换)
- [原理:Store 在内存里怎么存?](#原理:Store 在内存里怎么存?)
- 梳理
- 四、运行效果
一、前言
大家常说的状态管理,说白了本质就是数据管理。名词听上去很高大上,实际上理解起来并不复杂。本期我们就借助实战案例,带大家通俗易懂地认识并学习 Pinia。
二、案例
1、项目结构

2、index.html和跟根组件
index.html
html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>vue-demo06</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
根组件
html
<!--
根组件:只负责导航链接 + 路由出口
业务数据不放这里,统一走 Pinia Store
-->
<script setup>
// 不需要在这里 import Operate / List
// 它们由 router.js 按路径自动匹配到 <router-view>
</script>
<template>
<div class="app-root">
<nav class="nav">
<!-- 路由链接:点击切换页面,不会整页刷新,Pinia 数据通常还在 -->
<router-link to="/operate">去操作页</router-link>
<span class="sep">|</span>
<router-link to="/list">去列表页</router-link>
</nav>
<!--
路由出口(显示屏):
/operate → Operate.vue
/list → List.vue
-->
<router-view />
</div>
</template>
<style scoped>
.app-root {
padding: 16px;
max-width: 640px;
margin: 0 auto;
text-align: left;
}
.nav {
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid #ddd;
}
.sep {
margin: 0 8px;
color: #999;
}
/* 当前激活的路由链接高亮 */
.router-link-active {
font-weight: bold;
color: #42b883;
}
</style>
3、程序入口文件
js
/**
* 应用入口
* 顺序:创建应用 → 挂载 Pinia(开仓库)→ 挂载路由 → 挂到页面
* 漏掉 app.use(pinia) 时,组件里一用 store 就会报错
*/
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
// 从 pinia 包导入(注意:不是从 store/store.js 导入)
import { createPinia } from 'pinia'
// 路由:用来切换 Operate / List 两个页面
import router from './routers/router.js'
const app = createApp(App) // ① 创建 Vue 应用
const pinia = createPinia() // ② 创建 Pinia 容器(类似 IOC,管理所有 store)
app.use(pinia) // ③ 正式启用状态管理------之后才能 definedPerson()
app.use(router) // ④ 启用路由
app.mount('#app') // ⑤ 挂到 index.html 的 #app
4、状态配置文件
js
/**
* Pinia Store:人的共享状态仓库
*
* 类比 Java 实体类:
* id → 类名(必须全局唯一)
* state → 属性(存数据)
* getters → get 方法(只读计算,一般不改数据)
* actions → 业务方法(可以改数据)
*
* 用法:组件里 const person = definedPerson()
* 第一次调用创建实例,之后都返回同一个单例 → 多组件共享
*/
import { defineStore } from 'pinia'
// 第一个参数 'personPinia':Store 的唯一 ID,不能和别的 store 撞名
export const definedPerson = defineStore('personPinia', {
// ---------- state:存数据 ----------
// 推荐箭头函数返回对象;本身已是响应式,组件里不用再包 ref,也不用 .value
state: () => ({
username: '张三', // 姓名
age: 0, // 年龄
hobbies: ['唱歌', '跳舞'], // 爱好列表
}),
// ---------- getters:只读计算(类似 computed)----------
// 在组件里当属性用:person.getAge、person.getHobbiesCount
getters: {
// 爱好个数
getHobbiesCount() {
return this.hobbies.length
},
// 取年龄(演示 getter 用法)
getAge() {
return this.age
},
},
// ---------- actions:真正改数据的业务方法 ----------
actions: {
// 年龄翻倍
doubleAge() {
this.age = this.age * 2
},
},
})
5、路由配置文件
js
/**
* 路由配置:门牌对照表
* /operate → 操作页(改 Pinia 数据)
* /list → 列表页(读同一份 Pinia 数据)
*/
import { createRouter, createWebHashHistory } from 'vue-router'
import Operate from '../components/Operate.vue'
import List from '../components/List.vue'
const router = createRouter({
// Hash 模式:地址带 #,如 http://localhost:5173/#/operate
history: createWebHashHistory(),
routes: [
{
path: '/',
redirect: '/operate', // 默认进操作页
},
{
path: '/operate',
component: Operate, // 遥控器:改仓库
},
{
path: '/list',
component: List, // 显示屏:读仓库
},
],
})
export default router
6、两个组件
操作页
html
<!--
操作页 Operate:遥控器
职责:修改 Pinia 中央仓库里的数据
改完后,List 页会自动同步(因为是同一个 store 单例)
-->
<script setup>
// 导入 store 定义(具名导出)
import { definedPerson } from '../store/store.js'
/**
* 调用 definedPerson() → 拿到 Store 实例
* - 第一次:创建单例
* - 之后(包括 List 页):返回同一个对象
*/
const person = definedPerson()
/**
* $patch:批量修改多个字段(Pinia 内置)
* 比一个个赋值更适合"一次改多项"
*/
const patchDemo = () => {
person.$patch({
username: '奥特曼',
age: 100,
})
}
/**
* $reset:恢复成 store 里 state 的初始值(Pinia 内置)
* 即回到:张三 / 0 / ['唱歌','跳舞']
*/
const resetDemo = () => {
person.$reset()
}
</script>
<template>
<div class="operate-page">
<h3>操作页 Operate</h3>
<p class="tip">在这里改数据 → 切到「列表页」能看见同一份结果</p>
<!-- v-model 直接绑 store 字段:改输入框 = 改中央仓库 -->
<div class="row">
<label for="username">姓名:</label>
<input id="username" type="text" v-model="person.username" />
</div>
<div class="row">
<label for="age">年龄:</label>
<!-- v-model.number:保证 age 是数字,方便 doubleAge 计算 -->
<input id="age" type="number" v-model.number="person.age" />
</div>
<!--
复选框绑到数组:
勾选 → 往 hobbies 添加对应 value
取消 → 从 hobbies 移除
不是整段字符串替换
-->
<div class="row">
<span>爱好:</span>
<label>
<input type="checkbox" value="唱歌" v-model="person.hobbies" />
唱歌
</label>
<label>
<input type="checkbox" value="跳舞" v-model="person.hobbies" />
跳舞
</label>
<label>
<input type="checkbox" value="打球" v-model="person.hobbies" />
打球
</label>
</div>
<div class="actions">
<!-- 调用 store 的 actions -->
<button type="button" @click="person.doubleAge()">年龄 × 2</button>
<button type="button" @click="resetDemo">$reset 重置</button>
<button type="button" @click="patchDemo">$patch 批量改</button>
</div>
<!-- 调试:把整个 store 当前状态打出来看 -->
<h4>当前 store 快照</h4>
<pre class="debug">{{ person }}</pre>
</div>
</template>
<style scoped>
.operate-page {
text-align: left;
}
.tip {
color: #666;
font-size: 14px;
}
.row {
margin: 10px 0;
}
.row label {
margin-right: 8px;
}
.actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin: 16px 0;
}
.debug {
background: #f6f8fa;
padding: 12px;
border-radius: 6px;
overflow: auto;
font-size: 13px;
}
</style>
列表页
html
<!--
列表页 List:显示屏
职责:只读展示 Pinia 仓库数据
与 Operate 调用同一个 definedPerson() → 共享同一单例
-->
<script setup>
import { definedPerson } from '../store/store.js'
// 关键:和 Operate.vue 调的是同一个 defineStore
// Pinia 单例机制 → 拿到的是同一对象,所以那边改、这边自动变
const person = definedPerson()
</script>
<template>
<div class="list-page">
<h3>列表页 List</h3>
<p class="tip">数据来自中央仓库;去操作页修改后回到这里应已同步</p>
<!-- 直接读 state -->
<p>姓名:{{ person.username }}</p>
<p>年龄(state):{{ person.age }}</p>
<!-- getters 当属性用,不用加括号 -->
<p>年龄(getter getAge):{{ person.getAge }}</p>
<p>爱好个数(getter):{{ person.getHobbiesCount }}</p>
<h4>爱好列表</h4>
<ul>
<!-- v-for 遍历 state 里的数组 -->
<li v-for="(item, index) in person.hobbies" :key="index">
{{ item }}
</li>
</ul>
<!-- 爱好为空时的提示 -->
<p v-if="person.hobbies.length === 0" class="empty">暂无爱好</p>
</div>
</template>
<style scoped>
.list-page {
text-align: left;
}
.tip {
color: #666;
font-size: 14px;
}
.empty {
color: #999;
}
ul {
padding-left: 20px;
}
</style>
三、详细解释
0. 咱要把什么做出来?
目标成品:
- 一个中央数据仓库(Store):存姓名、年龄、爱好
- 一个「操作页」改数据
- 一个「列表页」看数据
- 两个页面用路由切换,改了立刻两边同步

📖 路线图说明
| 步骤 | 咱要做的事 | 做完立刻得到 |
|---|---|---|
| 1 | 搭项目、装依赖 | 有可运行的空壳 |
| 2 | main.js 挂载 Pinia |
应用"认识"状态仓库 |
| 3 | 写 store.js |
有共享数据容器 |
| 4 | 写 Operate 操作页 | 能改仓库里的数据 |
| 5 | 写 List 展示页 | 能看见同一份数据 |
| 6 | 配路由联调 | 换页数据还在(刷新会丢) |
第 0 步:先搞懂「为什么要 Pinia」
前端常见三种传数据方式:
| 方式 | 像什么 | 痛点 |
|---|---|---|
组件传参 props/$emit |
快递层层转手 | 隔好几层组件,传参很烦 |
| 路由传参 | 把纸条贴在网址上 | 依赖 URL,不适合复杂对象 |
| Pinia | 小区中央仓库 | 谁需要谁来取,改了大家同步 |

📖 说明
- 左:父子传参 ------ 中间组件也得当"搬运工"
- 中:路由传参 ------ 数据挂在地址栏,能力有限
- 右:Pinia ------ 多组件直连同一个仓库
什么时候用 Pinia(笔记):多组件共享状态、要集中管理、(配合 Storage)要持久化、中大型应用状态管理。
和后端类比:
| 前端 | 后端类似物 |
|---|---|
| 组件 props | 方法参数 |
| 路由参数 | 请求里带点信息 |
| Pinia | 像 Redis / 应用内共享容器 |
刷新丢数据(我们如果不点击路由,直接刷新页面,那么此时我们的数据就丢失) |
进程重启内存没了 |
第 1 步:搭项目 + 装依赖
这步做什么?
准备一个 Vite + Vue3 项目,装上 pinia 和 vue-router@4。
写什么代码 / 命令?
bash
# 创建项目(笔记示例名可自定,如 01-PE-demo)
npm create vite@latest
cd 你的项目名
npm i
# 必须多装这两个
npm i pinia
npm i vue-router@4
# 启动(必须在项目目录里)
npm run dev
写了就有了什么功能?
- 浏览器能打开空页面
node_modules里有了 Pinia,但还不能用(还没挂载)
⚠️ 只安装 ≠ 已启用。下一步才是挂载。
第 2 步:在 main.js 挂载 Pinia(应用才"开仓库大门")
这步做什么?
让 Vue 应用正式启用 Pinia,就像给 IOC 容器通电。

写什么代码?
javascript
// src/main.js
import { createApp } from 'vue' // 创建 Vue 应用
import App from './App.vue' // 根组件
import { createPinia } from 'pinia' // ⚠️ 从 pinia 包导入,不是从 store 文件导入
const app = createApp(App) // ① 创建应用
const pinia = createPinia() // ② 创建 Pinia 容器(像 IOC)
app.use(pinia) // ③ 挂到应用上------从此才能用 store
app.mount('#app') // ④ 挂到页面

如果后面还要路由(第 6 步),最终常见写法:
javascript
import { createApp } from 'vue'
import App from './App.vue'
import { createPinia } from 'pinia'
import router from './routers/router.js' // 第 6 步再加
const app = createApp(App)
app.use(createPinia()) // 状态仓库
app.use(router) // 路由
app.mount('#app')
写了就有了什么功能?
| 做了 | 能力 |
|---|---|
createPinia() + app.use(pinia) |
组件里可以创建/获取 store |
漏掉 use(pinia) |
一用 store 就报错(笔记常见坑) |
📖 说明
- 安装只是把工具买回家
createPinia是建仓库管理员app.use是正式开门营业- 没开门,后面
defineStore定义了也用不了
第 3 步:定义 Store(中央仓库长什么样)
这步做什么?
新建 src/store/store.js,用 defineStore 声明一份共享数据。

类比
| Pinia | 像 Java |
|---|---|
id |
类名(必须唯一) |
state |
属性(存值) |
getters |
get 方法(只读计算) |
actions |
业务方法(可改数据) |
写什么代码?
javascript
// src/store/store.js
import { defineStore } from 'pinia' // 导入"定义仓库"的工厂函数
// defineStore(唯一ID, 配置对象)
// 第一个参数 'personPinia':全应用唯一,不能和别的 store 撞名
export const definedPerson = defineStore('personPinia', {
// ---------- state:存数据(推荐箭头函数返回对象)----------
// 特点:本身已是响应式,组件里不用再包一层 ref,也不用 .value
state: () => ({
username: '张三', // 姓名
age: 0, // 年龄
hobbies: ['唱歌', '跳舞'], // 爱好列表
}),
// ---------- getters:只读计算(类似 computed)----------
// 一般不在这里改数据;用法像属性:person.getAge
getters: {
getHobbiesCount() {
return this.hobbies.length // 爱好个数
},
getAge() {
return this.age // 取年龄(演示用)
},
},
// ---------- actions:真正改数据的业务方法 ----------
actions: {
doubleAge() {
this.age = this.age * 2 // 年龄翻倍
},
},
})
① state → 公共数据源(相当于组件data())
js
state: () => ({ username: '张三', age: 0, hobbies: ['唱歌','跳舞'] })
✅ 规范:必须使用箭头函数返回对象 (避免多个组件实例共享同一份数据)
✅ 特性:自带响应式,组件获取数据不需要ref、不需要.value
✅ 作用:存放全局共享的数据
② getters → 计算属性(相当于组件computed)
js
getters: {
getHobbiesCount(){
return this.hobbies.length
}
}
- 依赖state数据,生成派生、加工后的数据
- 原则:只读,不在getters内部修改state
- 组件使用方式:直接访问,不需要调用
store.getHobbiesCount
③ actions → 业务方法(相当于组件methods)
js
actions: {
doubleAge() {
this.age = this.age * 2
}
}
- 唯一推荐修改state数据的地方,存放业务逻辑
- 内部通过
this直接访问state、getters - 支持同步代码,也可以写异步请求

写了就有了什么功能?
- 有了"人"这份共享状态的定义(类)
- 还没有页面在用 ;要在组件里调用
definedPerson()才会拿到实例
📖 说明
defineStore= 开一张图纸state / getters / actions= 属性 / get / 业务方法- 可以再定义 order、product 等多个 store(模块化)
第 4 步:写 Operate.vue(操作仓库)
这步做什么?
做一个"遥控器"页面:改姓名、年龄、爱好,调用翻倍 / 重置 / 批量改。
写什么代码?
html
<!-- src/components/Operate.vue -->
<script setup>
// 导入 store 定义(具名导出)
import { definedPerson } from '../store/store.js'
// 调用函数 → 拿到单例实例(第一次创建,之后都是同一个)
const person = definedPerson()
// 批量改几个字段(Pinia 内置)
const patchDemo = () => {
person.$patch({
username: '奥特曼',
age: 100,
})
}
// 恢复成 state 初始值(Pinia 内置)
const resetDemo = () => {
person.$reset()
}
</script>
<template>
<div>
<h3>操作页 Operate</h3>
<!-- 直接双向绑定 store 的字段:改输入框 = 改中央仓库 -->
姓名:
<input v-model="person.username" />
<br />
年龄:
<input type="number" v-model.number="person.age" />
<br />
<!-- 复选框绑到数组:勾选会往 hobbies 里添加/移除,不是整段替换 -->
爱好:
<label><input type="checkbox" value="唱歌" v-model="person.hobbies" />唱歌</label>
<label><input type="checkbox" value="跳舞" v-model="person.hobbies" />跳舞</label>
<label><input type="checkbox" value="打球" v-model="person.hobbies" />打球</label>
<br />
<button @click="person.doubleAge()">年龄 × 2</button>
<button @click="resetDemo">$reset 重置</button>
<button @click="patchDemo">$patch 批量改</button>
<!-- 调试:直接把整个 store 打出来看 -->
<pre>{{ person }}</pre>
</div>
</template>
写了就有了什么功能?
| 操作 | 效果 |
|---|---|
| 改输入框 | person.username / age 立刻变 |
| 勾选爱好 | hobbies 数组增减 |
doubleAge() |
走 actions 改年龄 |
$reset() |
回到初始「张三 / 0 / 唱歌跳舞」 |
$patch({...}) |
一次改多个字段 |
此时如果还没有 List 页,你改了只能在本页看见------下一步让另一个组件也读同一份。
第 5 步:写 List.vue(展示同一份仓库)
这步做什么?
再做一个"显示屏"页面:只负责读,证明共享成立。

写什么代码?
html
<!-- src/components/List.vue -->
<script setup>
import { definedPerson } from '../store/store.js'
// 关键:和 Operate 调的是同一个 defineStore
// Pinia 单例 → 拿到的是同一对象
const person = definedPerson()
</script>
<template>
<div>
<h3>列表页 List</h3>
<p>姓名:{{ person.username }}</p>
<p>年龄(state):{{ person.age }}</p>
<p>年龄(getter):{{ person.getAge }}</p>
<p>爱好个数:{{ person.getHobbiesCount }}</p>
<ul>
<li v-for="(item, index) in person.hobbies" :key="index">
{{ item }}
</li>
</ul>
</div>
</template>
写了就有了什么功能?
- Operate 里改姓名 → List 里不用传 props也会变
- 这就是 Pinia 要解决的核心问题:跨组件共享 + 自动同步

📖 说明
- 中间是唯一的 Store 实例
- 左边写、右边读,箭头都指向同一块内存
- 路由切换 :一般还在;浏览器刷新:内存清空,回到初始 state(笔记重点)
第 6 步:配路由,让两个页能切换
这步做什么?
用 vue-router@4 把 Operate / List 挂到不同路径,App 留 <router-view>。
写什么代码?
① src/routers/router.js
javascript
import { createRouter, createWebHashHistory } from 'vue-router'
import Operate from '../components/Operate.vue'
import List from '../components/List.vue'
const router = createRouter({
history: createWebHashHistory(), // Hash 模式,学习期省事
routes: [
{ path: '/', redirect: '/operate' },
{ path: '/operate', component: Operate },
{ path: '/list', component: List },
],
})
export default router
② src/App.vue
html
<script setup>
// 根组件只负责导航 + 出口,不直接管业务数据
</script>
<template>
<div>
<nav>
<router-link to="/operate">去操作页</router-link> |
<router-link to="/list">去列表页</router-link>
</nav>
<!-- 路由出口:切换组件显示在这里 -->
<router-view />
</div>
</template>
③ main.js 记得同时 use(router) + use(pinia)
写了就有了什么功能?
| 操作 | 预期 |
|---|---|
| 点「去操作页」 | 渲染 Operate |
| 改完数据再点「去列表页」 | List 显示刚改的值(未刷新浏览器) |
| 按浏览器刷新 | 数据回到初始(会话级内存) |
联调口令
bash
npm run dev
/#/operate把姓名改成「李四」,年龄改成 10- 切到
/#/list→ 应看到李四 / 10 - F5 刷新 → 变回张三 / 0(除非以后做持久化)
原理:Store 在内存里怎么存?

📖 图解说明
| 概念 | 住哪 | 含义 |
|---|---|---|
defineStore(...) |
模块作用域 | 只是"图纸/类定义" |
第一次 definedPerson() |
Pinia 容器(堆) | 创建唯一实例 |
再次 definedPerson() |
返回同一引用 | Operate / List 共享 |
state 里的字段 |
实例内部的响应式对象 | 改字段 → 依赖视图更新 |
| 浏览器刷新 | 整页 JS 内存清空 | 实例没了 → 重新按初始 state 建 |
text
栈上:组件里的变量 person ──引用──► 堆上:同一个 Store 实例
├─ username: '张三'
├─ age: 0
└─ hobbies: [...]
和组件里 ref 的区别:
组件 ref |
Pinia state |
|
|---|---|---|
| 作用域 | 当前组件 | 全应用(同 id) |
| 访问 | 脚本要 .value |
直接 person.age |
| 换页 | 组件销毁可能没了 | 路由切换通常还在 |
| 刷新 | 没了 | 也没了(需持久化/后端重拉) |
常见报错
| 现象 | 可能原因 | 处理 |
|---|---|---|
xxx is not a function |
导出/导入方式不匹配 | export const ↔ 具名 import |
| 一用 store 就挂 | 没 app.use(createPinia()) |
检查 main.js |
| 路由切走数据没了? | 其实是刷新了 | 区分"切路由"和"F5" |
| 响应式怪 | 错误解构 store | 需要时用 storeToRefs |


三种传参怎么选
| 场景 | 优先 |
|---|---|
| 父子简单传值 | props / emit |
| 跳转带少量标识 | 路由 query/params |
| 多页面、多组件长期共享 | Pinia |
梳理
- Pinia = 前端中央仓库:解决多组件共享,避免 props 层层传。
- 顺序不能乱 :安装 →
use(createPinia)→defineStore→ 组件里definedPerson()→(可选)路由切换验证。 - 会话级内存 :切路由通常还在;刷新会重置------要持久化再配合 localStorage / 后端重拉。
💬 下一步思考 :如果希望刷新后姓名还在,你会把 username 存到哪一层?是改 Pinia 插件持久化,还是登录后从后端再拉一次?
四、运行效果


