练兵场组件代码剖析
作者:海川,发表于: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 输出
关键特点:
- 单一数据源:主组件管理 state,通过 ref 共享 editor 实例
- Hook 分离:每个 Hook 负责一个功能域
- Ref 驱动:使用 ref 存储需要跨组件访问的可变对象
- 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 };
};
关键点:
- Ref 在 useEffect 外初始化:确保 ref 在组件生命周期内一直有效
- 依赖数组为空:编辑器只初始化一次
- cleanup 函数:调用
dispose()释放 Monaco Editor 的资源 - 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 };
};
关键点:
- getValue() - 获取编辑器当前的代码内容
- iframe 隔离 - 用户的代码运行在独立的 iframe 中,不污染主应用
- console 拦截 - 通过
postMessage将 console 输出传回主窗口 - 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>`;
为什么要这样构造?
- 字符串模板拼接 - 将用户代码作为字符串插入到 HTML 模板中
- 完整的 HTML 结构 - 包含 DOCTYPE、head、body 等标准结构
- CSS 在头部 - 确保样式在 HTML 加载前应用
- 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 };
};
关键点:
- 双向通信:主窗口 → iframe 和 iframe → 主窗口
- DOM 操作:动态创建控制台输出元素
- 自动滚动:新输出时自动滚到底部
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 };
};
关键点:
- setValue() - 设置编辑器的代码内容
- Blob 创建 - 生成二进制数据用于下载
- 资源清理 -
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 | 性能优化 | 整个组件 |
学习建议
- 初学者:理解 State、Ref、useEffect 的概念
- 进阶:学习如何设计 Hook 和 Ref 的共享
- 高级:研究 iframe 通信、性能优化、错误处理
相关最佳实践
- 📌 Hook 设计:单一职责,易于测试和复用
- 📌 Ref 管理:必须时才用,不要过度使用
- 📌 性能:useCallback 用于回调,useMemo 用于计算
- 📌 清理:useEffect 必须有 cleanup 函数
- 📌 通信:父组件通过 props,兄弟通过 ref 和回调