CodeMirror 6 在 React 中的深度集成与 SQL 编辑器定制

CodeMirror 6 在 React 中的深度集成与 SQL 编辑器定制

简介

CodeMirror 6(CM6)是新一代的代码编辑器框架,相比 CM5 进行了彻底的重构:基于模块化架构、TypeScript 原生支持、高性能虚拟渲染、可插拔的状态管理。在数据库管理工具、API 调试器、在线 IDE 等场景中,CM6 已成为首选。

本文从零开始,讲解如何在 React 项目中集成 CodeMirror 6,并以此为基础打造一个功能完整的 SQL 编辑器——支持语法高亮、自动补全、多方言切换、主题定制和快捷键绑定。内容基于 Electron + React 19 + Vite 技术栈的实际项目经验。

前置要求

  • Node.js 18+(推荐 20 LTS)
  • React 18+(本文基于 React 19)
  • 熟悉 React Hooks(useState、useEffect、useRef、useCallback)
  • 基本的 TypeScript 知识
  • 已初始化的 React 项目(Vite 或 CRA 均可)

详细步骤

1. 安装 CodeMirror 6 依赖

CM6 采用模块化设计,核心包和扩展包需要分别安装:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 核心包
npm install @codemirror/state @codemirror/view @codemirror/language

# 主题
npm install @codemirror/theme-one-dark

# 语言支持
npm install @codemirror/lang-sql @codemirror/lang-javascript @codemirror/lang-json

# 实用扩展
npm install @codemirror/commands @codemirror/autocomplete @codemirror/search
npm install @codemirror/lint @codemirror/collab @codemirror/fold
npm install @codemirror/highlight @codemirror/language-data

包说明:

包名 作用
@codemirror/state 编辑器状态管理(EditorState)
@codemirror/view 编辑器视图层(EditorView)
@codemirror/language 语言支持基础设施
@codemirror/lang-sql SQL 语言支持(含多种方言)
@codemirror/commands 标准命令集(缩进、注释、补全等)
@codemirror/autocomplete 自动补全框架
@codemirror/search 搜索替换面板
@codemirror/lint 代码检查框架
@codemirror/theme-one-dark One Dark 主题

2. 基础集成:创建可复用的 SQL 编辑器组件

2.1 基础组件骨架

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
// SqlEditor.tsx
import React, { useEffect, useRef } from 'react';
import { EditorState } from '@codemirror/state';
import { EditorView, keymap, lineNumbers, highlightActiveLineGutter } from '@codemirror/view';
import { basicSetup } from 'codemirror';
import { sql, PostgreSQL } from '@codemirror/lang-sql';
import { oneDark } from '@codemirror/theme-one-dark';
import { defaultKeymap, indentWithTab } from '@codemirror/commands';

interface SqlEditorProps {
value: string;
onChange?: (value: string) => void;
dbType?: 'mysql' | 'postgresql' | 'sqlite' | 'mariadb' | 'mssql';
readOnly?: boolean;
height?: string;
placeholder?: string;
}

export function SqlEditor({
value,
onChange,
dbType = 'mysql',
readOnly = false,
height = '300px',
placeholder = '输入 SQL 语句...',
}: SqlEditorProps) {
const editorRef = useRef<HTMLDivElement>(null);
const viewRef = useRef<EditorView | null>(null);

useEffect(() => {
if (!editorRef.current) return;

// 选择 SQL 方言
const sqlLang = sql({ dialect: getDialect(dbType) });

// 构建扩展列表
const extensions = [
basicSetup, // 基础功能(行号、折叠、选中高亮等)
lineNumbers(), // 行号
highlightActiveLineGutter(), // 当前行号高亮
sqlLang, // SQL 语法支持
oneDark, // 暗色主题
keymap.of([...defaultKeymap, indentWithTab]), // 快捷键
EditorView.lineWrapping, // 自动换行
EditorView.updateListener.of((update) => {
if (update.docChanged && onChange) {
onChange(update.state.doc.toString());
}
}),
];

if (readOnly) {
extensions.push(EditorView.editable.of(false));
}

if (placeholder) {
extensions.push(placeholderExtension(placeholder));
}

// 创建编辑器状态
const state = EditorState.create({
doc: value,
extensions,
});

// 创建编辑器视图
const view = new EditorView({
state,
parent: editorRef.current,
});

viewRef.current = view;

// 清理函数
return () => {
view.destroy();
viewRef.current = null;
};
}, [dbType, readOnly, placeholder]); // 注意:不包含 value/onChange

// 外部 value 变化时同步到编辑器
useEffect(() => {
const view = viewRef.current;
if (!view) return;

const currentDoc = view.state.doc.toString();
if (value !== currentDoc) {
view.dispatch({
changes: {
from: 0,
to: currentDoc.length,
insert: value,
},
});
}
}, [value]);

return (
<div
ref={editorRef}
style={{
height,
border: '1px solid #333',
borderRadius: '4px',
overflow: 'hidden',
}}
/>
);
}

// 方言映射函数
function getDialect(dbType: string) {
switch (dbType) {
case 'postgresql': return PostgreSQL;
case 'sqlite': return { dialect: 'sqlite' } as any;
case 'mariadb': return { dialect: 'mysql' } as any;
case 'mssql': return { dialect: 'mssql' } as any;
default: return { dialect: 'mysql' } as any;
}
}

// 占位符扩展(通过 CSS 实现,无需 JS 逻辑)
function placeholderExtension(text: string) {
return EditorView.theme({
'&': {
'&::before': {
content: `"${text}"`,
position: 'absolute',
top: 0,
left: 0,
padding: '4px 8px',
color: '#6c7086',
fontStyle: 'italic',
pointerEvents: 'none',
display: 'none',
},
'&.cm-focused::before': {
display: 'none',
},
},
});
}

2.2 使用组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// App.tsx
import { useState } from 'react';
import { SqlEditor } from './SqlEditor';

function App() {
const [sql, setSql] = useState('SELECT * FROM users WHERE id = 1;');
const [dbType, setDbType] = useState('postgresql');

return (
<div style={{ padding: 20 }}>
<select value={dbType} onChange={(e) => setDbType(e.target.value)}>
<option value="mysql">MySQL</option>
<option value="postgresql">PostgreSQL</option>
<option value="sqlite">SQLite</option>
<option value="mssql">SQL Server</option>
</select>

<SqlEditor
value={sql}
onChange={setSql}
dbType={dbType as any}
height="400px"
placeholder="在此输入 SQL..."
/>

<button onClick={() => executeQuery(sql)}>执行</button>
</div>
);
}

3. 高级定制:自定义 SQL 方言

CM6 的 @codemirror/lang-sql 支持通过 SQLDialect 类创建自定义方言。这在需要支持特定数据库的关键字、内置函数或语法扩展时非常有用。

3.1 创建自定义方言

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import { SQLDialect, sql } from '@codemirror/lang-sql';

// 创建 PostgreSQL 增强方言
const enhancedPgDialect = SQLDialect.define({
// 追加关键字
keyword: [
'MATERIALIZED', 'VIEW', 'REFRESH', 'CONCURRENTLY',
'UPSERT', 'ON_CONFLICT', 'DO_NOTHING', 'DO_UPDATE',
'RETURNING', 'LATERAL', 'WINDOW', 'PARTITION',
'RANGE', 'ROWS', 'GROUPS', 'EXCLUDE',
'RECURSIVE', 'TEMPORARY', 'TEMP', 'UNLOGGED',
'IF', 'NOT', 'EXISTS', 'ILIKE', 'SIMILAR',
],
// 追加内置函数
builtin: [
'JSONB_BUILD_OBJECT', 'JSONB_AGG', 'JSON_BUILD_OBJECT',
'JSON_AGG', 'ARRAY_AGG', 'STRING_AGG',
'ROW_NUMBER', 'RANK', 'DENSE_RANK', 'NTILE',
'LEAD', 'LAG', 'FIRST_VALUE', 'LAST_VALUE',
'COALESCE', 'NULLIF', 'GREATEST', 'LEAST',
'EXTRACT', 'DATE_TRUNC', 'TO_CHAR', 'TO_DATE',
'GEN_RANDOM_UUID', 'PG_SLEEP', 'CURRENT_TIMESTAMP',
],
// 追加类型
atomTypes: [
'SERIAL', 'BIGSERIAL', 'SMALLSERIAL',
'UUID', 'JSONB', 'JSON', 'BYTEA',
'INTERVAL', 'TSVECTOR', 'TSQUERY',
'MACADDR', 'INET', 'CIDR', 'XML',
'INT4RANGE', 'INT8RANGE', 'NUMRANGE',
'TSRANGE', 'TSTZRANGE', 'DATERANGE',
],
});

// 使用自定义方言
const customSqlLang = sql({ dialect: enhancedPgDialect });

3.2 根据数据库类型动态切换方言

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { MySQL, PostgreSQL, SQLite, MSSQL } from '@codemirror/lang-sql';

// 方言注册表
const dialectRegistry: Record<string, SQLDialect> = {
mysql: MySQL,
postgresql: PostgreSQL,
sqlite: SQLite,
mssql: MSSQL,
};

// 在组件中动态切换
function getSqlExtension(dbType: string) {
const dialect = dialectRegistry[dbType] || MySQL;
return sql({ dialect });
}

4. 高级定制:自动补全

4.1 基于数据库 schema 的智能补全

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
import { CompletionContext, CompletionResult } from '@codemirror/autocomplete';
import { snippetCompletion } from '@codemirror/autocomplete';

// 数据库表结构信息
interface TableSchema {
tableName: string;
columns: { name: string; type: string }[];
}

// 生成自动补全源
function createSqlCompletions(tables: TableSchema[]) {
const tableNames = tables.map((t) => t.tableName);
const allColumns = tables.flatMap((t) =>
t.columns.map((c) => ({
label: `${t.tableName}.${c.name}`,
detail: c.type,
type: 'property' as const,
}))
);

// SQL 关键字片段
const snippets = [
snippetCompletion('SELECT ${1:column} FROM ${2:table} WHERE ${3:condition};', {
label: 'SELECT-FROM-WHERE',
detail: '完整查询模板',
type: 'keyword',
}),
snippetCompletion('INSERT INTO ${1:table} (${2:columns}) VALUES (${3:values});', {
label: 'INSERT-INTO',
detail: '插入语句模板',
type: 'keyword',
}),
snippetCompletion('UPDATE ${1:table} SET ${2:column} = ${3:value} WHERE ${4:condition};', {
label: 'UPDATE-SET',
detail: '更新语句模板',
type: 'keyword',
}),
snippetCompletion('DELETE FROM ${1:table} WHERE ${2:condition};', {
label: 'DELETE-FROM',
detail: '删除语句模板',
type: 'keyword',
}),
snippetCompletion('CREATE TABLE ${1:table_name} (\n ${2:id} ${3:INTEGER PRIMARY KEY},\n ${4:name} ${5:TEXT}\n);', {
label: 'CREATE-TABLE',
detail: '建表模板',
type: 'keyword',
}),
];

return function sqlCompletions(context: CompletionContext): CompletionResult | null {
const word = context.matchBefore(/\w*/);
if (!word || (word.from === word.to && !context.explicit)) return null;

return {
from: word.from,
options: [
...snippets,
...tableNames.map((name) => ({
label: name,
type: 'class' as const,
detail: '表',
})),
...allColumns,
],
validFor: /^\w*$/,
};
};
}

// 在编辑器中使用
const completions = createSqlCompletions([
{
tableName: 'users',
columns: [
{ name: 'id', type: 'INTEGER' },
{ name: 'name', type: 'VARCHAR(100)' },
{ name: 'email', type: 'VARCHAR(255)' },
{ name: 'created_at', type: 'TIMESTAMP' },
],
},
{
tableName: 'orders',
columns: [
{ name: 'id', type: 'INTEGER' },
{ name: 'user_id', type: 'INTEGER' },
{ name: 'amount', type: 'DECIMAL(10,2)' },
{ name: 'status', type: 'VARCHAR(20)' },
],
},
]);

const extensions = [
sql({ dialect: PostgreSQL }),
autocompletion({ override: [completions] }),
];

4.2 关键字大小写自动转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { Transaction } from '@codemirror/state';

// 输入时自动将 SQL 关键字转为大写
const autoUpperCase = EditorState.transactionFilter.of((tr: Transaction) => {
if (!tr.docChanged) return tr;

// 获取当前输入的内容
const changes = tr.changes;
if (changes.length === 0) return tr;

// 只处理单字符输入
if (changes.length > 1) return tr;

return tr;
});

5. 高级定制:主题与样式

5.1 自定义主题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
import { EditorView } from '@codemirror/view';
import { HighlightStyle, syntaxHighlighting } from '@codemirror/language';
import { tags } from '@lezer/highlight';

// 自定义暗色主题
const customDarkTheme = EditorView.theme(
{
'&': {
backgroundColor: '#1e1e2e',
color: '#cdd6f4',
fontSize: '14px',
fontFamily: '"JetBrains Mono", "Fira Code", monospace',
},
'.cm-gutters': {
backgroundColor: '#181825',
color: '#6c7086',
border: 'none',
},
'.cm-activeLineGutter': {
backgroundColor: '#313244',
},
'.cm-activeLine': {
backgroundColor: '#31324444',
},
'.cm-cursor': {
borderLeftColor: '#f5c2e7',
},
'.cm-selectionBackground': {
backgroundColor: '#585b7044',
},
'&.cm-focused .cm-selectionBackground': {
backgroundColor: '#585b7088',
},
'.cm-matchingBracket': {
backgroundColor: '#a6e3a144',
outline: '1px solid #a6e3a1',
},
'.cm-placeholder': {
color: '#6c7086',
fontStyle: 'italic',
},
},
{ dark: true }
);

// 自定义语法高亮颜色
const customHighlightStyle = HighlightStyle.define([
{ tag: tags.keyword, color: '#cba6f7', fontWeight: 'bold' },
{ tag: tags.string, color: '#a6e3a1' },
{ tag: tags.number, color: '#fab387' },
{ tag: tags.comment, color: '#6c7086', fontStyle: 'italic' },
{ tag: tags.function(tags.variableName), color: '#89b4fa' },
{ tag: tags.typeName, color: '#f9e2af' },
{ tag: tags.operator, color: '#89dceb' },
{ tag: tags.bracket, color: '#cdd6f4' },
{ tag: tags.bool, color: '#fab387' },
{ tag: tags.null, color: '#fab387' },
{ tag: tags.builtin, color: '#89b4fa' },
{ tag: tags.variableName, color: '#cdd6f4' },
{ tag: tags.atom, color: '#f9e2af' },
]);

// 组合使用
const customTheme = [
customDarkTheme,
syntaxHighlighting(customHighlightStyle),
];

5.2 亮色/暗色主题切换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function useCodeMirrorTheme(isDark: boolean) {
return useMemo(() => {
if (isDark) {
return [oneDarkTheme, syntaxHighlighting(oneDarkHighlightStyle)];
}
return [defaultTheme, syntaxHighlighting(defaultHighlightStyle)];
}, [isDark]);
}

// 在组件中使用
function ThemedSqlEditor(props: SqlEditorProps) {
const { theme } = useAppTheme(); // 自定义 hook
const themeExtensions = useCodeMirrorTheme(theme === 'dark');

// 将 themeExtensions 合并到编辑器的 extensions 数组中
}

5.3 使用 Catppuccin 主题(社区推荐)

1
npm install @val-town/codemirror-theme-catppuccin
1
2
3
4
5
6
7
import { catppuccinLatte, catppuccinMocha } from '@val-town/codemirror-theme-catppuccin';

// 亮色模式
const extensions = [catppuccinLatte];

// 暗色模式
const extensions = [catppuccinMocha];

6. 高级定制:快捷键与命令

6.1 自定义快捷键

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import { keymap } from '@codemirror/view';

// SQL 执行快捷键
const sqlKeymap = keymap.of([
{
key: 'Ctrl-Enter',
run: (view) => {
// 触发 SQL 执行事件
const event = new CustomEvent('execute-sql', {
detail: { sql: view.state.doc.toString() },
});
window.dispatchEvent(event);
return true;
},
},
{
key: 'Shift-Ctrl-Enter',
run: (view) => {
// 格式化 SQL
const sql = view.state.doc.toString();
const formattedSql = formatSql(sql);
view.dispatch({
changes: { from: 0, to: view.state.doc.length, insert: formattedSql },
});
return true;
},
},
{
key: 'Ctrl-Space',
run: (view) => {
// 触发自动补全
autocompletionStart(view);
return true;
},
},
{
key: 'Ctrl-/',
run: (view) => {
// 注释/取消注释当前行
toggleComment(view);
return true;
},
},
]);

6.2 Ctrl+Enter 执行查询的完整实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 在 SqlEditor 组件中添加执行回调
interface SqlEditorProps {
// ... 其他 props
onExecute?: (sql: string) => void;
}

// 在扩展中添加快捷键
const executeKeymap = keymap.of([
{
key: 'Ctrl-Enter',
run: (view) => {
if (onExecute) {
onExecute(view.state.doc.toString());
}
return true;
},
},
]);

// 在父组件中使用
<SqlEditor
value={sql}
onChange={setSql}
onExecute={(sqlText) => {
executeQuery(sqlText);
}}
/>

7. 性能优化与最佳实践

7.1 编辑器懒加载

对于列表中的多个编辑器(如多标签页),使用虚拟渲染或懒加载:

1
2
3
4
5
6
7
// 只有标签被激活时才创建编辑器实例
function LazySqlEditor({ isActive, ...props }: LazySqlEditorProps) {
if (!isActive) {
return <div style={{ height: props.height }} />;
}
return <SqlEditor {...props} />;
}

7.2 大文档优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 限制语法高亮的行数范围
const extensions = [
syntaxHighlighting(
highlightStyle,
{ fallback: true }
),
// 折叠超长行
EditorView.theme({
'.cm-line': {
maxWidth: '100%',
overflowX: 'auto',
},
}),
];

7.3 合理管理 EditorView 生命周期

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 正确做法:每次依赖变化时重建 view
useEffect(() => {
// 创建新 view
const view = new EditorView({ ... });
viewRef.current = view;

return () => {
view.destroy();
viewRef.current = null;
};
}, [dbType]); // 方言变化时需要重建

// 错误做法:尝试复用 view 并动态更新配置
// CM6 的配置只能在创建时设置,不能动态修改

8. 常见问题排查

问题 原因 解决方案
编辑器不显示 父容器没有高度 确保容器有明确的 height
内容不同步 value prop 未正确监听 确保第二个 useEffect 正确 dispatch changes
内存泄漏 view.destroy() 未调用 useEffect 返回清理函数
自动补全不工作 未安装 autocomplete 包 npm install @codemirror/autocomplete
中文输入法问题 IME 兼容性 CM6 原生支持 IME,检查是否禁用了 composition
多编辑器冲突 共享同一个 viewRef 每个编辑器使用独立的 ref

总结

本文从基础集成到高级定制,完整讲解了 CodeMirror 6 在 React 中的使用。核心要点:

  1. 模块化架构:CM6 将编辑器拆分为 state、view、language 等独立包,按需加载
  2. React 集成模式:通过 ref 管理 EditorView 生命周期,useEffect 同步外部状态
  3. SQL 编辑器扩展:方言切换、schema 感知的自动补全、snippet 模板
  4. 主题系统:自定义暗色/亮色主题,支持 Catppuccin 等社区主题
  5. 快捷键绑定:Ctrl+Enter 执行查询、Ctrl+Space 触发补全等
  6. 性能优化:懒加载、大文档优化、合理管理生命周期

这套 SQL 编辑器是 DBView 数据库管理工具的核心组件之一,与其搭配的 Electron 桌面应用架构请参考 Electron 35 + React 19 + Vite 桌面应用开发实战