codemirror 是一个轻量级、模块化的代码编辑器核心库(当前主流为 v6 版本),适合需要高度定制和较小体积的场景。monaco-editor 是 VS Code 的核心引擎,功能极其强大但体积较重,适合需要 IDE 级体验的场景。react-codemirror2 是 CodeMirror v5 的 React 封装组件,由于锁定在旧版核心,新项目中需谨慎评估。
在 Web 应用中嵌入代码编辑器是一个常见但复杂的需求。codemirror、monaco-editor 和 react-codemirror2 代表了三种不同的解决思路:现代核心库、重型 IDE 引擎、以及旧版框架封装。它们在设计哲学、集成成本和功能边界上有显著差异。让我们从架构师的角度深入对比。
codemirror (v6) 采用完全模块化的设计。
// codemirror v6: 手动组装扩展
import { EditorState } from "@codemirror/state";
import { EditorView, basicSetup } from "@codemirror/view";
import { javascript } from "@codemirror/lang-javascript";
const state = EditorState.create({
doc: "console.log('Hello')",
extensions: [basicSetup, javascript()]
});
const view = new EditorView({ state, parent: document.body });
monaco-editor 是一个完整的 IDE 引擎。
// monaco-editor: 配置驱动
import * as monaco from 'monaco-editor';
monaco.editor.create(document.getElementById('container'), {
value: "console.log('Hello')",
language: 'javascript',
minimap: { enabled: true },
automaticLayout: true
});
react-codemirror2 是 CodeMirror v5 的 React 封装。
// react-codemirror2: 组件化封装 (CM5)
import { Controlled as ControlledEditor } from 'react-codemirror2';
import 'codemirror/lib/codemirror.css';
function Editor() {
return (
<ControlledEditor
value="console.log('Hello')"
options={{ mode: 'javascript', lineNumbers: true }}
/>
);
}
codemirror (v6) 需要手动绑定 React 生命周期。
useEffect 中初始化 EditorView。// codemirror v6 + React: 手动管理
useEffect(() => {
const view = new EditorView({ state, parent: containerRef.current });
return () => view.destroy(); // 必须手动清理
}, []);
monaco-editor 需要处理 Loader 和生命周期。
create 和 dispose。// monaco-editor + React: 手动管理实例
useEffect(() => {
const editor = monaco.editor.create(containerRef.current, options);
return () => editor.dispose(); // 必须手动销毁
}, []);
react-codemirror2 提供原生 React 组件。
// react-codemirror2: 直接使用组件
<ControlledEditor
onBeforeChange={(editor, data, value) => setValue(value)}
value={value}
/>
codemirror (v6) 拥有强大的扩展系统。
// codemirror v6: 自定义扩展
const myExtension = EditorView.theme({
"&": { backgroundColor: "#f0f0f0" }
});
// 将扩展加入 extensions 数组即可生效
monaco-editor 通过配置和 API 定制。
// monaco-editor: 注册自定义提示
monaco.languages.registerCompletionItemProvider('javascript', {
provideCompletionItems: (model, position) => {
return { suggestions: [...] };
}
});
react-codemirror2 定制能力受限于 CM5。
options 对象配置。// react-codemirror2: 通过 options 配置
<ControlledEditor
options={{
mode: 'javascript',
theme: 'dracula',
lineWrapping: true
}}
/>
这是一个关键的架构决策点。
codemirror (npm 包) 当前默认指向 v6。
react-codemirror2 锁定在 CodeMirror v5。
@uiw/react-codemirror 等社区方案。monaco-editor 独立版本迭代。
| 特性 | codemirror (v6) | monaco-editor | react-codemirror2 |
|---|---|---|---|
| 核心版本 | ✅ 现代 (v6) | ✅ 独立引擎 | ❌ 遗留 (v5) |
| 打包体积 | ⚡ 轻量 (按需加载) | 🐘 重型 (完整 IDE) | ⚡ 轻量 (但含旧核) |
| React 集成 | 🛠️ 手动/社区封装 | 🛠️ 手动/社区封装 | ✅ 开箱即用组件 |
| 定制能力 | 🔓 极高 (扩展系统) | 🔓 高 (配置+API) | 🔒 中 (受限于 v5) |
| 智能提示 | 📦 需自行配置扩展 | ✅ 开箱即用 (TS/JS) | 📦 需自行配置插件 |
| 维护状态 | 🟢 活跃开发 | 🟢 活跃开发 | 🟡 维护模式 (CM5) |
codemirror (v6) 是大多数 Web 应用的最佳平衡点。它轻量、现代,且拥有活跃的生态。虽然 React 集成需要一点样板代码(或使用 @uiw/react-codemirror),但长期维护成本最低。
monaco-editor 是重型武器的选择。如果你正在构建在线 IDE、复杂的 JSON/YAML 配置器,或者用户强烈依赖 VS Code 的操作习惯,它是唯一选择。不要因为它功能多就在简单表单中使用,体积代价太高。
react-codemirror2 应被视为遗留技术。除非你正在维护一个无法迁移的 CM5 项目,否则不要在新项目中引入它。CodeMirror v6 的架构变化太大,无法平滑升级,现在选择 CM5 意味着未来必须重构。
最终思考:编辑器的选择不仅是选一个组件,而是选择一种维护负担。现代项目应优先拥抱模块化架构(CM6)或成熟引擎(Monaco),避免被旧版封装锁定技术栈。
选择 react-codemirror2 仅当你维护基于 CodeMirror v5 的遗留项目。由于它不支持最新的 CodeMirror v6 架构,新项目建议使用 codemirror 核心配合现代 React 封装(如 @uiw/react-codemirror)以获得长期支持。
选择 codemirror 如果你需要现代模块化架构(v6),希望打包体积可控,且需要深度定制编辑器行为。它适合构建轻量级代码输入框、文档编辑器或需要高度灵活性的场景。
选择 monaco-editor 如果你需要开箱即用的 IDE 级功能(如智能提示、重构、迷你地图),且不在乎较大的打包体积。它适合构建在线 IDE、复杂配置文件编辑器或需要 VS Code 体验的场景。
demo @ scniro.github.io/react-codemirror2
npm install react-codemirror2 codemirror --save
react-codemirror2 ships with the notion of an uncontrolled and controlled component. UnControlled consists of a simple wrapper largely powered by the inner workings of codemirror itself, while Controlled will demand state management from the user, preventing codemirror changes unless properly handled via value. The latter will offer more control and likely be more appropriate with redux heavy apps.
import {UnControlled as CodeMirror} from 'react-codemirror2'
<CodeMirror
value='<h1>I ♥ react-codemirror2</h1>'
options={{
mode: 'xml',
theme: 'material',
lineNumbers: true
}}
onChange={(editor, data, value) => {
}}
/>
import {Controlled as CodeMirror} from 'react-codemirror2'
<CodeMirror
value={this.state.value}
options={options}
onBeforeChange={(editor, data, value) => {
this.setState({value});
}}
onChange={(editor, data, value) => {
}}
/>
codemirror comes as a peer dependency, meaning you'll need to require it in your project in addition to react-codemirror2. This prevents any versioning conflicts that would arise if codemirror came as a dependency through this wrapper. It's been observed that version mismatches can cause difficult to trace issues such as syntax highlighting disappearing without any explicit errors/warnings
Since codemirror ships mostly unconfigured, the user is left with the responsibility for requiring any additional resources should they be necessary. This is often the case when specifying certain language modes and themes. How to import/require these assets will vary according to the specifics of your development environment. Below is a sample to include the assets necessary to specify a mode of xml (HTML) and a material theme.
note that the base codemirror.css file is required in all use cases
@import 'codemirror/lib/codemirror.css';
@import 'codemirror/theme/material.css';
import CodeMirror from 'react-codemirror2';
require('codemirror/mode/xml/xml');
require('codemirror/mode/javascript/javascript');
| prop | type default | components | description |
|---|---|---|---|
autoCursor | boolean true | Controlled UnControlled | should component cursor position correct when value changed |
autoScroll | boolean true | Controlled UnControlled | should component scroll cursor position into view when value changed |
className | string | Controlled UnControlled | pass through class class="react-codemirror2 className" |
defineMode | object | Controlled UnControlled | pass a custom mode via {name: 'custom', fn: myModeFn} |
detach | boolean | UnControlled | should component ignore new props |
options | object | Controlled UnControlled | codemirror configuration |
value | string | *Controlled UnControlled | * component value must be managed for controlled components |
cursor - setCursorwill programmatically set cursor to the position specified
<CodeMirror
[...]
cursor={{
line: 5,
ch: 10
}}
onCursor={(editor, data) => {}}
/>
scroll - scrollTowill programmatically scroll to the specified coordinate
<CodeMirror
[...]
scroll={{
x: 50,
y: 50
}}
onScroll={(editor, data) => {}}
/>
selection={{ranges: array<{anchor, head}>, focus?: boolean} - setSelectionswill programmatically select the ranges specified
<CodeMirror
[...]
selection={{
ranges: [{
anchor: {ch: 8, line: 5},
head: {ch: 37, line: 5}
}],
focus: true // defaults false if not specified
}}
onSelection={(editor, data) => {}}
/>
| event | components | description |
|---|---|---|
| editorDidAttach(editor) | UnControlled | component is now responding to new props |
| editorDidConfigure(editor) | Controlled UnControlled | component configuration has been set |
| editorDidDetach(editor) | UnControlled | component is now ignoring new props |
| editorDidMount(editor, [next]) | Controlled UnControlled | * invoking optional next will trigger editorDidConfigure |
| editorWillUnmount(editor) | Controlled UnControlled | invoked before componentWillUnmount |
| onBeforeChange(editor, data, value, [next]) | Controlled UnControlled | * if used, next is returned via UnControlled and must be invoked to trigger onChange |
| onChange(editor, data, value) | Controlled UnControlled | the component value has been changed |
onBlur(editor, event) - bluronContextMenu(editor, event) - contextmenuonCopy(editor) - copyonCursor(editor, data)- cursorActivityonCursorActivity(editor) - cursorActivityonCut(editor) - cutonDblClick(editor, event) - dblclickonDragEnter(editor, event) - dragenteronDragOver(editor, event) - dragoveronDragLeave(editor, event) - dragleaveonDragStart(editor, event) - dragstartonDrop(editor, event) - droponFocus(editor, event) - focusonGutterClick(editor, lineNumber, gutter, event) - gutterClickonInputRead(editor, changeObj) - gutterClickonKeyDown(editor, event) - keydownonKeyHandled(editor, key, event) - keyhandledonKeyPress(editor, event) - keypressonKeyUp(editor, event) - keyuponMouseDown(editor, event) - mousedownonPaste(editor) - pasteonScroll(editor, data) - scrollonSelection(editor, data) - beforeSelectionChangeonTouchStart(editor, event) - touchstartonUpdate(editor, event) - updateonViewportChange(editor, from, to) - viewportChangeYes. react-codemirror2 will prevent rendering in absence of window. You can also force the component to not render via a PREVENT_CODEMIRROR_RENDER global.
The recommended technique to get the instance is to persist the editor returned via event callbacks. There is no static method to get it on demand, e.g. CodeMirror.getInstance(). Example...
constructor() {
this.instance = null;
}
render() {
<CodeMirror editorDidMount={editor => { this.instance = editor }}/>
}
Check out bokuweb/re-resizable. Wrapping your component with <Resizable/>'s works well
Pull Requests are welcome. Be mindful of the available scripts below to help submitting a well-received contribution.
npm run start to run the app on localhost:8000npm run test to ensure tests continue to passnpm run build to generate the demo bundlenote that it's necessary to bump the package.json version prior to final npm run build so we can grab the proposed new version as seen in the demo header. Also note, the core changes are to be made in src/index.tsx as ./index.js and ./index.d.ts are generated