688 lines
15 KiB
Markdown
688 lines
15 KiB
Markdown
# CodeMirror 6 编辑器文档
|
||
|
||
> **项目**: U-Desk
|
||
> **组件**: CodeEditor.vue
|
||
> **更新日期**: 2026-02-05
|
||
> **维护者**: 开发团队
|
||
|
||
---
|
||
|
||
## 📚 目录
|
||
|
||
- [简介](#简介)
|
||
- [版本信息](#版本信息)
|
||
- [核心架构](#核心架构)
|
||
- [主题系统](#主题系统)
|
||
- [语言支持](#语言支持)
|
||
- [API 参考](#api-参考)
|
||
- [最佳实践](#最佳实践)
|
||
- [常见问题](#常见问题)
|
||
- [升级指南](#升级指南)
|
||
- [参考资料](#参考资料)
|
||
|
||
---
|
||
|
||
## 简介
|
||
|
||
### 什么是 CodeMirror 6?
|
||
|
||
CodeMirror 6 是一个**基于 TypeScript 重写的现代代码编辑器**,采用模块化架构,提供:
|
||
|
||
- 🚀 **高性能**: 比 v5 快 40%,内存少 35%
|
||
- 📦 **模块化**: 只加载需要的功能
|
||
- 🎨 **可定制**: 灵活的主题和扩展系统
|
||
- 🔍 **准确**: 基于 Lezer 的语法解析
|
||
- 💪 **类型安全**: 完整的 TypeScript 支持
|
||
|
||
### 为什么选择 CodeMirror 6?
|
||
|
||
| 特性 | CodeMirror 6 | Monaco (VS Code) | Ace |
|
||
|------|--------------|------------------|-----|
|
||
| 包体积 | ~50KB (gzip) | ~2MB | ~300KB |
|
||
| TypeScript | ✅ 原生支持 | ✅ 支持 | ⚠️ 部分 |
|
||
| 模块化 | ✅ 高度模块化 | ❌ 单体 | ⚠️ 中等 |
|
||
| 性能 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ |
|
||
| 移动端 | ✅ 良好支持 | ⚠️ 一般 | ⚠️ 一般 |
|
||
|
||
---
|
||
|
||
## 版本信息
|
||
|
||
### 当前使用的版本
|
||
|
||
```json
|
||
{
|
||
"@codemirror/view": "6.39.8",
|
||
"@codemirror/state": "6.5.3",
|
||
"@codemirror/language": "6.12.1",
|
||
"@codemirror/commands": "6.10.1",
|
||
"@codemirror/highlight": "0.19.8",
|
||
"@codemirror/theme-one-dark": "6.1.3",
|
||
"@codemirror/lang-javascript": "6.2.4",
|
||
"@codemirror/lang-python": "6.2.1",
|
||
"@codemirror/lang-go": "6.0.1",
|
||
"@codemirror/lang-json": "6.0.2",
|
||
"@codemirror/legacy-modes": "6.5.2"
|
||
}
|
||
```
|
||
|
||
### 最新版本(2026-02)
|
||
|
||
- **@codemirror/view**: 6.39.12 (2026-01-30)
|
||
- **@codemirror/state**: 6.5.3 (当前版本)
|
||
- **@codemirror/language**: 6.12.1 (当前版本)
|
||
|
||
> 注:我们的版本略旧但稳定,建议在下次迭代时更新
|
||
|
||
---
|
||
|
||
## 核心架构
|
||
|
||
### 包结构
|
||
|
||
```
|
||
@codemirror/
|
||
├── view/ # 编辑器视图和 DOM 交互
|
||
├── state/ # 编辑器状态和事务
|
||
├── language/ # 语言支持和高亮
|
||
├── commands/ # 内置命令
|
||
├── search/ # 搜索和替换
|
||
├── autocomplete/ # 自动补全
|
||
├── lint/ # 代码检查
|
||
├── lang-*/ # 语言包
|
||
└── legacy-modes/ # 旧版语言模式
|
||
```
|
||
|
||
### 核心概念
|
||
|
||
#### 1. EditorState(状态)
|
||
|
||
编辑器的不可变状态,包含文档内容:
|
||
|
||
```javascript
|
||
import { EditorState } from '@codemirror/state'
|
||
|
||
const state = EditorState.create({
|
||
doc: 'console.log("Hello, World!")',
|
||
extensions: [/* ... */]
|
||
})
|
||
```
|
||
|
||
#### 2. EditorView(视图)
|
||
|
||
编辑器的 UI 表示:
|
||
|
||
```javascript
|
||
import { EditorView } from '@codemirror/view'
|
||
|
||
const view = new EditorView({
|
||
state: state,
|
||
parent: document.body
|
||
})
|
||
```
|
||
|
||
#### 3. Extensions(扩展)
|
||
|
||
配置编辑器功能的核心机制:
|
||
|
||
```javascript
|
||
const extensions = [
|
||
lineNumbers(), // 显示行号
|
||
highlightActiveLine(), // 高亮当前行
|
||
history(), // 撤销/重做
|
||
keymap.of(defaultKeymap) // 键盘映射
|
||
]
|
||
```
|
||
|
||
---
|
||
|
||
## 主题系统
|
||
|
||
### 主题构成
|
||
|
||
CodeMirror 6 的主题由两部分组成:
|
||
|
||
1. **基础样式** (`EditorView.theme`) - UI 元素样式
|
||
2. **高亮样式** (`HighlightStyle.define`) - 语法高亮颜色
|
||
|
||
### 暗色主题(One Dark)
|
||
|
||
```javascript
|
||
import { oneDark } from '@codemirror/theme-one-dark'
|
||
|
||
// 直接使用
|
||
extensions.push(oneDark)
|
||
```
|
||
|
||
### 亮色主题(自定义)
|
||
|
||
```javascript
|
||
import { HighlightStyle } from '@codemirror/language'
|
||
import { tags } from '@lezer/highlight'
|
||
|
||
// 1. 定义语法高亮样式
|
||
const lightHighlightStyle = HighlightStyle.define([
|
||
{ tag: tags.keyword, color: '#d73a49', fontWeight: 'bold' },
|
||
{ tag: tags.string, color: '#032f62' },
|
||
{ tag: tags.number, color: '#005cc5' },
|
||
{ tag: tags.comment, color: '#6a737d', fontStyle: 'italic' },
|
||
{ tag: tags.function(tags.variableName), color: '#6f42c1' },
|
||
{ tag: tags.className, color: '#22863a' },
|
||
{ tag: tags.propertyName, color: '#e36209' },
|
||
{ tag: tags.variableName, color: '#005cc5' }
|
||
])
|
||
|
||
// 2. 定义基础主题样式
|
||
const lightTheme = EditorView.theme({
|
||
'&': { backgroundColor: '#ffffff' },
|
||
'.cm-gutters': { backgroundColor: '#f7f7f7', color: '#999' },
|
||
'.cm-line': { caretColor: '#000' },
|
||
'.cm-selection': { backgroundColor: '#d9d9d9' }
|
||
})
|
||
|
||
// 3. 应用主题
|
||
extensions.push(lightTheme, lightHighlightStyle)
|
||
```
|
||
|
||
### 主题切换
|
||
|
||
```javascript
|
||
import { Compartment } from '@codemirror/state'
|
||
|
||
// 创建主题隔离区
|
||
const themeCompartment = new Compartment()
|
||
|
||
// 初始化
|
||
const view = new EditorView({
|
||
extensions: [
|
||
themeCompartment.of(oneDark) // 初始主题
|
||
]
|
||
})
|
||
|
||
// 切换主题
|
||
function switchTheme(isDark) {
|
||
view.dispatch({
|
||
effects: themeCompartment.reconfigure(
|
||
isDark ? oneDark : lightTheme
|
||
)
|
||
})
|
||
}
|
||
```
|
||
|
||
### 可用的标签(Tags)
|
||
|
||
```javascript
|
||
import { tags } from '@lezer/highlight'
|
||
|
||
// 基础标签
|
||
tags.keyword // 关键字 (const, var, function)
|
||
tags.string // 字符串
|
||
tags.number // 数字
|
||
tags.comment // 注释
|
||
tags.variableName // 变量名
|
||
tags.function // 函数
|
||
tags.className // 类名
|
||
tags.propertyName // 属性名
|
||
tags.operator // 操作符
|
||
tags.tagName // HTML/XML 标签
|
||
tags.attributeName // 属性名
|
||
tags.bool // 布尔值
|
||
tags.null // null 值
|
||
|
||
// 组合标签
|
||
tags.function(tags.variableName) // 函数调用的变量名
|
||
tags.definition(tags.name) // 定义时的名称
|
||
```
|
||
|
||
---
|
||
|
||
## 语言支持
|
||
|
||
### 现代语言包
|
||
|
||
支持 30+ 编程语言,动态加载:
|
||
|
||
```javascript
|
||
// JavaScript/TypeScript
|
||
import { javascript } from '@codemirror/lang-javascript'
|
||
javascript({ jsx: true, typescript: true })
|
||
|
||
// Python
|
||
import { python } from '@codemirror/lang-python'
|
||
python()
|
||
|
||
// Go
|
||
import { go } from '@codemirror/lang-go'
|
||
go()
|
||
|
||
// JSON
|
||
import { json } from '@codemirror/lang-json'
|
||
json()
|
||
|
||
// Markdown
|
||
import { markdown } from '@codemirror/lang-markdown'
|
||
markdown({ codeLanguages: languages })
|
||
```
|
||
|
||
### Legacy 语言包
|
||
|
||
通过 StreamLanguage 包装旧模式:
|
||
|
||
```javascript
|
||
import { StreamLanguage } from '@codemirror/language'
|
||
import { ruby } from '@codemirror/legacy-modes/mode/ruby'
|
||
|
||
StreamLanguage.define(ruby)
|
||
```
|
||
|
||
### 文件扩展名映射
|
||
|
||
```javascript
|
||
const langMap = {
|
||
// JavaScript/TypeScript
|
||
'js': 'javascript', 'jsx': 'javascript',
|
||
'ts': 'typescript', 'tsx': 'typescript',
|
||
|
||
// 样式
|
||
'css': 'css', 'scss': 'css', 'less': 'css',
|
||
|
||
// 数据
|
||
'json': 'json', 'yaml': 'yaml', 'xml': 'xml',
|
||
|
||
// 脚本
|
||
'py': 'python', 'rb': 'ruby', 'sh': 'shell',
|
||
|
||
// 编译型
|
||
'go': 'go', 'rs': 'rust', 'cpp': 'cpp'
|
||
}
|
||
```
|
||
|
||
### 动态加载语言
|
||
|
||
我们的实现使用缓存和动态导入:
|
||
|
||
```javascript
|
||
// 1. 语言缓存
|
||
const languageCache = new Map()
|
||
|
||
// 2. 动态导入
|
||
export async function loadLanguageExtension(language) {
|
||
if (languageCache.has(language)) {
|
||
return languageCache.get(language)
|
||
}
|
||
|
||
try {
|
||
// 动态导入语言包
|
||
const mod = await import(`@codemirror/lang-${language}`)
|
||
const extension = mod[language]()
|
||
|
||
languageCache.set(language, extension)
|
||
return extension
|
||
} catch (error) {
|
||
console.error(`加载语言包失败: ${language}`, error)
|
||
return null
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## API 参考
|
||
|
||
### 核心属性
|
||
|
||
```javascript
|
||
const props = {
|
||
modelValue: String, // 编辑器内容 (v-model)
|
||
fileExtension: String // 文件扩展名 (如 'js', 'py')
|
||
}
|
||
```
|
||
|
||
### 核心事件
|
||
|
||
```javascript
|
||
const emit = {
|
||
'update:modelValue': String // 内容变化时触发
|
||
}
|
||
```
|
||
|
||
### 主要方法
|
||
|
||
#### createEditor(docContent)
|
||
|
||
创建编辑器实例:
|
||
|
||
```javascript
|
||
const createEditor = async (docContent = '') => {
|
||
const extensions = await createExtensions()
|
||
const state = EditorState.create({
|
||
doc: docContent,
|
||
extensions
|
||
})
|
||
view = new EditorView({ state, parent: editorContainer.value })
|
||
}
|
||
```
|
||
|
||
#### recreateEditor()
|
||
|
||
重建编辑器(切换主题/语言时):
|
||
|
||
```javascript
|
||
const recreateEditor = async () => {
|
||
if (!view) return
|
||
const currentDoc = view.state.doc.toString()
|
||
view.destroy()
|
||
await createEditor(currentDoc)
|
||
}
|
||
```
|
||
|
||
#### createExtensions()
|
||
|
||
构建扩展配置:
|
||
|
||
```javascript
|
||
const createExtensions = async () => {
|
||
const extensions = [
|
||
// 基础功能
|
||
lineNumbers(),
|
||
highlightActiveLineGutter(),
|
||
history(),
|
||
keymap.of(defaultKeymap),
|
||
bracketMatching(),
|
||
|
||
// 事件监听
|
||
EditorView.updateListener.of((update) => {
|
||
if (update.docChanged) {
|
||
emit('update:modelValue', update.state.doc.toString())
|
||
}
|
||
}),
|
||
|
||
// 自定义样式
|
||
EditorView.theme({ /* ... */ })
|
||
]
|
||
|
||
// 主题
|
||
if (themeStore.isDark) {
|
||
extensions.push(oneDark)
|
||
} else {
|
||
extensions.push(lightTheme, lightHighlightStyle)
|
||
}
|
||
|
||
// 语言支持
|
||
const language = getLanguageFromExtension(props.fileExtension)
|
||
if (language !== 'text') {
|
||
const langExtension = await loadLanguageExtension(language)
|
||
if (langExtension) {
|
||
extensions.push(langExtension)
|
||
}
|
||
}
|
||
|
||
return extensions
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 最佳实践
|
||
|
||
### 1. 使用 Compartment 动态切换
|
||
|
||
❌ **不好的做法**:重建整个编辑器
|
||
|
||
```javascript
|
||
// 每次切换都重建,性能差
|
||
watch(language, () => {
|
||
view.destroy()
|
||
view = new EditorView({ /* ... */ })
|
||
})
|
||
```
|
||
|
||
✅ **推荐做法**:使用 Compartment
|
||
|
||
```javascript
|
||
const languageCompartment = new Compartment()
|
||
|
||
watch(language, async (newLang) => {
|
||
const lang = await loadLanguageExtension(newLang)
|
||
view.dispatch({
|
||
effects: languageCompartment.reconfigure(lang)
|
||
})
|
||
})
|
||
```
|
||
|
||
### 2. 异步加载语言包
|
||
|
||
```javascript
|
||
// 预加载常用语言
|
||
export async function preloadCommonLanguages() {
|
||
await Promise.all([
|
||
'javascript',
|
||
'json',
|
||
'markdown',
|
||
'python',
|
||
'sql'
|
||
].map(loadLanguageExtension))
|
||
}
|
||
|
||
// 在应用启动时调用
|
||
onMounted(() => {
|
||
preloadCommonLanguages()
|
||
})
|
||
```
|
||
|
||
### 3. 防抖更新
|
||
|
||
```javascript
|
||
import { debounce } from 'lodash-es'
|
||
|
||
const debouncedUpdate = debounce((value) => {
|
||
emit('update:modelValue', value)
|
||
}, 300)
|
||
|
||
EditorView.updateListener.of((update) => {
|
||
if (update.docChanged) {
|
||
debouncedUpdate(update.state.doc.toString())
|
||
}
|
||
})
|
||
```
|
||
|
||
### 4. 内存管理
|
||
|
||
```javascript
|
||
onBeforeUnmount(() => {
|
||
// 务必销毁编辑器
|
||
view?.destroy()
|
||
view = null
|
||
})
|
||
```
|
||
|
||
### 5. 主题持久化
|
||
|
||
```javascript
|
||
// 从 localStorage 读取
|
||
const savedTheme = localStorage.getItem('editor-theme') || 'dark'
|
||
|
||
// 保存主题变化
|
||
watch(theme, (newTheme) => {
|
||
localStorage.setItem('editor-theme', newTheme)
|
||
})
|
||
```
|
||
|
||
---
|
||
|
||
## 常见问题
|
||
|
||
### Q1: 语法高亮不显示?
|
||
|
||
**可能原因**:
|
||
1. 语言扩展未正确加载
|
||
2. 主题样式未配置
|
||
3. 文件扩展名映射错误
|
||
|
||
**解决方案**:
|
||
```javascript
|
||
// 检查语言是否加载
|
||
console.log('Language:', language, 'Extension:', langExtension)
|
||
|
||
// 确保主题包含高亮样式
|
||
extensions.push(lightHighlightStyle)
|
||
```
|
||
|
||
### Q2: 切换主题时编辑器闪烁?
|
||
|
||
**原因**:重建整个编辑器导致
|
||
|
||
**解决方案**:使用 Compartment
|
||
```javascript
|
||
const themeCompartment = new Compartment()
|
||
|
||
view.dispatch({
|
||
effects: themeCompartment.reconfigure(newTheme)
|
||
})
|
||
```
|
||
|
||
### Q3: 大文件性能差?
|
||
|
||
**优化方案**:
|
||
```javascript
|
||
// 虚拟滚动已内置,但可以调整
|
||
const virtualScroll = new Compartment()
|
||
|
||
extensions.push(
|
||
virtualScroll.of({
|
||
// 调整渲染窗口
|
||
viewportMargin: 1000
|
||
})
|
||
)
|
||
```
|
||
|
||
### Q4: 如何添加自定义语言?
|
||
|
||
```javascript
|
||
// 1. 使用 Lezer 定义语法
|
||
import { parser } from '@lezer/generator'
|
||
|
||
// 2. 创建语言包
|
||
import { LanguageSupport } from '@codemirror/language'
|
||
|
||
const myLanguage = new LanguageSupport(parser)
|
||
|
||
// 3. 使用
|
||
extensions.push(myLanguage)
|
||
```
|
||
|
||
---
|
||
|
||
## 升级指南
|
||
|
||
### 从 v5 升级到 v6
|
||
|
||
主要变化:
|
||
|
||
| v5 | v6 |
|
||
|----|----|
|
||
| `CodeMirror(document)` | `new EditorView({ state })` |
|
||
| `{line, ch}` 位置 | 数字偏移量 |
|
||
| `getValue()` / `setValue()` | `state.doc.toString()` / `dispatch()` |
|
||
| `setOption()` | 使用 `Compartment.reconfigure()` |
|
||
|
||
完整迁移指南:https://codemirror.net/docs/migration/
|
||
|
||
### 升级步骤
|
||
|
||
1. **更新依赖**
|
||
```bash
|
||
npm install @codemirror/view@latest @codemirror/state@latest
|
||
```
|
||
|
||
2. **调整 API 调用**
|
||
```javascript
|
||
// v5
|
||
editor.setValue('new content')
|
||
|
||
// v6
|
||
view.dispatch({
|
||
changes: {
|
||
from: 0,
|
||
to: view.state.doc.length,
|
||
insert: 'new content'
|
||
}
|
||
})
|
||
```
|
||
|
||
3. **更新主题系统**
|
||
```javascript
|
||
// v5: 使用 CSS 类
|
||
editor.setOption('theme', 'my-theme')
|
||
|
||
// v6: 使用扩展
|
||
extensions.push(EditorView.theme({ /* ... */ }))
|
||
```
|
||
|
||
---
|
||
|
||
## 参考资料
|
||
|
||
### 官方文档
|
||
|
||
- [📖 CodeMirror 文档首页](https://codemirror.net/docs/)
|
||
- [📚 参考手册](https://codemirror.net/docs/ref/)
|
||
- [🎨 示例:样式定制](https://codemirror.net/examples/styling/)
|
||
- [⚙️ 示例:配置](https://codemirror.net/examples/config/)
|
||
- [📝 变更日志](https://www.codemirror.net/docs/changelog/)
|
||
|
||
### 社区资源
|
||
|
||
- [CodeMirror 6 快速入门](https://discuss.codemirror.net/t/codemirror-6-quickstart-and-learn-by-examples/5375)
|
||
- [构建代码编辑器教程](https://davidmyers.dev/blog/how-to-build-a-code-editor-with-codemirror-6-and-typescript/introduction)
|
||
- [Material UI 集成](https://www.bayanbennett.com/posts/styling-codemirror-v6-with-material-ui-devlog-005/)
|
||
- [中文入门教程](https://segmentfault.com/a/1190000043463221)
|
||
|
||
### 相关包
|
||
|
||
- [@codemirror/language-data](https://github.com/codemirror/language-data) - 文件类型检测
|
||
- [@uiw/react-codemirror](https://www.npmjs.com/package/@uiw/react-codemirror) - React 封装
|
||
|
||
### 论坛讨论
|
||
|
||
- [优雅支持多种语言](https://discuss.codemirror.net/t/elegant-way-to-support-a-ton-of-languages/3600)
|
||
- [动态加载语法高亮](https://codemirror.net/docs/ref/#lang.StreamLanguage)
|
||
- [主题系统设计讨论](https://discuss.codemirror.net/t/styling-and-theming-design-discussion/2958)
|
||
|
||
---
|
||
|
||
## 维护日志
|
||
|
||
### 2026-02-05
|
||
- ✅ 修复亮色主题语法高亮问题
|
||
- ✅ 添加自定义亮色主题支持
|
||
- ✅ 创建完整的技术文档
|
||
|
||
### 未来计划
|
||
- [ ] 升级到最新版本(6.39.12)
|
||
- [ ] 添加更多主题选项
|
||
- [ ] 支持自定义快捷键
|
||
- [ ] 添加代码折叠功能
|
||
- [ ] 集成 LSP(语言服务器协议)
|
||
- [ ] 性能优化(大文件处理)
|
||
|
||
---
|
||
|
||
## 相关文件
|
||
|
||
```
|
||
frontend/src/
|
||
├── components/
|
||
│ └── CodeEditor.vue # 主编辑器组件
|
||
├── utils/
|
||
│ └── codeMirrorLoader.js # 语言包动态加载
|
||
└── stores/
|
||
└── theme.js # 主题状态管理
|
||
```
|
||
|
||
---
|
||
|
||
**文档维护**: 开发团队
|
||
**最后更新**: 2026-02-05
|
||
**版本**: 1.0.0
|