【Vue3速成】06-vue3官方库路由机制之Pinia状态管理

🎯 你正在阅读「【专栏名字】」系列文章 🎯

🔥 弹简特 个人主页

❄️ 个人专栏直通车:

靠热爱去书写自己,靠勇敢去书写生活!


🌟 博主简介:


文章目录:


一、前言

大家常说的状态管理,说白了本质就是数据管理。名词听上去很高大上,实际上理解起来并不复杂。本期我们就借助实战案例,带大家通俗易懂地认识并学习 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. 咱要把什么做出来?

目标成品

  1. 一个中央数据仓库(Store):存姓名、年龄、爱好
  2. 一个「操作页」改数据
  3. 一个「列表页」看数据
  4. 两个页面用路由切换,改了立刻两边同步
📖 路线图说明
步骤 咱要做的事 做完立刻得到
1 搭项目、装依赖 有可运行的空壳
2 main.js 挂载 Pinia 应用"认识"状态仓库
3 store.js 有共享数据容器
4 写 Operate 操作页 能改仓库里的数据
5 写 List 展示页 能看见同一份数据
6 配路由联调 换页数据还在(刷新会丢)

第 0 步:先搞懂「为什么要 Pinia」

前端常见三种传数据方式:

方式 像什么 痛点
组件传参 props/$emit 快递层层转手 隔好几层组件,传参很烦
路由传参 把纸条贴在网址上 依赖 URL,不适合复杂对象
Pinia 小区中央仓库 谁需要谁来取,改了大家同步
📖 说明
  1. :父子传参 ------ 中间组件也得当"搬运工"
  2. :路由传参 ------ 数据挂在地址栏,能力有限
  3. :Pinia ------ 多组件直连同一个仓库

什么时候用 Pinia(笔记):多组件共享状态、要集中管理、(配合 Storage)要持久化、中大型应用状态管理。

和后端类比

前端 后端类似物
组件 props 方法参数
路由参数 请求里带点信息
Pinia 像 Redis / 应用内共享容器
刷新丢数据(我们如果不点击路由,直接刷新页面,那么此时我们的数据就丢失) 进程重启内存没了

第 1 步:搭项目 + 装依赖

这步做什么?

准备一个 Vite + Vue3 项目,装上 piniavue-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 就报错(笔记常见坑)
📖 说明
  1. 安装只是把工具买回家
  2. createPinia 是建仓库管理员
  3. app.use 是正式开门营业
  4. 没开门,后面 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() 才会拿到实例
📖 说明
  1. defineStore = 开一张图纸
  2. state / getters / actions = 属性 / get / 业务方法
  3. 可以再定义 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 要解决的核心问题:跨组件共享 + 自动同步
📖 说明
  1. 中间是唯一的 Store 实例
  2. 左边写、右边读,箭头都指向同一块内存
  3. 路由切换 :一般还在;浏览器刷新:内存清空,回到初始 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
  1. /#/operate 把姓名改成「李四」,年龄改成 10
  2. 切到 /#/list → 应看到李四 / 10
  3. 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

梳理

  1. Pinia = 前端中央仓库:解决多组件共享,避免 props 层层传。
  2. 顺序不能乱 :安装 → use(createPinia)defineStore → 组件里 definedPerson() →(可选)路由切换验证。
  3. 会话级内存 :切路由通常还在;刷新会重置------要持久化再配合 localStorage / 后端重拉。

💬 下一步思考 :如果希望刷新后姓名还在,你会把 username 存到哪一层?是改 Pinia 插件持久化,还是登录后从后端再拉一次?


四、运行效果


相关推荐
java1234_小锋11 小时前
Vue3专题 - 响应式基础
vue·vue3·响应式
Wiktok19 小时前
Vue3(Vite)打包安卓APP 完整最佳实践
android·vue
sugar__salt1 天前
Vue3 自定义指令与插槽(Slot)技术详解
前端·javascript·vue.js·前端框架·vue
万亿少女的梦1682 天前
基于Vue、Vite与CesiumJS的江西省铜矿资源WebGIS系统设计
vue·vite·cesiumjs·webgis·空间数据可视化
天丁o2 天前
我把一套 Flowable + Spring Boot + Vue 的 BPM 流程引擎整理开源了:从流程设计器到待办闭环
spring boot·vue·工作流引擎·flowable·bpm流程引擎·流程审批
弹简特2 天前
【Vue3速成】05-vue3官方库路由机制之路由守卫
vue·路由
小堂子这厢有礼了3 天前
Chet.Admin 模块详解⑥:字典管理 + useDict 联动表单
vue·后台管理系统·vbenadmin·chet.admin
猫猫不是喵喵.3 天前
Vue3 hooks 函数
vue
Pu_Nine_95 天前
Vue3+TS+Express 转 Nuxt4 全栈开发笔记(Drizzle ORM + MySQL)
前端·vue·express·nuxt.js·全栈框架