25 分钟
项目全解析

状态管理:Zustand Store 逐行解析

进度存储、游戏化系统和本地持久化的实现

  • 理解 Zustand 的 create 和 persist 中间件
  • 掌握学习进度的数据结构设计
  • 理解游戏化系统的实现
  • 学会安全的 localStorage 封装

进度 Store:store/progressStore.ts

TypeScript
// ===== 数据结构设计 =====
interface ProgressState {
  lessons: Record<string, LessonProgress>;
  currentLessonId: string | null;
  startLesson: (lessonId: string) => void;
  completeLesson: (lessonId: string) => void;
  saveCodeDraft: (lessonId: string, key: string, code: string) => void;
}

interface LessonProgress {
  lessonId: string;
  startedAt: string;
  completedAt?: string;
  exercises: Record<string, ExerciseResult>;
  challengePassed: boolean;
  codeDrafts: Record<string, string>;
  timeSpent: number;
}

/*
 * 设计思路:
 * 1. Record<string, LessonProgress>:用对象而非数组,O(1) 查找
 * 2. codeDrafts:用户写了一半的代码自动保存,刷新不丢失
 * 3. timeSpent:累计学习时长,用于每日目标统计
 */

Zustand Store 创建

TypeScript
import { create } from "zustand";
import { persist } from "zustand/middleware";

export const useProgressStore = create<ProgressState>()(
  persist(
    (set, get) => ({
      lessons: {},
      currentLessonId: null,

      startLesson: (lessonId) =>
        set((state) => {
          if (state.lessons[lessonId]) {
            return { currentLessonId: lessonId };
          }
          return {
            currentLessonId: lessonId,
            lessons: {
              ...state.lessons,
              [lessonId]: createEmptyProgress(lessonId),
            },
          };
        }),
    }),
    {
      name: "Pyxis-V2.0.2-progress",
      storage: createJSONStorage(() => safeStorage),
    }
  )
);

/*
 * 逐行解析:
 * 1. create<ProgressState>():TypeScript 泛型声明状态形状
 * 2. persist 中间件:自动把状态同步到 localStorage
 * 3. set:合并新状态(不可变更新,展开运算符)
 * 4. name: localStorage 的 key
 */

安全存储封装

TypeScript
// ===== lib/utils/safeStorage.ts =====
const memoryStore = new Map<string, string>();

function isStorageAvailable(): boolean {
  try {
    const key = "__test__";
    window.localStorage.setItem(key, "1");
    window.localStorage.removeItem(key);
    return true;
  } catch {
    return false;  // 隐私模式、配额满都可能导致不可用
  }
}

export const safeStorage = {
  getItem(key: string): string | null {
    try {
      return isStorageAvailable()
        ? window.localStorage.getItem(key)
        : memoryStore.get(key) ?? null;
    } catch {
      return memoryStore.get(key) ?? null;
    }
  },
  setItem(key: string, value: string): void {
    try {
      if (isStorageAvailable()) {
        window.localStorage.setItem(key, value);
      } else {
        memoryStore.set(key, value);
      }
    } catch {
      memoryStore.set(key, value);  // 配额满也降级
    }
  },
};

/*
 * 为什么需要 safeStorage?
 * 1. Safari 隐私模式:localStorage 存在但 setItem 抛异常
 * 2. 移动端 WebView:可能禁用 localStorage
 * 3. 存储配额满:5MB 用完后抛 QuotaExceededError
 * 降级策略:所有异常静默捕获,降级到内存 Map
 */

游戏化 Store:经验值与等级

TypeScript
// 等级计算公式:xpForLevel(n) = Math.floor(100 * 1.15^(n-1))
// 达到 Lv1 需 100 XP、Lv2 需 114、Lv10 需 351、Lv11 需 404
// 指数增长让前期升级快(正反馈密集),后期变慢(成就感稀缺)

addXp: (amount: number) =>
  set((state) => {
    const newXp = state.totalXp + amount;
    const oldLevel = state.level;
    const newLevel = levelFromXp(newXp).level;

    if (newLevel > oldLevel) {
      emit({ type: "levelup", oldLevel, newLevel });
    }

    return { totalXp: newXp, level: newLevel };
  }),
🐍事件发射模式

emit() 是简单的发布-订阅模式。Store 不直接依赖 UI 组件,而是发射事件。UI 层订阅事件并显示动画。这样 Store 保持纯粹,动画逻辑可以独立变化。

选择题

safeStorage 降级到内存存储时,会丢失什么功能?

选择题

学习进度按课程 ID 存取,为什么用字典/Record 而不是数组?

资深工程师加餐

底层原理 · 大厂视角 · 工程经验,点卡片展开

移动网络不稳定,离线优先的思路是:本地是唯一可信数据源,UI 先读本地立即响应,后台再静默与服务器同步,冲突按策略合并。它带来秒开、可离线使用的体验,代价是要处理同步状态、冲突解决和幂等。本项目把课程与 Python 运行时全部内置、开箱即用,正是这一思想的体现——把「没有网络也能完整学习」当成核心能力而非降级方案。

挑战任务

用字典给记录建索引

简单+50 XP

进度按课程 ID 存取最频繁。写 build_index(records):records 是若干 [课程id, 是否完成],返回 {id: 完成状态} 的字典;再写 is_done(index, lesson_id) 用字典 O(1) 查询。

用字典给记录建索引
1 个测试用例