惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

V
V2EX - 技术
D
DataBreaches.Net
阮一峰的网络日志
阮一峰的网络日志
Recent Announcements
Recent Announcements
V
V2EX
Hugging Face - Blog
Hugging Face - Blog
T
The Exploit Database - CXSecurity.com
Simon Willison's Weblog
Simon Willison's Weblog
Cisco Talos Blog
Cisco Talos Blog
Microsoft Security Blog
Microsoft Security Blog
C
Cyber Attacks, Cyber Crime and Cyber Security
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
K
Kaspersky official blog
F
Fortinet All Blogs
GbyAI
GbyAI
Forbes - Security
Forbes - Security
The Cloudflare Blog
博客园 - 司徒正美
博客园_首页
量子位
Schneier on Security
Schneier on Security
G
GRAHAM CLULEY
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
P
Proofpoint News Feed
N
News | PayPal Newsroom
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
博客园 - 聂微东
T
Tor Project blog
V
Vulnerabilities – Threatpost
Y
Y Combinator Blog
Jina AI
Jina AI
Help Net Security
Help Net Security
T
Threat Research - Cisco Blogs
Recent Commits to openclaw:main
Recent Commits to openclaw:main
C
Cybersecurity and Infrastructure Security Agency CISA
Project Zero
Project Zero
N
News and Events Feed by Topic
I
Intezer
B
Blog
美团技术团队
C
CERT Recently Published Vulnerability Notes
NISL@THU
NISL@THU
L
LINUX DO - 最新话题
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Blog — PlanetScale
Blog — PlanetScale
AWS News Blog
AWS News Blog
T
Tailwind CSS Blog
The Last Watchdog
The Last Watchdog
雷峰网
雷峰网
有赞技术团队
有赞技术团队

windsong

从画图纸到捏泥巴:从后端到 JavaScript 清华校园网认证笔记 摆脱“被动焦虑”的终极解药:一个尼采主义者的自我救赎与“强力意志”觉醒 windsong Vue 组件通信 词法作用域与动态作用域 全栈架构:三套 Schema 前后端交互的桥梁:Axios 全栈容器化应用的环境变量管理 早抛晚捕:异常处理 股票市场常识 大噜村的债务:过去和未来 大噜村的发展故事 重读设计模式:从理论到实践的反思(二) Transformer 个人入门 Go Viper:设计哲学与最佳实践 简单的 Go WebSocket 服务器 最简单的 Go HTTP 服务器 Go 语言核心机制:命名类型与接口
Pinia Store :前端的 MVVM 解耦
xxxx · 2026-01-10 · via windsong

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,我们可以实现关注点分离:

  1. Store (Model/ViewModel):负责定义数据结构(state)、计算属性(getters)和业务动作(actions)。它完全不关心数据是如何展示的(是列表?是图表?还是纯文本?)。
  2. 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>