项目全解析
状态管理: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) 查询。
用字典给记录建索引
Python 0%
1 个测试用例