413 lines
13 KiB
Markdown
413 lines
13 KiB
Markdown
# 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 多实例问题。
|