Skip to content

2.2 Pinia 与本地持久化 ​

阶段 1 只有阶段选择保存在页面内存,refresh 后会回到默认阶段。阶段 2 新增收藏与浏览记录,并让阶段、收藏、历史三者在 refresh 后恢复。这里需要两个边界:persistence 解释 storage,Pinia store 处理用户 action。

text
apps/web/src/persistence/recommendation-state.ts
apps/web/src/persistence/recommendation-state.test.ts
apps/web/src/stores/recommendation-store.ts
apps/web/src/stores/recommendation-store.test.ts

页面和 component 不调用 uni.getStorageSync / uni.setStorageSync。

Hydrate 先问读取结果是什么 ​

Persistence 使用固定 key:

text
chenyue:recommendation-state:v1

readRecommendationState() 返回 snapshot 和 status。Store 不把五种结果压成同一个“默认状态”:

StatusStore 行为
loaded恢复 stage、favorite IDs 与 history IDs
empty使用默认状态,后续操作可保存
invalid使用默认状态并提示损坏;下一次操作重建 v1
unsupported-version使用默认状态但阻止写入,避免覆盖未来格式
read-error使用默认状态但阻止写入,避免把“读不到”误判成“没有”

未知 storage stage 在 decoder 中回 DEFAULT_STAGE_ID。Store 的 selectStage(stageId) 面对未知调用参数则直接拒绝,不修改内存,也不写 storage。这两个入口不能混为一谈。

收藏和历史只接收固定的五个 RecommendationId。Snapshot 中的其他 ID 在 decode 时被过滤;action 收到其他字符串时也不写入。

写保护取决于能否安全覆盖 ​

损坏 JSON 属于当前 v1 的无效内容。Store 显示一次重置提示,保留默认内存状态;用户下一次切阶段或收藏时,可以写入新的合法 v1。

未来版本与读取异常不同。此时旧数据可能仍然完整,只是当前代码无法解释。Store 保留用户本次内存修改,但 persist() 不调用 storage:

ts
function persist(): void {
  if (writeProtectionNotice) {
    persistenceNotice.value = writeProtectionNotice;
    return;
  }

  const snapshot: RecommendationStateSnapshot = {
    version: 1,
    selectedStage: selectedStage.value,
    favoriteIds: [...favoriteIds.value],
    historyIds: [...historyIds.value],
  };

  persistenceNotice.value = saveRecommendationState(snapshot)
    ? ""
    : PERSISTENCE_FAILURE_NOTICE;
}

保护状态下再次 hydrate() 会重新读取。结果仍是 read-error 或 unsupported-version 时继续阻止写入;变为 loaded、empty 或 invalid 时解除保护。invalid 使用默认状态并显示重置提示,下一次用户操作可以写入新的 v1。没有“强制 reset”去覆盖未来版本。

普通写异常也不回滚刚才的操作。UI 保留新的阶段或收藏,提示用户 refresh 后可能回到旧状态。

Store 管 action,不管数据内容 ​

Store 暴露:

ts
const selectedStage = ref<StageId>(DEFAULT_STAGE_ID);
const favoriteIds = ref<RecommendationId[]>([]);
const historyIds = ref<RecommendationId[]>([]);

function hydrate(): void;
function selectStage(stageId: StageId): void;
function setFavorite(recommendationId: string, favorite: boolean): void;
function recordViewed(recommendationId: string): void;
function clearPersistenceNotice(): void;

hydrate() 在正常读取后只执行一次,防止页面重新进入时覆盖内存修改。收藏是显式 add/remove;history 把最近打开的 ID 移到首位、去重并保留 10 条:

ts
export function prependHistoryId<T extends string>(
  historyIds: readonly T[],
  recommendationId: T,
): T[] {
  return [
    recommendationId,
    ...historyIds.filter((id) => id !== recommendationId),
  ].slice(0, 10);
}

Store 不保存完整 Recommendation,也不负责按 ID 取标题。展示内容由 2.3 的 client 返回。

定向测试先覆盖失败分支 ​

从教学仓 root 运行:

bash
npm run test:web -- recommendation-state.test.ts
npm run test:web -- recommendation-store.test.ts

Fresh 结果:

text
src/persistence/recommendation-state.test.ts (38 tests)
Tests  38 passed (38)

src/stores/recommendation-store.test.ts (18 tests)
Tests  18 passed (18)

Reviewer 不只看数量。Persistence 断言要覆盖五种 read status、unknown stage fallback、固定 ID、过滤去重、history 10、冻结副本和 read/write throw。Store 断言要覆盖 hydrate once、无效 action、损坏 v1 重建、未来版本与读异常写保护、保护状态后的重新读取与解除、写失败保留内存状态。

在 H5 验证 refresh ​

bash
npm --workspace apps/web run dev:h5 -- --host 127.0.0.1 --port 4173

打开 http://127.0.0.1:4173/:

  1. 选择 1–3 岁,确认列表显示 1 条。
  2. Refresh,确认按钮仍是 aria-pressed=true,列表仍为 1 条。
  3. 收藏当前推荐,再 refresh,确认收藏态保持。
  4. 打开同一详情两次,确认“最近浏览”仍为 1 条。

Test 绿但 refresh 丢状态时,沿 page hydrate() → store action → persistence key 查。不要在 page 增加第二份 storage 代码。

继续 2.3 请求 Client 与 Mock 场景。

晨悦 AI 实践手册