【Vue3】day80-pinia-basic

作者:mario 发布时间: 2026-09-03 阅读量:4 评论数:0

Day 80 · Pinia 上手 — defineStore:全局状态的正规军

替换周第二刀:Pinia 接管全局数据。今天先建 useDashboardStore——把 Day 65 ScreenStore 的"状态部分"(latest / alarms / tempWindows / connState)迁入 Pinia 的 state,用 getters 替代派生 computed。今天只迁状态,数据源(WS 连接与订阅)明天接——一次只换一个变量。


目录


一、安装与创建

npm install pinia
// src/main.ts —— 注册 Pinia(在路由之前,顺序无强制但建议状态先行)
import { createApp } from "vue";
import { createPinia } from "pinia";
import App from "./App.vue";
import { router } from "./router";
import "./styles/tokens.css";

createApp(App).use(createPinia()).use(router).mount("#app");

二、defineStore 三件套:state / getters / actions

import { defineStore } from "pinia";

export const useCounterStore = defineStore("counter", {
  // state:状态(组件里的 ref 集合)
  state: () => ({ count: 0 }),

  // getters:派生(组件里的 computed,自动缓存)
  getters: {
    double: (state) => state.count * 2,
  },

  // actions:变更与异步(组件里的方法 + watch 副作用的归口)
  actions: {
    increment() { this.count++; },
  },
});

2.1 与组件内响应式的概念映射

组件里 Pinia 里 说明
ref / shallowRef state 状态上移到全局
computed getters 派生跟着状态走
方法 / 异步取数 actions 变更的唯一入口

大屏视角的心智转换:Week 11 的 useScreenStore 是"每个组件订阅一份镜像";Pinia 是全局唯一状态树,组件直接读——镜像冗余(Day 75 遗留问题)从根上消失。


三、实战:dashboard store

// src/stores/dashboard.ts
import { defineStore } from "pinia";
import type { PanelSnapshot, Alarm } from "../core/types";

/**
 * 大屏全局数据中枢(Pinia 版)
 * 迁移自 Day 65 ScreenStore 的状态部分 + Day 75 useScreenStore 的镜像
 *
 * 分层说明:
 *   state   —— 四类状态(原 ScreenStore.state 的字段一一对应)
 *   getters —— 原生版在各屏手写的派生(平均温/告警排序等)统一收编
 *   actions —— 今天只放纯状态变更;数据源接入明天进 actions(Day 81)
 *
 * 性能纪律(延续 Day 72 选型三连):
 *   快照/窗口这类"大而整体替换"的数据,赋值动作必须换引用
 */
export const useDashboardStore = defineStore("dashboard", {
  state: () => ({
    latest: null as PanelSnapshot | null,                 // 最新快照
    alarms: [] as Alarm[],                                // 告警累积
    tempWindows: new Map<string, { ts: number; v: number }[]>(),  // 温度窗口
    connState: "closed" as "connected" | "connecting" | "closed",
  }),

  getters: {
    /** 平均温度(原 Day 63 面板手写计算 → Day 73 computed → 现在收编为全局派生) */
    avgTemp(state): number {
      if (!state.latest) return 0;
      const ds = state.latest.devices;
      return ds.reduce((s, d) => s + d.temperature, 0) / ds.length;
    },

    /** 活跃告警(未确认)——Day 73 告警派生链的 store 版 */
    activeAlarms(state): Alarm[] {
      return state.alarms.filter((a) => !a.acked);
    },

    /** 危险级告警计数(状态灯徽标用) */
    dangerCount(): number {
      return this.activeAlarms.filter((a) => a.level === "danger").length;
    },
  },

  actions: {
    /** 摄入快照:整体替换(换引用——shallowRef 哲学在 Pinia 的延续) */
    setSnapshot(snap: PanelSnapshot): void {
      this.latest = snap;
      // 温度窗口维护(Day 65 pushWindows 逻辑迁移)
      for (const d of snap.devices) {
        const buf = this.tempWindows.get(d.deviceId) ?? [];
        buf.push({ ts: snap.ts, v: d.temperature });
        if (buf.length > 120) buf.shift();               // 定长窗口
        this.tempWindows.set(d.deviceId, buf);
      }
    },

    /** 告警到达:拷贝追加(保证数组引用变化触发更新——Day 75 坑 4 纪律) */
    pushAlarms(incoming: Alarm[]): void {
      this.alarms = [...this.alarms, ...incoming].slice(-200);   // 上限截断
    },

    /** 确认告警(屏 A/B 共用动作) */
    ackAlarm(id: string): void {
      this.alarms = this.alarms.map((a) =>
        a.id === id ? { ...a, acked: true } : a
      );
    },

    /** 连接状态变更 */
    setConnState(s: "connected" | "connecting" | "closed"): void {
      this.connState = s;
    },
  },
});

注意 state 里的 Map:Pinia 的 state 是 reactive 包装,Map 天然被深度代理。温度窗口每秒 push 两次会触发代理开销——这是已知代价,Day 81 评估后可能改为"窗口放 store 外、getters 暴露只读快照"的优化,先跑通再优化。


四、组件消费:storeToRefs 的必要性

4.1 标准消费姿势

<script setup lang="ts">
import { storeToRefs } from "pinia";
import { useDashboardStore } from "../stores/dashboard";

const store = useDashboardStore();

// ✅ 状态与派生:storeToRefs 解构——保持响应性
const { latest, connState } = storeToRefs(store);
const { dangerCount } = storeToRefs(store);       // getters 也走这里

// ✅ 方法:直接解构(函数不存在响应性问题)
const { ackAlarm } = store;
</script>

<template>
  <ConnIndicator :state="connState" />
  <span>{{ dangerCount }} 条危险告警</span>
</template>

4.2 不用 storeToRefs 会怎样

// ❌ 直接解构 state:拿到的是当下值的快照,响应性当场死亡
const { connState } = store;
// connState 从此是 "closed" 这个字符串——store 更新它不变

// ❌ 直接解构 getter:同理
const { dangerCount } = store;

原理:store 实例是一个 reactive 对象,解构 = 把属性值拷出来,与 reactive 解构陷阱(Day 72 坑 1)同源。storeToRefs 把每个字段包成 ref 再交给你,连接不断。

肌肉记忆state/getters 走 storeToRefs,actions 直接解构


五、Devtools:状态可视化调试

Pinia 与 Vue Devtools 深度集成(这是它对手写 Store 的降维优势):

Vue Devtools → Pinia 面板:
  ▸ dashboard store 实时状态树(latest/alarms/connState 当前值)
  ▸ 时间旅行:点击历史快照回放状态(告警列表的时序调试利器)
  ▸ 手动触发 actions / 直接改 state(模拟告警不用等服务器)

实战价值:调试"屏 A 徽标没更新"时,先看 Pinia 面板——state 里 dangerCount 变了没?变了 → 组件订阅问题;没变 → actions 没被调。二分定位,一步锁定层位——手写 Store 时代要靠 console.log 硬看。


六、常见坑点

坑 1:直接解构 store 丢响应性

最高频坑(第四节详述)。报错特征:“首屏有值,之后再也不更新”。规范:storeToRefs 管状态与派生。

坑 2:在组件里直接改 store state

store.connState = "connected" 技术上可行(Pinia 默认允许),但变更绕过了 actions 汇聚点,审查时无法追踪"谁改的"。团队规范:变更一律走 actions(保留 Devtools 的变更追踪)。

坑 3:state 里的函数/类实例

WebSocket 实例、定时器句柄不属于状态——放 store 外的模块作用域(Day 81 的分层设计)。state 只放"可序列化的数据",时间旅行与持久化才可用。

坑 4:getters 里访问其他 getters 忘了 this

getters: { dangerCount(state) { return state.activeAlarms... } }——❌ state 上没有 activeAlarms(它是 getter)。访问兄弟 getter 用 this(见第三节 dangerCount 写法),且该 getter 的签名不能带 state 参数(带了 TS 推断 this 类型会错)。

坑 5:Map 状态的响应性误判

直接对 reactive Map 调 set/push,Vue 能追踪(深层代理生效)——但引用没变,若有组件 watch 该 Map 的引用(浅侦听),不会触发。规范:凡希望"变化被感知"的赋值,换引用(第三节的拷贝纪律)。


七、自测挑战

T1 · store 建立 + 消费改造(60 分钟)

完成第三节 store;把 ScreenB 的 useScreenStore() 消费改为 useDashboardStore()(数据暂用 Devtools 手动触发 actions 喂数——store.setSnapshot(mock) 验证链路)。四类状态 + 三 getters 全部上屏。

T2 · 解构陷阱复现(20 分钟)

故意直接解构 const { connState } = store,Devtools 里改 state,观察组件死住不更新;再换 storeToRefs 恢复。坑 1 的肌肉记忆成型。

T3 · Devtools 工作流(30 分钟)

用 Pinia 面板完成一次"模拟告警"调试:手动 pushAlarms 造 3 条告警 → 观察徽标与列表 → 时间旅行回放 → ackAlarm 后列表变化。把操作录屏存档(这是"状态管理调试能力"的证据)。


八、总结

环节 要点
三件套 state=ref 集合 / getters=computed / actions=变更唯一入口
消费 storeToRefs 管状态派生,actions 直接解构
拷贝纪律 赋值必换引用(Map/数组同理)
Devtools 状态树 + 时间旅行——二分定位层位
进度 状态已入 store;数据源(WS)明天接入

状态树就位但还没有活水。明天把 WS 数据源接进 actions,手写 ScreenStore 完成退役。

评论