Pinia Store :前端的 MVVM 解耦
在 Vue 开发的早期阶段,或者在编写简单的 .vue 文件时,我们习惯把 数据(State)、业务逻辑(Methods) 和 HTML 模板(View) 写在一起。这种“全家桶”式的写法虽然上手快,但随着业务复杂度增加,痛点也随之而来:UI 和业务逻辑紧紧捆绑在一个文件中。如果另一个页面也需要这份数据,或者想对这段复杂的逻辑进行单元测试,会发现寸步难行。
现状:耦合的代码 (The Problem)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <!-- Component.vue --> <script setup> import { ref } from 'vue'
// 数据和逻辑都被锁死在 UI 文件内部 const count = ref(0) const doubleCount = computed(() => count.value * 2)
function add() { // 假设这里还有复杂的 API 调用或权限校验 count.value++ } </script>
<template> <button @click="add">{{ count }} (Double: {{ doubleCount }})</button> </template>
|
痛点:这个 .vue 文件承担了太多的责任。它既要负责“长什么样”,又要负责“怎么运作”。
解决方案:引入 Pinia (The Solution)
是否有办法把数据的定义、计算和更新逻辑从 .vue 文件中彻底挪出来呢?
Vue 官方推荐的状态管理库 Pinia 正是为此而生。通过 Pinia,我们可以实现关注点分离:
- Store (Model/ViewModel):负责定义数据结构(
state)、计算属性(getters)和业务动作(actions)。它完全不关心数据是如何展示的(是列表?是图表?还是纯文本?)。
- Component (.vue):回归纯粹的 View。它只负责渲染数据和触发用户事件。
类比 WPF/MVVM
此时 .vue 只是数据的“订阅者”和“命令发送者”。如果熟悉 C# WPF 开发,这就是 MVVM 模式在前端的完美复刻:
- Store =
ViewModel
- 持有数据属性:
IsLoading, ChartData
- 持有命令/逻辑:
FetchCommand, CalculatedPrice
- Vue Component =
XAML (View)
- 通过 Binding 绑定数据
- 通过 Event/Command 绑定行为
如何组织与定义
项目结构组织
在工程化项目中,我们通常会在 src 目录下建立独立的 stores 文件夹。建议遵循 Modular 的原则,按照业务领域划分 Store。
1 2 3 4 5 6 7 8 9
| frontend/ ├── src/ │ ├── components/ # UI 组件 (View) │ │ └── UserProfile.vue │ ├── stores/ # 状态管理 (ViewModel) │ │ ├── index.ts # (可选) 统一导出 │ │ ├── counter.ts # 计数器相关逻辑 │ │ └── user.ts # 用户信息相关逻辑 │ └── App.vue
|
定义 Store (Setup Syntax)
在 src/stores/counter.ts 中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| import { defineStore } from 'pinia' import { ref, computed } from 'vue'
export const useCounterStore = defineStore('counter', () => { const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() { count.value++ }
async function asyncIncrement() { await new Promise(r => setTimeout(r, 500)) count.value++ }
return { count, doubleCount, increment, asyncIncrement } })
|
这一层是纯逻辑,不知 UI 为何物。 它可以被任何组件复用,甚至可以在 Node.js 环境下单独测试。
在组件中使用 (The View)
现在,.vue 文件变得异常清爽。组件只管“调用”,不管“如何实现”。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <!-- Component.vue --> <script setup> import { useCounterStore } from '@/stores/counter'
// 1. 实例化 const store = useCounterStore() </script>
<template> <!-- 2. 直接通过 store 实例访问,响应式完全正常 --> <div> <h1>{{ store.count }}</h1> <button @click="store.increment">Add</button> </div> </template>
|