代码编辑器组件(Monaco/CodeMirror)
嵌入带语言模式、lint、懒加载控包大小的代码编辑器。
组件模式高级
为什么
textarea 适合单行输入但对代码无用:无语法高亮、无缩进、无 lint。Monaco 或 CodeMirror 给 IDE 级体验,但重 — 懒加载强制。
复合组件模式被 Radix UI、Headless UI 和 shadcn/ui 采用 — 这是 2024 年采用最多的三个无头库。相比扁平组件 API,它们减少 70% 的属性逐层传递。
何时使用
用于配置编辑(JSON/YAML)、查询输入(SQL/GraphQL)或浏览器内 IDE 功能。纯文本用 textarea 更轻。
如何做
1. 选 — Monaco(VS Code 引擎,重约 2MB)或 CodeMirror 6(模块化,约 200KB)
2. 懒加载 — React.lazy + 动态 import,加载前显示 textarea 占位
3. 语言 — 仅注册所需模式(json、sql),非整语言包
4. lint — 接 LanguageServer 或简单 linter 函数到 editor.onDidChange
5. 主题 — 匹配应用主题;Monaco 主题是 JSON,CodeMirror 用 @codemirror/theme-one-dark
6. 值同步 — 受控模式棘手;外部变时用 editor.setValue,防循环
7. worker — Monaco 用 web worker 做语言服务;配置 MonacoEnvironment.getWorkerUrl
示例
import { lazy, Suspense, useState } from "react";
// 仅编辑器挂载时懒加载 Monaco
const MonacoEditor = lazy(() => import("@monaco-editor/react").then((m) => ({ default: m.default })));
function CodeEditor({ value, onChange, language = "json" }: Props) {
const [loaded, setLoaded] = useState(false);
return (
<Suspense fallback={<textarea value={value} onChange={(e) => onChange(e.target.value)}
className="w-full h-64 font-mono" />}>
<MonacoEditor
value={value}
language={language}
theme="vs-dark"
onChange={(v) => onChange(v ?? "")}
onMount={() => setLoaded(true)}
options={{ minimap: { enabled: false }, fontSize: 13, lineNumbers: "on" }}
loading={<div className="grid place-items-center h-64">加载编辑器…</div>}
/>
</Suspense>
);
}
常见问题
- ⚠Monaco 在主包 — 2MB 发给每个用户即使从不开编辑器
- ⚠worker URL 配错 — 语言服务静默失败,无自动补全或诊断
- ⚠受控值循环 — onChange 设状态、setValue 触发 onChange、光标跳
最佳实践
- ✓懒加载编辑器 — 首次用动态 import,textarea 回退保持页面轻
- ✓仅注册所需语言 — Monaco 全语言包巨大,仅 import json/sql
- ✓用非受控模型 — 让编辑器拥有值,提交时读,避免 setValue 循环