核心文件深度解析
就看执行器、判题引擎、状态管理这些核心文件
- 理解 Pyodide 执行器的 Worker 通信机制
- 掌握判题引擎的测试用例执行逻辑
- 理解 Zustand 状态管理设计
- 了解课程内容的数据结构
executor.ts — Python 执行引擎
这是项目最核心的文件,负责在 Web Worker 里跑 Python 代码。
// lib/pyodide/executor.ts 核心逻辑(简化版)
// 单例 Worker,所有组件共享
let worker: Worker | null = null;
let pyodideReady = false;
let pyodideReadyPromise: Promise<void> | null = null;
// 待处理请求映射(请求 ID → Promise)
const pendingRequests = new Map();
function createWorker() {
const w = new Worker("/pyodide/worker.js", { type: "module" });
w.onmessage = (e) => {
const msg = e.data;
if (msg.type === "result") {
// 根据请求 ID 找到对应的 Promise 并 resolve
const pending = pendingRequests.get(msg.id);
if (pending) {
clearTimeout(pending.timer);
pendingRequests.delete(msg.id);
pending.resolve({
output: msg.output,
error: msg.error,
executionTime: msg.executionTime,
});
}
}
};
return w;
}
export async function executePython(code: string, stdin?: string[]) {
await ensureWorker();
if (pyodideReadyPromise) await pyodideReadyPromise;
const id = ++requestId;
return new Promise((resolve) => {
// 超时保护:5秒不返回就终止
const timer = setTimeout(() => {
pendingRequests.delete(id);
resolve({ output: "", error: "执行超时(可能含无限循环)" });
}, 5000);
pendingRequests.set(id, { resolve, timer });
worker.postMessage({ id, type: "execute", code, options: { stdin } });
});
}这里用了几种设计模式:单例模式(Worker 全局唯一)Promise 模式(回调转 Promise)请求-响应模式(ID 匹配请求和响应)代理模式(Executor 代理 Worker 通信)。异步的 Worker 通信就变得像同步函数调用一样简单。
engine.ts — 判题引擎
// lib/judge/engine.ts 核心逻辑(简化版)
export async function runJudge(userCode: string, testCases: TestCase[]) {
const results = [];
for (const testCase of testCases) {
// 把用户代码和测试输入组合执行
const testCode = `
${userCode}
# 测试代码
import sys
${testCase.stdin ? `sys.stdin = ['${testCase.stdin.join("','")}']` : ''}
${testCase.testCode || ""}
`;
const result = await executePython(testCode, testCase.stdin);
if (result.error) {
results.push({ passed: false, error: translatePythonError(result.error) });
} else if (testCase.expectedOutput !== undefined) {
// 比对输出(忽略首尾空白)
const passed = result.output.trim() === testCase.expectedOutput.trim();
results.push({ passed, expected: testCase.expectedOutput, actual: result.output });
}
}
return {
passed: results.every(r => r.passed),
results,
score: results.filter(r => r.passed).length / results.length,
};
}判题支持三种模式:标准输出比对(最常用)函数返回值断言异常捕获测试。输出比对时忽略首尾空白,避免换行符差异导致误判。错误信息经过人性化翻译,帮助小白理解问题。
Zustand Store — 全局状态
// lib/store/progressStore.ts(简化版)
import { create } from "zustand";
import { persist } from "zustand/middleware";
export const useProgressStore = create(
persist(
(set, get) => ({
completedLessons: [],
xp: 0,
streak: 0,
achievements: [],
completeLesson: (lessonId: string, xpGain: number) =>
set((state) => ({
completedLessons: [...state.completedLessons, lessonId],
xp: state.xp + xpGain,
})),
addXp: (amount: number) => set((state) => ({ xp: state.xp + amount })),
}),
{ name: "Pyxis-V2.0.2-progress" } // localStorage key
)
);Redux 要写 action、reducer、dispatch,模板代码多。Zustand 只需要定义一个 hook,直接调用方法就能更新状态。这个项目的规模,Zustand 足够用,还更简洁。persist 中间件自动同步到 localStorage,零配置。
课程数据结构
// types/index.ts 课程相关类型
interface Lesson {
id: string; // "s1-m1-l1"
title: string;
description: string;
duration: number; // 预计分钟数
objectives: string[]; // 学习目标
content: LessonContent[]; // 教学内容块
challenge: Challenge; // 挑战任务
knowledgeCards: KnowledgeCard[];
homework: Homework[];
}
// 内容块是联合类型,支持多种交互形式
type LessonContent =
| { type: "heading"; level: 1|2|3; text: string }
| { type: "paragraph"; text: string }
| { type: "code"; code: string; runnable: boolean }
| { type: "callout"; variant: string; title: string; text: string }
| { type: "choice"; question: string; options: string[]; correctIndex: number }
| { type: "fill-blank"; code: string; blanks: Blank[] }
| { type: "predict"; code: string; answer: string }
| { type: "find-bug"; buggyCode: string; fixedCode: string }
| { type: "visual"; codeLines: string[]; steps: VisualStep[] };课程内容不是硬编码在组件里,而是数据结构驱动。添加一节课只需创建一个 Lesson 对象,CourseRenderer 自动渲染所有交互模块。这种设计让内容和展示分离,非程序员也能编写课程内容,只需了解数据结构。
Executor 中用什么机制匹配请求和响应?
Zustand 的 persist 中间件的作用是什么?
资深工程师加餐
底层原理 · 大厂视角 · 工程经验,点卡片展开
客户端把进度存到本地(localStorage / SQLite)后,下一版字段结构一变,老用户存档就可能解析失败、进度全丢。专业做法是给存档加 version 字段,读取时按版本依次执行迁移函数(补默认值、改名、重组结构),并对解析全程 try/catch、坏档能回退到安全默认。写入时还要防御 NaN/undefined(如 JSON.stringify(NaN) 会变成 null),这正是「数据比代码活得久」的含义。
挑战任务
用 Python 理解 persist:状态的序列化与恢复
persist 中间件做的事,等价于把字典 dump 成字符串存起来、需要时再 load 回来。补全 save_then_load(state):先 json.dumps 序列化、再 json.loads 恢复,返回恢复后的状态。
课后作业
阅读源码
打开 lib/pyodide/executor.ts 和 lib/judge/engine.ts,逐行阅读,给每行加注释,搞懂每个函数是干嘛的。