跨组件使用与持久化
Pinia 状态管理 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。
学习目标
这一节解决 store 的两个现实问题:如何安全地持久化(刷新不丢、脏数据不崩),以及如何在组件里优雅地使用 store(避免解构陷阱、避免过度订阅)。学完你能写出一个「刷新后仍在、坏数据不炸」的购物车。
问题场景
购物车用 Pinia 管理,功能都对,刷新后清空。你加了一段 localStorage.setItem("cart", JSON.stringify(items)),又遇到新问题:用户手动改过 localStorage 后 JSON.parse 抛异常,整个应用白屏;存储里是旧版本结构(没有 qty 字段),渲染时 it.price * it.qty 得到 NaN。持久化不是一行 setItem 的事。
核心解释
持久化分三步:读时校验、写时监听、版本兜底。
// stores/cart.js
import { computed, ref, watch } from "vue";
import { defineStore } from "pinia";
const STORAGE_KEY = "reactlab.cart.v1";
function loadItems() {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return [];
const data = JSON.parse(raw);
if (!Array.isArray(data)) return [];
return data
.filter((it) => it && typeof it.id === "number" && typeof it.price === "number")
.map((it) => ({ id: it.id, name: String(it.name ?? ""), price: it.price, qty: Number(it.qty) || 1 }));
} catch {
return [];
}
}
export const useCartStore = defineStore("cart", () => {
const items = ref(loadItems());
const count = computed(() => items.value.reduce((s, it) => s + it.qty, 0));
const total = computed(() => items.value.reduce((s, it) => s + it.price * it.qty, 0));
function add(product) {
const found = items.value.find((it) => it.id === product.id);
if (found) found.qty += 1;
else items.value.push({ ...product, qty: 1 });
}
function remove(id) {
items.value = items.value.filter((it) => it.id !== id);
}
// 写时监听:深度侦听,变化后整体写回
watch(
items,
(value) => {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(value));
} catch {
/* 存储不可用(隐私模式/配额满)时静默降级 */
}
},
{ deep: true }
);
return { items, count, total, add, remove };
});
三个设计点:key 带版本号(v1),结构升级时可以换 key 或写迁移;loadItems 做字段级校验,脏数据被过滤而不是让整个应用崩溃;watch 的 deep: true 让每次增删改都同步——注意不是监听 computed(computed 不可写也不该被监听)。
storeToRefs:组件里想解构又保持响应式:
<script setup>
import { storeToRefs } from "pinia";
import { useCartStore } from "../stores/cart";
const cart = useCartStore();
const { count, total } = storeToRefs(cart); // 只有 state/getters 被转成 ref
const { add, remove } = cart; // actions 直接解构
</script>
storeToRefs 只处理 state/getters,actions 是普通函数,直接解构即可。
避免过度订阅:组件只读 count 时,不要 const cart = useCartStore() 然后到处 cart.items——虽然不会真的重新请求,但模板依赖越少,渲染越精确。习惯上:只取需要的字段(storeToRefs),action 单独拿。
持久化范围:只持久化「恢复后仍然有效」的 UI 状态。购物车、主题、草稿可以存;加载态、错误信息、临时弹窗状态不要存——刷新后它们应该回到初始值。
示例拆解
验证三个场景,这是持久化代码的验收标准:
// 1. 正常刷新:加两件商品 → 刷新 → 角标仍是 2
// 2. 脏数据:DevTools 里把 localStorage 的值改成 "{bad json" → 刷新 → 应用正常启动,购物车为空
// 3. 旧结构:把某项的 qty 删掉 → 刷新 → 该项 qty 被兜底为 1,不出现 NaN
第 2、3 条最能区分「能跑」和「写得稳」。很多线上白屏就是 JSON.parse 没有 try/catch 导致的。
动手任务
- 实现上面的持久化购物车,逐条验证三个场景。
- 给持久化加「节流」:连续快速加购时不是每次都写 localStorage(提示:
watch里用setTimeout防抖)。 - 给 key 升级到
v2,写一段迁移逻辑:读到v1时转换结构后写入v2并删除v1。
常见错误
- JSON.parse 不包 try/catch:脏数据直接抛异常,白屏。
- 只写不校验:旧结构数据导致 NaN 或渲染崩溃。读时逐字段校验。
- 监听 computed 而不是源状态:computed 不可写,也没有必要侦听。监听
items本身。 - 把 loading/error 也持久化:刷新后组件带着「加载中」状态启动,永远转圈。
检查与复盘
- 能写出「读时校验 + 写时监听」的完整持久化
- 能说清 storeToRefs 只处理什么、actions 为什么不处理
- 能判断哪些状态该持久化、哪些不该
参考资料
- Pinia 官方文档「核心概念」:storeToRefs 与解构
- MDN:Web Storage API 的异常与配额
读完这一课了?
标记完成后,工作台会安排接下来的学习。