海川的日志

练兵场组件代码剖析

作者:海川,发表于:2026年1月28日 01:09:37

深度剖析在线代码编辑器组件,从架构设计、组件通信、React Hooks 到 Monaco Editor 集成,完整讲解现代 React 组件开发

前言

这是一个在线代码编辑器组件(简称”练兵场”),可以实时编辑 HTML、CSS、JavaScript,并在右侧预览结果。这个项目涉及:

  • 架构设计:如何组织复杂的 React 组件
  • 状态管理:多个 Editor 实例的状态同步
  • Hook 设计:将逻辑分离成可复用的 Hook
  • 第三方库集成:Monaco Editor 集成
  • 通信机制:iframe 与主窗口的通信

本文逐层剖析这个组件,让你掌握生产级别的 React 组件开发


一、整体架构

项目结构

CodeEditor/
├── index.tsx              # 主组件入口
├── types.ts               # 类型定义
├── EditorPanel.tsx        # 左侧编辑器面板
├── PreviewPanel.tsx       # 右侧预览面板
├── ConsolePanel.tsx       # 控制台输出面板
├── hooks/                 # 自定义 Hooks
│   ├── useCodeEditor.ts       # 编辑器初始化
│   ├── usePreviewUpdate.ts    # 预览更新逻辑
│   ├── useConsoleOutput.ts    # 控制台输出
│   ├── useEditorActions.ts    # 编辑器操作
│   ├── useTabAndTheme.ts      # 标签页和主题
│   └── hooks/
└── utils/                 # 工具函数
    └── editorUtils.ts         # 编辑器默认代码

数据流向

CodeEditor (主组件)
  ├─ State: activeTab, theme
  ├─ Refs: editorsRef, containerRefs, previewIframeRef, consoleOutputRef

  ├─ EditorPanel (左侧编辑器)
  │  ├─ 显示 HTML/CSS/JS 编辑器
  │  ├─ 响应标签页切换
  │  └─ 主题切换

  └─ PreviewPanel (右侧预览)
     ├─ 预览 iframe
     └─ ConsolePanel (控制台)
        └─ 显示 console.log 输出

关键特点

  1. 单一数据源:主组件管理 state,通过 ref 共享 editor 实例
  2. Hook 分离:每个 Hook 负责一个功能域
  3. Ref 驱动:使用 ref 存储需要跨组件访问的可变对象
  4. iframe 隔离:预览内容在独立的 iframe 中运行

二、主组件剖析(index.tsx)

核心结构

const CodeEditor: React.FC<CodeEditorProps> = ({ storagePrefix = '' }) => {
  // 1. 状态管理
  const [activeTab, setActiveTab] = useState<TabType>('html');
  const [theme, setTheme] = useState('vs-dark');

  // 2. Ref 管理(跨组件共享的可变对象)
  const previewIframeRef = useRef<HTMLIFrameElement | null>(null);
  const consoleOutputRef = useRef<HTMLDivElement | null>(null);

  // 3. 各个功能 Hook
  const { editorsRef, containerRefs } = useCodeEditor({ ... });
  const { updatePreview } = usePreviewUpdate({ ... });
  const { handleConsoleClear } = useConsoleOutput({ ... });
  const { handleClearAll, handleDownload } = useEditorActions({ ... });
  const { handleTabChange, handleThemeChange } = useTabAndTheme({ ... });

  // 4. 事件处理
  const handleTabChange = (tab: TabType) => { ... };
  const handleThemeChange = (newTheme: string) => { ... };
  const handleRefresh = () => { ... };

  // 5. 渲染
  return (
    <div className="flex flex-row gap-1 h-full">
      <EditorPanel ... />
      <div className="flex flex-col gap-1 flex-1">
        <PreviewPanel ... />
        <ConsolePanel ... />
      </div>
    </div>
  );
};

关键概念解读

State vs Ref 的选择

// ✅ 用 State:需要触发重新渲染
const [activeTab, setActiveTab] = useState<TabType>("html");
const [theme, setTheme] = useState("vs-dark");
// 当这些值改变时,组件重新渲染

// ✅ 用 Ref:需要存储但不触发渲染
const previewIframeRef = useRef<HTMLIFrameElement | null>(null);
const consoleOutputRef = useRef<HTMLDivElement | null>(null);
// 这些对象本身不变,只是其内容改变,无需重新渲染

为什么用 Ref 来存储 editor 实例?

const { editorsRef, containerRefs } = useCodeEditor({ ... });

// editorsRef 中存储的是 Monaco Editor 实例:
// {
//   htmlEditor: IStandaloneCodeEditor,
//   cssEditor: IStandaloneCodeEditor,
//   jsEditor: IStandaloneCodeEditor
// }

// 原因:
// 1. Editor 实例是复杂对象,不适合用 State 管理
// 2. 当代码内容改变时,只需更新 preview,不需要重新渲染
// 3. 多个组件需要共享这些实例

包装状态更新函数的目的

// 使用自定义 Hook 返回的函数
const { handleTabChange: computeTabChange } = useTabAndTheme({ editorsRef });

// 在主组件中再包装一次
const handleTabChange = (tab: TabType) => {
  setActiveTab(tab); // 更新 UI State
  computeTabChange(tab); // 执行 Hook 中的逻辑
};

// 为什么?
// ✅ Hook 可能需要访问某些状态或 ref
// ✅ 主组件负责 UI 状态,Hook 负责编辑器逻辑
// ✅ 分离关注点,提高代码可维护性

三、Hook 设计深度讲解

1. useCodeEditor - 编辑器初始化

export const useCodeEditor = ({
  storagePrefix,
  theme,
}: UseCodeEditorConfig) => {
  // ✅ 存储编辑器实例(Ref,不触发渲染)
  const editorsRef = useRef<EditorInstances>({
    htmlEditor: null,
    cssEditor: null,
    jsEditor: null,
  });

  // ✅ 存储容器引用(DOM 挂载点)
  const containerRefs = useRef<ContainerRefs>({
    html: null,
    css: null,
    js: null,
  });

  // ✅ 初始化编辑器
  useEffect(() => {
    // 1. 检查容器是否已挂载
    if (
      !containerRefs.current.html ||
      !containerRefs.current.css ||
      !containerRefs.current.js
    ) {
      return;
    }

    // 2. 从 localStorage 恢复代码或使用默认代码
    const htmlStorageKey = `${storagePrefix}htmlCode`;
    const htmlEditor = monaco.editor.create(containerRefs.current.html, {
      value: localStorage.getItem(htmlStorageKey) || defaultHTML,
      language: "html",
      theme,
      fontSize: 13,
      lineNumbers: "on",
      automaticLayout: true, // ✅ 重要:容器大小改变时自动调整
      minimap: { enabled: false },
      formatOnPaste: true,
      formatOnType: true,
    });

    editorsRef.current = { htmlEditor, cssEditor, jsEditor };

    // 3. 清理:销毁编辑器,释放资源
    return () => {
      htmlEditor.dispose();
      cssEditor.dispose();
      jsEditor.dispose();
    };
  }, []); // ✅ 只在挂载时初始化一次

  return { editorsRef, containerRefs };
};

关键点

  1. Ref 在 useEffect 外初始化:确保 ref 在组件生命周期内一直有效
  2. 依赖数组为空:编辑器只初始化一次
  3. cleanup 函数:调用 dispose() 释放 Monaco Editor 的资源
  4. localStorage 持久化:代码自动保存和恢复

2. usePreviewUpdate - 预览更新逻辑

export const usePreviewUpdate = ({ editorsRef, previewIframeRef, ... }) => {
  // ✅ 使用 useCallback 缓存函数,防止不必要的闭包重建
  const updatePreview = useCallback(() => {
    const { htmlEditor, cssEditor, jsEditor } = editorsRef.current || {};
    if (!htmlEditor || !cssEditor || !jsEditor || !previewIframeRef.current) {
      return;
    }

    // 1. 获取编辑器的代码
    const html = htmlEditor.getValue();
    const css = cssEditor.getValue();
    const js = jsEditor.getValue();

    // 2. 保存到 localStorage
    localStorage.setItem(htmlStorageKey, html);
    localStorage.setItem(cssStorageKey, css);
    localStorage.setItem(jsStorageKey, js);

    // 3. 构造 HTML 文档
    const iframeContent = `<!DOCTYPE html>
<html>
  <head>
    <style>
      ${css}
    </style>
  </head>
  <body>
    ${html}
    <script>
      // ✅ 关键:拦截 console 输出
      console.log = function(...args) {
        window.parent.postMessage({
          type: 'console',
          method: 'log',
          message: args.join(' ')
        }, '*');
      };

      ${js}
    </script>
  </body>
</html>`;

    // 4. 写入 iframe
    const iframeDoc = previewIframeRef.current.contentDocument;
    iframeDoc.open();
    iframeDoc.write(iframeContent);
    iframeDoc.close();
  }, [editorsRef, previewIframeRef, storagePrefix]);

  return { updatePreview };
};

关键点

  1. getValue() - 获取编辑器当前的代码内容
  2. iframe 隔离 - 用户的代码运行在独立的 iframe 中,不污染主应用
  3. console 拦截 - 通过 postMessage 将 console 输出传回主窗口
  4. localStorage 持久化 - 每次更新都保存代码

3. 代码执行和预览的完整原理

这是整个系统最核心的部分,理解它需要掌握几个关键概念:

第一步:生成完整的 HTML 文档

const iframeContent = `<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <style>
      * { margin: 0; padding: 0; box-sizing: border-box; }
      body { background: #f5f5f5; padding: 20px; }
      ${css}  <!-- 用户编写的 CSS 注入到这里 -->
    </style>
  </head>
  <body>
    ${html}  <!-- 用户编写的 HTML 注入到这里 -->
    <script>
      // ... console 拦截代码 ...
      ${js}  <!-- 用户编写的 JavaScript 注入到这里 -->
    </script>
  </body>
</html>`;

为什么要这样构造?

  1. 字符串模板拼接 - 将用户代码作为字符串插入到 HTML 模板中
  2. 完整的 HTML 结构 - 包含 DOCTYPE、head、body 等标准结构
  3. CSS 在头部 - 确保样式在 HTML 加载前应用
  4. JS 在底部 - 确保 DOM 加载完后再执行

第二步:将 HTML 文档写入 iframe

const iframeDoc = previewIframeRef.current.contentDocument;
iframeDoc.open(); // 打开文档写入流
iframeDoc.write(iframeContent); // 写入完整的 HTML
iframeDoc.close(); // 关闭文档写入流

这段代码的作用:

方法作用为什么需要
contentDocument获取 iframe 内部的 Document 对象访问 iframe 的 DOM
open()打开文档写入流准备接收新内容
write()向文档写入 HTML 内容将构造好的 HTML 注入进去
close()关闭写入流并触发渲染完成注入,浏览器开始解析和渲染

第三步:浏览器自动执行

write() 调用完成

浏览器解析 HTML(从上到下)

加载 CSS 样式(在 <style> 中)

渲染 HTML 内容到 DOM

执行 <script> 中的 JavaScript

用户看到最终结果

完整的执行流程示例

假设用户编写了:

<!-- HTML 编辑器中 -->
<h1>计数器</h1>
<button id="btn">点击</button>
<div id="result">计数:0</div>
/* CSS 编辑器中 */
button {
  padding: 10px 20px;
  background: blue;
  color: white;
}
// JavaScript 编辑器中
let count = 0;
document.getElementById("btn").addEventListener("click", () => {
  count++;
  document.getElementById("result").textContent = `计数:${count}`;
  console.log("计数增加:", count);
});

完整流程

用户点击"刷新"按钮

调用 updatePreview()

获取三个编辑器的代码(getValue)

生成完整的 HTML 文档(字符串拼接)

通过 iframeDoc.write() 注入到 iframe

浏览器自动解析和执行

- CSS 应用样式
- HTML 渲染成可见的元素
- JavaScript 注册事件监听器

用户可以与页面交互(点击按钮)

点击按钮触发事件处理函数

console.log 被拦截并转发到主窗口

主窗口的 useConsoleOutput Hook 接收消息并显示

关键的隔离机制

为什么用户的代码不会污染主应用?

// ❌ 如果直接执行
eval(userCode); // 危险!用户代码可以访问 window、DOM 等

// ✅ 正确做法:使用 iframe
// iframe 创建了一个完全独立的全局作用域
// 用户的 window 对象指向 iframe 的 window
// 用户的 document 对象指向 iframe 的 document
// 主应用的 window、document 完全隔离

iframe 的内部和外部通信

// iframe 内部(被注入的代码中)
window.parent; // 指向主窗口
window.self; // 指向 iframe 本身

// 主窗口中
previewIframeRef.current.contentWindow; // 指向 iframe 的 window

// 通过 postMessage 可以安全地通信
// iframe 内:
window.parent.postMessage({ type: "console", message: "Hello" }, "*");

// 主窗口中:
window.addEventListener("message", (event) => {
  console.log(event.data); // { type: 'console', message: 'Hello' }
});

localStorage 的持久化原理

// 每次点击"刷新"时都会执行
localStorage.setItem(`${storagePrefix}htmlCode`, html);
localStorage.setItem(`${storagePrefix}cssCode`, css);
localStorage.setItem(`${storagePrefix}jsCode`, js);

// 下次页面加载时
const htmlEditor = monaco.editor.create(container, {
  value: localStorage.getItem(`${storagePrefix}htmlCode`) || defaultHTML,
  // ...
});

这实现了:

  • 自动保存:每次更新预览都会保存
  • 自动恢复:页面刷新后代码不丢失
  • 多实例支持:不同的 storagePrefix 可以存储不同的代码

4. iframe 和 postMessage 机制

// iframe 内部(被注入的 HTML)
console.log = function (...args) {
  window.parent.postMessage(
    {
      type: "console",
      method: "log",
      message: args.join(" "),
    },
    "*",
  ); // '*' 表示接收来自任何源的消息
};

// ============= 分界线 =============

// 主窗口中(useConsoleOutput Hook)
useEffect(() => {
  const handleMessage = (event: MessageEvent) => {
    if (event.data.type === "console") {
      // 接收来自 iframe 的 console 输出
      const { method, message } = event.data;
      appendConsoleOutput(method, message);
    }
  };

  window.addEventListener("message", handleMessage);
  return () => window.removeEventListener("message", handleMessage);
}, []);

5. useConsoleOutput - 控制台输出

export const useConsoleOutput = ({ consoleOutputRef, previewIframeRef }) => {
  useEffect(() => {
    // ✅ 监听来自 iframe 的消息
    const handleMessage = (event: MessageEvent) => {
      if (event.data.type !== "console") return;

      const { method, message } = event.data;
      const output = consoleOutputRef.current;

      if (!output) return;

      // 创建输出元素
      const line = document.createElement("div");
      line.className = `console-${method}`; // error/log/warn 等不同样式
      line.textContent = `[${method}] ${message}`;

      output.appendChild(line);
      output.scrollTop = output.scrollHeight; // ✅ 自动滚到最底部
    };

    window.addEventListener("message", handleMessage);
    return () => window.removeEventListener("message", handleMessage);
  }, []);

  // ✅ 清空控制台输出
  const handleConsoleClear = () => {
    if (consoleOutputRef.current) {
      consoleOutputRef.current.innerHTML = "";
    }
    // 通知 iframe 清空 console
    previewIframeRef.current?.contentWindow?.postMessage(
      { type: "console-clear" },
      "*",
    );
  };

  return { handleConsoleClear };
};

关键点

  1. 双向通信:主窗口 → iframe 和 iframe → 主窗口
  2. DOM 操作:动态创建控制台输出元素
  3. 自动滚动:新输出时自动滚到底部

6. useEditorActions - 编辑器操作

export const useEditorActions = ({
  editorsRef,
  storagePrefix,
  updatePreview,
}) => {
  // ✅ 清空所有代码
  const handleClearAll = () => {
    if (confirm("确定要清空所有代码吗?")) {
      const { htmlEditor, cssEditor, jsEditor } = editorsRef.current || {};
      htmlEditor?.setValue("");
      cssEditor?.setValue("");
      jsEditor?.setValue("");
      updatePreview();
    }
  };

  // ✅ 下载代码为 HTML 文件
  const handleDownload = () => {
    const { htmlEditor, cssEditor, jsEditor } = editorsRef.current || {};

    const html = htmlEditor?.getValue() || "";
    const css = cssEditor?.getValue() || "";
    const js = jsEditor?.getValue() || "";

    const content = `<!DOCTYPE html>
<html>
  <head>
    <style>${css}</style>
  </head>
  <body>
    ${html}
    <script>${js}<\/script>
  </body>
</html>`;

    // ✅ 创建 Blob 并下载
    const blob = new Blob([content], { type: "text/html" });
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = url;
    a.download = "code.html";
    a.click();
    URL.revokeObjectURL(url); // ✅ 清理资源
  };

  return { handleClearAll, handleDownload };
};

关键点

  1. setValue() - 设置编辑器的代码内容
  2. Blob 创建 - 生成二进制数据用于下载
  3. 资源清理 - revokeObjectURL() 释放内存中的 URL

四、关键 React 概念

概念 1:useRef vs useState

// ❌ 不好:用 State 存储编辑器实例
const [editors, setEditors] = useState<EditorInstances | null>(null);
// 每次 editors 变化都会重新渲染,而且编辑器实例本身不需要重新渲染

// ✅ 好:用 Ref 存储
const editorsRef = useRef<EditorInstances | null>(null);
// 直接存储可变对象,无需渲染

// 记住这个区别:
// State = 需要触发渲染的数据
// Ref = 需要保存但不触发渲染的数据

概念 2:useCallback 的作用

// ❌ 不用 useCallback
const updatePreview = () => {
  // 每次组件重新渲染,这个函数都会重新创建
};

// 当传递给子组件时:
<PreviewPanel onChange={updatePreview} />
// 子组件的 onChange 依赖项总是新的,导致不必要的重新渲染

// ✅ 用 useCallback
const updatePreview = useCallback(() => {
  // ...
}, [editorsRef, previewIframeRef]); // 只在依赖改变时重新创建

// 优势:
// 1. 避免子组件的不必要渲染
// 2. 稳定的函数引用
// 3. 用于性能优化(结合 React.memo)

概念 3:useEffect 的依赖数组

// ❌ 没有依赖数组:每次渲染都执行
useEffect(() => {
  // 这会导致无限渲染和内存泄漏!
  setupListener();
});

// ✅ 空依赖数组:只在挂载时执行一次
useEffect(() => {
  setupListener(); // 编辑器初始化
  return () => cleanup(); // 卸载时清理
}, []);

// ✅ 有依赖项:当依赖改变时执行
useEffect(() => {
  updateEditorTheme(theme);
}, [theme]); // 主题改变时更新编辑器

概念 4:Ref 的正确使用

// ❌ 错误:在 if 语句中初始化 ref
function BadComponent() {
  let ref;
  if (someCondition) {
    ref = useRef(null); // ❌ React Hook 规则违反
  }
  return <div ref={ref} />;
}

// ✅ 正确:在顶层初始化 ref
function GoodComponent() {
  const ref = useRef(null); // ✅ 无条件
  return <div ref={ref} />;
}

// ✅ 为什么要在 Hook 中而不是普通函数中?
const containerRefs = useRef<ContainerRefs>({ html: null, ... });
// useRef 确保 Ref 在组件生命周期内持久存在
// 普通变量每次渲染都会重新初始化

概念 5:forwardRef 模式

在这个项目中,EditorPanel 和 PreviewPanel 需要将 ref 传递给子组件:

// EditorPanel.tsx
interface EditorPanelProps {
  containerRefs: React.MutableRefObject<ContainerRefs>;
  // ...
}

const EditorPanel: React.FC<EditorPanelProps> = ({ containerRefs, ... }) => {
  return (
    <div>
      {activeTab === 'html' && <div ref={(el) => { if (el) containerRefs.current.html = el; }} />}
      {activeTab === 'css' && <div ref={(el) => { if (el) containerRefs.current.css = el; }} />}
      {activeTab === 'js' && <div ref={(el) => { if (el) containerRefs.current.js = el; }} />}
    </div>
  );
};

// 为什么不用 forwardRef?
// 因为我们传递的是 containerRefs 对象,不是单个 ref
// 这是一种更灵活的模式

五、性能优化分析

1. automaticLayout 的作用

const htmlEditor = monaco.editor.create(containerRefs.current.html, {
  automaticLayout: true, // ✅ 关键!
  // ...
});

// ✅ automaticLayout 做了什么?
// - 监听容器大小改变
// - 自动调整编辑器大小
// - 无需手动处理 resize 事件

// ❌ 如果不设置,需要手动处理
window.addEventListener("resize", () => {
  htmlEditor.layout();
});

2. useCallback 的必要性

// updatePreview 函数被多个地方使用:
// 1. 按钮点击时刷新
// 2. 代码改变时自动更新
// 3. 清空时调用

// 不用 useCallback 的后果:
// - 每次渲染都创建新函数
// - 传给子组件时导致不必要的重新渲染
// - 可能无限递归(如果在依赖项中)

3. localStorage 的优化

// 为什么要持久化?
// 1. 用户体验:刷新页面代码不丢失
// 2. 支持多个编辑器实例:用 storagePrefix 区分

const htmlStorageKey = `${storagePrefix}htmlCode`;
localStorage.setItem(htmlStorageKey, html);

// storagePrefix 允许同一页面有多个编辑器:
// 'playground_1_htmlCode'
// 'playground_2_htmlCode'
// 'blog_post_htmlCode'

六、常见问题与最佳实践

Q1:为什么编辑器代码改变时预览不自动更新?

// 目前的设计需要手动点击"刷新"按钮

// 如果要自动更新,可以这样做:
useEffect(() => {
  // 监听编辑器变化
  const htmlDisposable = htmlEditor?.onDidChangeModelContent(() => {
    updatePreview();
  });

  return () => {
    htmlDisposable?.dispose();
  };
}, []);

// ⚠️ 注意:这可能导致性能问题(频繁更新预览)
// 解决:用防抖
const debouncedUpdate = useCallback(
  debounce(() => updatePreview(), 500),
  [],
);

Q2:如何处理 JavaScript 执行错误?

// 目前的实现中,iframe 内的 error 事件被拦截:
window.addEventListener("error", function (e) {
  window.parent.postMessage(
    {
      type: "console",
      method: "error",
      message: e.message,
      stack: e.stack,
    },
    "*",
  );
});

// 这样用户写的代码出错时,会在控制台显示错误信息

Q3:如何防止 iframe 中的恶意代码?

// ✅ iframe sandbox 属性
<iframe
  srcDoc={iframeContent}
  sandbox="allow-scripts allow-same-origin"
  // sandbox 限制了 iframe 的权限
  // allow-scripts: 允许执行脚本
  // allow-same-origin: 允许同源访问(便于 postMessage)
/>

// ⚠️ 不应该允许:
// - allow-forms: 可能被用于钓鱼
// - allow-top-navigation: 可能改变主窗口的 URL
// - allow-pointer-lock: 可能获得过多权限

Q4:内存泄漏风险在哪里?

// ✅ 已处理的:
// 1. useEffect 中有 cleanup(dispose 编辑器)
// 2. event listener 有清理
// 3. Blob URL 有 revoke

// ⚠️ 潜在风险:
// 1. localStorage 无限增长(代码不断累积)
// 2. iframe 的 console 输出无限增长
// 解决:定期清理
const MAX_CONSOLE_LINES = 1000;
if (output.children.length > MAX_CONSOLE_LINES) {
  output.removeChild(output.firstChild);
}

七、扩展建议

1. 添加代码格式化

// 使用 prettier
const formatCode = async () => {
  const prettier = await import("prettier");
  const formatted = prettier.format(code, {
    parser: "babel",
    trailingComma: "es5",
  });
  htmlEditor.setValue(formatted);
};

2. 添加代码补全和错误检查

// Monaco Editor 本身支持
const htmlEditor = monaco.editor.create(container, {
  // 已启用:
  formatOnPaste: true,
  formatOnType: true,

  // 可以添加:
  suggestOnTriggerCharacters: true,
  quickSuggestions: { other: true, comments: false, strings: false },
});

3. 添加 URL 分享功能

const handleShare = () => {
  const code = {
    html: htmlEditor.getValue(),
    css: cssEditor.getValue(),
    js: jsEditor.getValue(),
  };

  // 方案 1:URL 编码
  const encoded = btoa(JSON.stringify(code));
  const url = `${location.origin}?code=${encoded}`;

  // 方案 2:Server 端存储
  const id = await saveCode(code);
  const url = `${location.origin}?id=${id}`;
};

4. 添加模板库

const templates = {
  hello: {
    html: "<h1>Hello World</h1>",
    css: "h1 { color: blue; }",
    js: 'alert("Hello");',
  },
  calculator: {
    /* ... */
  },
};

const loadTemplate = (name: string) => {
  const template = templates[name];
  htmlEditor.setValue(template.html);
  cssEditor.setValue(template.css);
  jsEditor.setValue(template.js);
  updatePreview();
};

八、总结与学习路径

核心概念清单

  • State vs Ref:何时用哪个
  • useCallback:优化函数引用
  • useEffect 依赖:防止无限循环
  • iframe 隔离:代码安全运行
  • postMessage:跨窗口通信
  • localStorage:数据持久化
  • Ref 转发:跨组件传递 ref

关键技术点

技术用途位置
Monaco Editor代码编辑useCodeEditor
iframe代码隔离运行PreviewPanel
postMessage通信机制ConsolePanel
localStorage代码持久化useCodeEditor、usePreviewUpdate
Ref & useCallback性能优化整个组件

学习建议

  1. 初学者:理解 State、Ref、useEffect 的概念
  2. 进阶:学习如何设计 Hook 和 Ref 的共享
  3. 高级:研究 iframe 通信、性能优化、错误处理

相关最佳实践

  • 📌 Hook 设计:单一职责,易于测试和复用
  • 📌 Ref 管理:必须时才用,不要过度使用
  • 📌 性能:useCallback 用于回调,useMemo 用于计算
  • 📌 清理:useEffect 必须有 cleanup 函数
  • 📌 通信:父组件通过 props,兄弟通过 ref 和回调