跳到主要内容

LiveEditor

组件实现

bash
npm install @babel/standalone

组件:

jsx
import React, { useState, useRef, useEffect } from "react";
import Editor from "@monaco-editor/react";

export default function LiveEditor({ code }) {
const htmlMatch = code.match(/<html[^>]*>([\s\S]*?)<\/html>/i);
const cssMatch = code.match(/<style[^>]*>([\s\S]*?)<\/style>/i);
const jsMatch = code.match(/<script[^>]*>([\s\S]*?)<\/script>/i);

const [activeTab, setActiveTab] = useState("html");
const [codes, setCodes] = useState({
html: htmlMatch ? htmlMatch[1].trim() : "<h1>Hello</h1>",
css: cssMatch ? cssMatch[1].trim() : "h1 { color: green; }",
js: jsMatch ? jsMatch[1].trim() : "console.log('Hi');",
});

const iframeRef = useRef(null);

useEffect(() => {
const timeout = setTimeout(() => {
if (iframeRef.current) {
const doc = iframeRef.current.contentDocument;
doc.open();
doc.write(`
<html>
<head><style>${codes.css}</style></head>
<body>${codes.html}<script>${codes.js}<\/script></body>
</html>
`);
doc.close();
}
}, 300);
return () => clearTimeout(timeout);
}, [codes]);

return (
<section style={{ display: "flex", height: "500px", gap: "8px", marginTop: "1rem" }}>
<div style={{ flex: 1, display: "flex", flexDirection: "column", border: "1px solid #333" }}>
<div style={{ display: "flex", background: "#222" }}>
{["html", "css", "js"].map((tab) => (
<button
key={tab}
onClick={() => setActiveTab(tab)}
style={{
flex: 1,
padding: "6px 0",
background: activeTab === tab ? "#333" : "#222",
color: activeTab === tab ? "#fff" : "#aaa",
border: "none",
cursor: "pointer",
}}
>
{tab.toUpperCase()}
</button>
))}
</div>
<div style={{ flex: 1 }}>
<Editor
height="100%"
language={activeTab}
theme="vs-dark"
value={codes[activeTab]}
onChange={(v) => setCodes((p) => ({ ...p, [activeTab]: v }))}
options={{ automaticLayout: true, fontSize: 14, minimap: { enabled: false } }}
/>
</div>
</div>
<div style={{ flex: 1, border: "1px solid #333" }}>
<iframe ref={iframeRef} title="preview" style={{ width: "100%", height: "100%", border: "none" }} />
</div>
</section>
);
}

css
.live-editor {
display: flex;
gap: 12px;
height: 600px;
font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
}

/* 左边代码编辑区 */
.editor-panel {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
}

.editor-group {
flex: 1;
display: flex;
flex-direction: column;
}

.editor-group label {
font-size: 14px;
font-weight: 600;
color: #444;
margin-bottom: 4px;
}

.editor-group textarea {
flex: 1;
resize: none;
padding: 8px 10px;
font-size: 13px;
border: 1px solid #ccc;
border-radius: 6px;
line-height: 1.4;
background: #1e1e1e;
color: #f8f8f2;
box-shadow: inset 0 1px 3px rgba(0,0,0,0.2);
}

.editor-group textarea:focus {
outline: none;
border-color: #007acc;
box-shadow: 0 0 0 2px rgba(0,122,204,0.3);
}

/* 右边预览区 */
.preview-panel {
flex: 1;
border: 1px solid #ccc;
border-radius: 6px;
overflow: hidden;
background: #fff;
box-shadow: 0 1px 5px rgba(0,0,0,0.1);
}

.preview-panel iframe {
width: 100%;
height: 100%;
border: none;
}


live-editor 代码块实现

remark
import { visit } from "unist-util-visit";

/**
* remark-live-editor
* 将 Markdown 中的 ```live-editor 代码块转换为 <LiveEditor code={`...`} />
*/
export default function remarkLiveEditor() {
return (tree) => {
visit(tree, "code", (node, index, parent) => {
if (node.lang === "live-editor") {
parent.children.splice(index, 1, {
type: "mdxJsxFlowElement",
name: "LiveEditor",
attributes: [
{
type: "mdxJsxAttribute",
name: "code",
value: node.value,
},
],
children: [],
});
}
});
};
}