30 分钟
项目全解析

核心文件深度解析

就看执行器、判题引擎、状态管理这些核心文件

  • 理解 Pyodide 执行器的 Worker 通信机制
  • 掌握判题引擎的测试用例执行逻辑
  • 理解 Zustand 状态管理设计
  • 了解课程内容的数据结构

executor.ts — Python 执行引擎

这是项目最核心的文件,负责在 Web Worker 里跑 Python 代码。

TypeScript
// 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 — 判题引擎

TypeScript
// 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 — 全局状态

TypeScript
// 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
  )
);
🐍为什么选 Zustand 而不是 Redux?

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:状态的序列化与恢复

简单+50 XP

persist 中间件做的事,等价于把字典 dump 成字符串存起来、需要时再 load 回来。补全 save_then_load(state):先 json.dumps 序列化、再 json.loads 恢复,返回恢复后的状态。

用 Python 理解 persist:状态的序列化与恢复
1 个测试用例

课后作业

阅读源码

中等+30 XP

打开 lib/pyodide/executor.ts 和 lib/judge/engine.ts,逐行阅读,给每行加注释,搞懂每个函数是干嘛的。

阅读源码
1 个测试用例