25 分钟
项目全解析

组件架构与 UI 系统设计

解析 React 组件树、Monaco 编辑器集成、CSS 变量主题系统

  • 理解 React 组件的分层设计
  • 掌握 Monaco Editor 的 React 集成方式
  • 理解 CSS 变量主题系统
  • 学会设计可复用的 UI 组件

组件分层架构

Python
# 朋友好学组件树
print("""
Layout(布局层)
├── TopNav          顶部导航栏(毛玻璃效果)
├── CourseSidebar   左侧课程目录(桌面端)
└── MobileNav       底部导航栏(手机端)

Page(页面层)
├── HomePage        落地页
├── Dashboard       学习仪表盘
├── CoursePage      课程学习页
│   ├── ContentBlock    内容渲染器(递归)
│   │   ├── CodeBlock       代码块 → CodeEditor
│   │   ├── FillBlank       填空题
│   │   ├── ChoiceQuestion  选择题
│   │   └── Callout         提示框
│   └── ChallengePanel  挑战任务面板
└── LabPage         代码实验室

Feature(功能层)
├── CodeEditor      Monaco 编辑器 + Pyodide 执行
├── CodingKeyboard  手机端编程键盘
└── AIAssistant     豆包悬浮助手

UI(基础层)
├── Button          按钮(6 种变体 × 4 种尺寸)
├── Card            卡片
└── Badge           徽章
""")
🐍组件分层原则

基础 UI 组件不知道任何业务逻辑;功能组件封装可复用功能;页面组件组合功能组件。依赖方向只能从上到下:Page → Feature → UI,UI 组件永远不依赖 Page。

CodeEditor 组件核心结构

JavaScript
// components/editor/CodeEditor.tsx
export function CodeEditor({
  initialCode, height = 300, autoSize = false,
  maxAutoHeight = 800, readOnly = false,
}: CodeEditorProps) {
  const [code, setCode] = useState(initialCode);
  const [isFocused, setIsFocused] = useState(false);
  const [editorHeight, setEditorHeight] = useState(autoSize ? 80 : height);
  const { isLoading, result, run } = usePythonExecutor();

  const handleMount = (editor, monaco) => {
    editorRef.current = editor;
    if (autoSize) {
      const updateHeight = () => {
        const h = editor.getContentHeight();
        setEditorHeight(Math.min(maxAutoHeight, Math.max(80, h)));
        editor.layout({ width: editor.getLayoutInfo().width, height: h });
      };
      updateHeight();
      editor.onDidContentSizeChange(updateHeight);
      editor.onDidFocusEditorText(() => setIsFocused(true));
      editor.onDidBlurEditorText(() => setIsFocused(false));
    }
  };

  return (
    <MonacoEditor
      height="100%"
      language="python"
      value={code}
      onMount={handleMount}
      options={{
        minimap: { enabled: false },
        wordWrap: "on",
        padding: { top: 4, bottom: 4 },
        renderLineHighlight: isFocused ? "all" : "none",
        scrollbar: {
          vertical: autoSize ? "hidden" : "auto",
          alwaysConsumeMouseWheel: false,
        },
      }}
    />
  );
}

/*
 * 关键设计:
 * 1. autoSize:onDidContentSizeChange 比手动计算更准确
 * 2. renderLineHighlight:未聚焦时不高亮当前行(减少视觉噪音)
 * 3. alwaysConsumeMouseWheel: false:滚轮不劫持页面滚动
 * 4. next/dynamic 懒加载:Monaco 依赖浏览器 API,不能 SSR
 */

CSS 变量主题系统

示例
/* globals.css */
:root, [data-theme="dark"] {
  --background: #000000;
  --card: #1c1c1e;
  --foreground: #f5f5f7;
  --primary: #4f46e5;
  --success: #10b981;
  --danger: #ef4444;
  --border: rgba(255,255,255,0.08);
  --code-bg: #0d1117;
}

[data-theme="light"] {
  --background: #f2f2f7;
  --card: #ffffff;
  --foreground: #1d1d1f;
  --border: rgba(0,0,0,0.08);
  --code-bg: #f6f8fa;
}

/* 毛玻璃效果 */
.glass-nav {
  background-color: rgba(10,10,12,0.55);
  backdrop-filter: blur(24px) saturate(200%);
}

/*
 * 主题切换原理:
 * 1. next-themes 在 <html> 上设置 data-theme
 * 2. CSS 变量根据属性选择器自动切换
 * 3. 所有 var(--xxx) 自动更新,零 JS 开销
 */

Button 组件设计

JavaScript
const variants = {
  primary: "bg-[var(--primary)] text-white hover:bg-[var(--primary-hover)]",
  secondary: "bg-[var(--secondary)]",
  outline: "border border-[var(--border-strong)]",
  ghost: "hover:bg-[var(--muted)]",
  destructive: "bg-[var(--danger)] text-white",
  success: "bg-[var(--success)] text-white",
};

const sizes = {
  sm: "h-8 px-3 text-sm",
  md: "h-9 px-4 text-sm",
  lg: "h-11 px-6 text-base",
  icon: "h-9 w-9",
};

export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
  ({ variant = "primary", size = "md", loading, children, ...props }, ref) => (
    <button ref={ref} className={cn("btn-base", variants[variant], sizes[size])}
      disabled={loading || props.disabled} {...props}>
      {loading && <Spinner />}
      {children}
    </button>
  )
);
💡cn() 工具函数

cn() = clsx() + twMerge()。clsx 处理条件类名,twMerge 解决 Tailwind 类名冲突(如同时有 px-3 和 px-4,后者覆盖前者)。

选择题

Monaco Editor 使用 next/dynamic 动态加载的原因是什么?

选择题

让代码编辑器随内容行数自动变高,最合理的实现是?

资深工程师加餐

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

路由级代码分割与懒加载、图片懒加载并提供合适尺寸、输入用防抖、滚动用节流、超长列表用虚拟滚动;memo/useMemo/useCallback 不要无脑加,先用性能工具定位真实瓶颈。优化顺序永远是「测量 → 找到最贵的部分 → 针对性改 → 再测」。

挑战任务

计算代码编辑器的自适应高度

简单+50 XP

编辑器要随代码行数自动变高,高度就是「行数 × 每行高度 + 上下内边距」。实现函数 editor_height,参数是 line_count,line_height 默认 20,padding 默认 16。

计算代码编辑器的自适应高度
1 个测试用例