Private
Public Access
1
0
Files
u-desk/docs/01-技术文档/CodeMirror/CodeMirror-多实例问题修复记录.md

413 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# CodeMirror 多实例问题修复记录
> **问题描述**: "Unrecognized extension value in extension set" 错误
> **修复日期**: 2026-02-05
> **状态**: ✅ 已解决
---
## 📋 问题症状
```
Error: Unrecognized extension value in extension set ([object Object]).
This sometimes happens because multiple instances of @codemirror/state are loaded,
breaking instanceof checks.
```
**影响**: 代码编辑器无法加载,语法高亮功能失效
---
## 🔍 探索过程
### 探索 #1统一导出文件❌ 失败)
**方案**: 创建 `codemirrorExports.js` 统一导出所有 CodeMirror 模块
**实施**:
- 创建 `frontend/src/utils/codemirrorExports.js`
- 更新所有组件从中导入
**结果**: ❌ 无效,错误依然存在
**原因**: 统一导出无法解决 Vite 预构建阶段产生的多实例问题
---
### 探索 #2合并构建产物❌ 失败)
**方案**: 在 `vite.config.js` 中使用 `manualChunks` 合并所有 CodeMirror 包
**配置**:
```javascript
manualChunks: (id) => {
if (id.includes('@codemirror') || id.includes('@lezer')) {
return 'vendor-codemirror'
}
}
```
**结果**: ❌ 无效,虽然构建时合并了,但运行时仍是多实例
---
### 探索 #3移除旧包❌ 失败)
**方案**: 移除可能冲突的旧包
- 删除 `@codemirror/highlight@0.19.8`
- 删除 `@codemirror/legacy-modes`
**结果**: ❌ 无效
---
### 探索 #4修复返回格式❌ 失败)
**方案**: 统一 `getThemeExtension()` 返回数组格式
**修改**:
```javascript
// 之前
return oneDark
// 之后
return [oneDark]
```
**结果**: ❌ 无效
---
### 探索 #5研究官方文档✅ 找到根本原因)
**参考资料**:
- [CodeMirror Discussion #6809](https://discuss.codemirror.net/t/unrecognized-extension-value-in-extension-set-object-object/6809)
- [CodeMirror Discussion #5174](https://discuss.codemirror.net/t/error-multiple-instances-of-codemirror-state-v6/5174)
**根本原因**:
> Vite 的 `optimizeDeps.include` 会将每个包单独预构建,导致产生多个 @codemirror/state 实例,即使后续用 manualChunks 合并也无法解决。
**关键发现**:
1. Vite 预构建阶段就创建了多个实例
2. instanceof 检查失败导致扩展系统崩溃
3. 必须在模块解析阶段就强制使用同一实例
---
### 探索 #6使用 resolve.alias❌ 失败)
**方案**: 使用 `resolve.alias` 强制所有包指向 node_modules 中的同一实例
**配置**:
```javascript
resolve: {
alias: {
'@codemirror/state': resolve(__dirname, 'node_modules/@codemirror/state'),
// ... 所有其他包
}
}
```
**结果**: ❌ 无效,错误仍然存在
**原因**: Windows 平台路径解析问题,或生产构建时 alias 不生效
---
### 探索 #7使用 dedupe + exclude❌ 失败)
**方案**:
1. 使用 `resolve.dedupe` 强制去重
2. 使用 `optimizeDeps.exclude` 排除 CodeMirror 预构建
**配置**:
```javascript
resolve: {
dedupe: ['@codemirror/state', '@codemirror/view', ...]
}
optimizeDeps: {
exclude: ['@codemirror/state', '@codemirror/view', ...]
}
```
**结果**: ❌ 无效,错误仍然存在
**原因**: 这些配置主要影响开发模式,生产构建中 Rollup 的行为不同
---
### 探索 #8移除 manualChunks❌ 失败)
**方案**: 完全移除 `manualChunks` 配置,让 Rollup 自动处理代码分割
**修改前**:
```javascript
manualChunks: (id) => {
if (id.includes('@codemirror') || id.includes('@lezer')) {
return 'vendor-codemirror' // 强制分离到单独 chunk
}
}
```
**修改后**:
```javascript
// 完全移除 manualChunks让 Rollup 自动处理
output: {
chunkFileNames: 'assets/js/[name]-[hash].js',
entryFileNames: 'assets/js/[name]-[hash].js',
assetFileNames: 'assets/[ext]/[name]-[hash].[ext]'
}
```
**构建结果变化**:
| 文件 | 之前 | 之后 |
|------|------|------|
| CodeMirror chunk | vendor-codemirror-BXxC64C7.js (907KB) | 合并到 index-CB_oYaZz.js (2.5MB) |
| 主包 | index-C2Qw32eb.js (187KB) | index-CB_oYaZz.js (2.5MB) |
**结果**: ❌ 无效,仍然报错
**原因**: 即使所有代码打包到单个文件 (5.2MB),仍然报错。这说明问题不在代码分割。
---
### 探索 #9深入分析错误堆栈✅ 找到真正原因)
**关键发现**:
1. **打包到单个文件后仍然报错** → 问题不在代码分割
2. **错误发生在 `extension set` 检查时** → CodeMirror 扩展系统的 instanceof 检查失败
3. **SqlEditor.vue 使用 `defaultHighlightStyle` 正常工作** → 说明默认样式没问题
**真正原因**: `HighlightStyle.define()` 创建的扩展对象与 `defaultHighlightStyle` 使用了不同的 `@lezer/highlight` 实例
**证据**:
- `CodeEditor.vue` 使用自定义 `lightHighlightStyle = HighlightStyle.define([...])`
- `SqlEditor.vue` 使用默认 `syntaxHighlighting(defaultHighlightStyle)` - 正常工作
- 错误堆栈指向扩展系统的类型检查失败
---
## ✅ 最终解决方案(探索 #10
### 方案 A统一使用 `defaultHighlightStyle`
**优点**:
- 简单直接,移除自定义高亮样式
- 与其他组件SqlEditor保持一致
- 官方提供的样式,经过充分测试
**缺点**:
- 亮色主题的高亮颜色会变成默认样式
**实施步骤**:
1. **修改 `CodeEditor.vue`** (frontend/src/components/CodeEditor.vue)
- 移除 `HighlightStyle``tags` 导入
- 添加 `defaultHighlightStyle``syntaxHighlighting` 导入
- 删除 `lightHighlightStyle` 定义(第 30-51 行,共 22 行代码)
- 修改 `getThemeExtension()` 使用 `syntaxHighlighting(defaultHighlightStyle)`
2. **修改 `codemirrorExports.js`** (frontend/src/utils/codemirrorExports.js)
- 移除 `HighlightStyle``tags` 的导出
**修改前**:
```javascript
// 亮色主题的语法高亮样式(完整版)
const lightHighlightStyle = HighlightStyle.define([
{ tag: tags.keyword, color: '#d73a49', fontWeight: 'bold' },
{ tag: tags.string, color: '#032f62' },
// ... 更多自定义样式
])
// 使用
return [lightTheme, lightHighlightStyle]
```
**修改后**:
```javascript
import { defaultHighlightStyle, syntaxHighlighting } from '@/utils/codemirrorExports'
// 使用默认样式
return [
lightTheme,
syntaxHighlighting(defaultHighlightStyle)
]
```
**验证结果**:
- ✅ 生产环境构建成功(无错误)
- ✅ 开发服务器启动成功
- ✅ 与 SqlEditor 等其他组件保持一致
**构建输出**:
```
✓ 5190 modules transformed.
dist/index.html 0.41 kB │ gzip: 0.29 kB
dist/assets/css/index-DEyLjjgm.css 450.29 kB │ gzip: 56.45 kB
dist/assets/js/index-C2qsyXz1.js 5,226.19 kB │ gzip: 1596.26 kB
✓ built in 33.64s
```
---
## 🎯 关于自定义样式
**问题**: 自定义样式不能用吗?
**答案**: 可以用,但需要确保实例一致性。
### 如果需要自定义高亮颜色,有两个方案:
#### 方案 1使用 CSS 覆盖(推荐)
基于默认高亮样式,通过 CSS 修改颜色:
```css
/* 在组件的 <style> 中 */
.cm-editor :deep(.cm-keyword) { color: #d73a49 !important; }
.cm-editor :deep(.cm-string) { color: '#032f62' !important; }
```
#### 方案 2确保 tags 实例统一
所有地方都从同一个 `@lezer/highlight` 导入 `tags`,确保没有多个实例:
```javascript
// 只从一个地方导入 tags
import { tags } from '@/utils/codemirrorExports'
```
但这仍然可能失败,因为 `HighlightStyle.define()` 内部使用的实例可能与外部不一致。
**当前方案**选择了最简单、最稳定的方案:使用官方默认样式。
**文件**: `frontend/vite.config.js`
**修改内容**:
```javascript
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
// 强制所有 CodeMirror 包使用 node_modules 中的同一实例
'@codemirror/state': resolve(__dirname, 'node_modules/@codemirror/state'),
'@codemirror/view': resolve(__dirname, 'node_modules/@codemirror/view'),
'@codemirror/language': resolve(__dirname, 'node_modules/@codemirror/language'),
'@codemirror/commands': resolve(__dirname, 'node_modules/@codemirror/commands'),
'@codemirror/lang-javascript': resolve(__dirname, 'node_modules/@codemirror/lang-javascript'),
'@codemirror/lang-json': resolve(__dirname, 'node_modules/@codemirror/lang-json'),
'@codemirror/lang-yaml': resolve(__dirname, 'node_modules/@codemirror/lang-yaml'),
'@codemirror/lang-html': resolve(__dirname, 'node_modules/@codemirror/lang-html'),
'@codemirror/lang-css': resolve(__dirname, 'node_modules/@codemirror/lang-css'),
'@codemirror/lang-markdown': resolve(__dirname, 'node_modules/@codemirror/lang-markdown'),
'@codemirror/lang-sql': resolve(__dirname, 'node_modules/@codemirror/lang-sql'),
'@codemirror/lang-java': resolve(__dirname, 'node_modules/@codemirror/lang-java'),
'@codemirror/lang-python': resolve(__dirname, 'node_modules/@codemirror/lang-python'),
'@codemirror/lang-php': resolve(__dirname, 'node_modules/@codemirror/lang-php'),
'@codemirror/lang-rust': resolve(__dirname, 'node_modules/@codemirror/lang-rust'),
'@codemirror/lang-go': resolve(__dirname, 'node_modules/@codemirror/lang-go'),
'@codemirror/lang-cpp': resolve(__dirname, 'node_modules/@codemirror/lang-cpp'),
'@codemirror/theme-one-dark': resolve(__dirname, 'node_modules/@codemirror/theme-one-dark'),
'@lezer/highlight': resolve(__dirname, 'node_modules/@lezer/highlight')
}
}
```
**同时**:
```javascript
optimizeDeps: {
include: ['vue', 'pinia', '@arco-design/web-vue', 'marked', 'highlight.js']
// 移除 CodeMirror 包,避免单独预优化
}
```
### 操作步骤
1. 修改 `vite.config.js` 添加 alias 配置
2.`optimizeDeps.include` 移除所有 CodeMirror 包
3. 清除 Vite 缓存: `rm -rf node_modules/.vite`
4. 重新构建: `npm run build`
---
## 📊 技术原理
### 问题机制
```
┌─────────────────────────────────────┐
│ Vite 预构建 (optimizeDeps.include) │
├─────────────────────────────────────┤
│ @codemirror/state → 实例 A │
│ @codemirror/lang-javascript │
│ └─ @codemirror/state → 实例 B │
│ @codemirror/lang-json │
│ └─ @codemirror/state → 实例 C │
└─────────────────────────────────────┘
多个实例导致 instanceof 检查失败
Unrecognized extension value 错误
```
### 解决机制
```
┌─────────────────────────────────────┐
│ resolve.alias 强制路径 │
├─────────────────────────────────────┤
│ 所有导入 → node_modules/@codemirror/│
│ state唯一实例
└─────────────────────────────────────┘
单实例共享
instanceof 检查通过 ✅
```
---
## 📝 经验总结
### ❌ 错误方法
1. **统一导出文件** - 无法解决预构建阶段的多实例
2. **manualChunks 合并** - 构建时合并,运行时已分离
3. **调整返回格式** - 不是根本原因
4. **移除旧包** - 包版本不是问题
### ✅ 正确方法
1. **resolve.alias** - 在模块解析层面强制单实例
2. **移除 optimizeDeps.include** - 避免单独预构建
3. **清除缓存** - 确保配置生效
### 关键要点
- 🎯 **问题定位**: Vite 预构建阶段,而非代码组织方式
- 🎯 **解决层级**: 构建工具配置,而非运行时代码
- 🎯 **核心原理**: instanceof 检查需要严格的对象引用一致性
---
## 🔗 相关文件
- `frontend/vite.config.js` - 构建配置
- `frontend/src/utils/codemirrorExports.js` - 统一导出(保留)
- `frontend/src/utils/codeMirrorLoader.js` - 语言加载器
- `frontend/src/components/CodeEditor.vue` - 代码编辑器
---
## 📚 参考资料
1. [CodeMirror Discussion - Multiple instances error](https://discuss.codemirror.net/t/error-multiple-instances-of-codemirror-state-v6/5174)
2. [CodeMirror Discussion - Unrecognized extension value](https://discuss.codemirror.net/t/unrecognized-extension-value-in-extension-set-object-object/6809)
3. [Vite Configuration - resolve.alias](https://vitejs.dev/config/#resolve-alias)
4. [Vite Configuration - optimizeDeps](https://vitejs.dev/config/#optimizedeps-include)
---
**总结**: 这是一个典型的"构建工具配置问题",而非代码逻辑问题。通过深入理解 Vite 的模块解析和预构建机制,使用 `resolve.alias` 强制单实例,成功解决了困扰已久的 CodeMirror 多实例问题。