13 KiB
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 包
配置:
manualChunks: (id) => {
if (id.includes('@codemirror') || id.includes('@lezer')) {
return 'vendor-codemirror'
}
}
结果: ❌ 无效,虽然构建时合并了,但运行时仍是多实例
探索 #3:移除旧包(❌ 失败)
方案: 移除可能冲突的旧包
- 删除
@codemirror/highlight@0.19.8 - 删除
@codemirror/legacy-modes
结果: ❌ 无效
探索 #4:修复返回格式(❌ 失败)
方案: 统一 getThemeExtension() 返回数组格式
修改:
// 之前
return oneDark
// 之后
return [oneDark]
结果: ❌ 无效
探索 #5:研究官方文档(✅ 找到根本原因)
参考资料:
根本原因:
Vite 的
optimizeDeps.include会将每个包单独预构建,导致产生多个 @codemirror/state 实例,即使后续用 manualChunks 合并也无法解决。
关键发现:
- Vite 预构建阶段就创建了多个实例
- instanceof 检查失败导致扩展系统崩溃
- 必须在模块解析阶段就强制使用同一实例
探索 #6:使用 resolve.alias(❌ 失败)
方案: 使用 resolve.alias 强制所有包指向 node_modules 中的同一实例
配置:
resolve: {
alias: {
'@codemirror/state': resolve(__dirname, 'node_modules/@codemirror/state'),
// ... 所有其他包
}
}
结果: ❌ 无效,错误仍然存在
原因: Windows 平台路径解析问题,或生产构建时 alias 不生效
探索 #7:使用 dedupe + exclude(❌ 失败)
方案:
- 使用
resolve.dedupe强制去重 - 使用
optimizeDeps.exclude排除 CodeMirror 预构建
配置:
resolve: {
dedupe: ['@codemirror/state', '@codemirror/view', ...]
}
optimizeDeps: {
exclude: ['@codemirror/state', '@codemirror/view', ...]
}
结果: ❌ 无效,错误仍然存在
原因: 这些配置主要影响开发模式,生产构建中 Rollup 的行为不同
探索 #8:移除 manualChunks(❌ 失败)
方案: 完全移除 manualChunks 配置,让 Rollup 自动处理代码分割
修改前:
manualChunks: (id) => {
if (id.includes('@codemirror') || id.includes('@lezer')) {
return 'vendor-codemirror' // 强制分离到单独 chunk
}
}
修改后:
// 完全移除 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:深入分析错误堆栈(✅ 找到真正原因)
关键发现:
- 打包到单个文件后仍然报错 → 问题不在代码分割
- 错误发生在
extension set检查时 → CodeMirror 扩展系统的 instanceof 检查失败 - SqlEditor.vue 使用
defaultHighlightStyle正常工作 → 说明默认样式没问题
真正原因: HighlightStyle.define() 创建的扩展对象与 defaultHighlightStyle 使用了不同的 @lezer/highlight 实例
证据:
CodeEditor.vue使用自定义lightHighlightStyle = HighlightStyle.define([...])SqlEditor.vue使用默认syntaxHighlighting(defaultHighlightStyle)- 正常工作- 错误堆栈指向扩展系统的类型检查失败
✅ 最终解决方案(探索 #10)
方案 A:统一使用 defaultHighlightStyle
优点:
- 简单直接,移除自定义高亮样式
- 与其他组件(SqlEditor)保持一致
- 官方提供的样式,经过充分测试
缺点:
- 亮色主题的高亮颜色会变成默认样式
实施步骤:
-
修改
CodeEditor.vue(frontend/src/components/CodeEditor.vue)- 移除
HighlightStyle和tags导入 - 添加
defaultHighlightStyle和syntaxHighlighting导入 - 删除
lightHighlightStyle定义(第 30-51 行,共 22 行代码) - 修改
getThemeExtension()使用syntaxHighlighting(defaultHighlightStyle)
- 移除
-
修改
codemirrorExports.js(frontend/src/utils/codemirrorExports.js)- 移除
HighlightStyle和tags的导出
- 移除
修改前:
// 亮色主题的语法高亮样式(完整版)
const lightHighlightStyle = HighlightStyle.define([
{ tag: tags.keyword, color: '#d73a49', fontWeight: 'bold' },
{ tag: tags.string, color: '#032f62' },
// ... 更多自定义样式
])
// 使用
return [lightTheme, lightHighlightStyle]
修改后:
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 修改颜色:
/* 在组件的 <style> 中 */
.cm-editor :deep(.cm-keyword) { color: #d73a49 !important; }
.cm-editor :deep(.cm-string) { color: '#032f62' !important; }
方案 2:确保 tags 实例统一
所有地方都从同一个 @lezer/highlight 导入 tags,确保没有多个实例:
// 只从一个地方导入 tags
import { tags } from '@/utils/codemirrorExports'
但这仍然可能失败,因为 HighlightStyle.define() 内部使用的实例可能与外部不一致。
当前方案选择了最简单、最稳定的方案:使用官方默认样式。
文件: frontend/vite.config.js
修改内容:
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')
}
}
同时:
optimizeDeps: {
include: ['vue', 'pinia', '@arco-design/web-vue', 'marked', 'highlight.js']
// 移除 CodeMirror 包,避免单独预优化
}
操作步骤
- 修改
vite.config.js添加 alias 配置 - 从
optimizeDeps.include移除所有 CodeMirror 包 - 清除 Vite 缓存:
rm -rf node_modules/.vite - 重新构建:
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 检查通过 ✅
📝 经验总结
❌ 错误方法
- 统一导出文件 - 无法解决预构建阶段的多实例
- manualChunks 合并 - 构建时合并,运行时已分离
- 调整返回格式 - 不是根本原因
- 移除旧包 - 包版本不是问题
✅ 正确方法
- resolve.alias - 在模块解析层面强制单实例
- 移除 optimizeDeps.include - 避免单独预构建
- 清除缓存 - 确保配置生效
关键要点
- 🎯 问题定位: Vite 预构建阶段,而非代码组织方式
- 🎯 解决层级: 构建工具配置,而非运行时代码
- 🎯 核心原理: instanceof 检查需要严格的对象引用一致性
🔗 相关文件
frontend/vite.config.js- 构建配置frontend/src/utils/codemirrorExports.js- 统一导出(保留)frontend/src/utils/codeMirrorLoader.js- 语言加载器frontend/src/components/CodeEditor.vue- 代码编辑器
📚 参考资料
- CodeMirror Discussion - Multiple instances error
- CodeMirror Discussion - Unrecognized extension value
- Vite Configuration - resolve.alias
- Vite Configuration - optimizeDeps
总结: 这是一个典型的"构建工具配置问题",而非代码逻辑问题。通过深入理解 Vite 的模块解析和预构建机制,使用 resolve.alias 强制单实例,成功解决了困扰已久的 CodeMirror 多实例问题。