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

13 KiB
Raw Blame History

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 合并也无法解决。

关键发现:

  1. Vite 预构建阶段就创建了多个实例
  2. instanceof 检查失败导致扩展系统崩溃
  3. 必须在模块解析阶段就强制使用同一实例

探索 #6使用 resolve.alias 失败)

方案: 使用 resolve.alias 强制所有包指向 node_modules 中的同一实例

配置:

resolve: {
  alias: {
    '@codemirror/state': resolve(__dirname, 'node_modules/@codemirror/state'),
    // ... 所有其他包
  }
}

结果: 无效,错误仍然存在

原因: Windows 平台路径解析问题,或生产构建时 alias 不生效


探索 #7使用 dedupe + exclude 失败)

方案:

  1. 使用 resolve.dedupe 强制去重
  2. 使用 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深入分析错误堆栈 找到真正原因)

关键发现:

  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)

    • 移除 HighlightStyletags 导入
    • 添加 defaultHighlightStylesyntaxHighlighting 导入
    • 删除 lightHighlightStyle 定义(第 30-51 行,共 22 行代码)
    • 修改 getThemeExtension() 使用 syntaxHighlighting(defaultHighlightStyle)
  2. 修改 codemirrorExports.js (frontend/src/utils/codemirrorExports.js)

    • 移除 HighlightStyletags 的导出

修改前:

// 亮色主题的语法高亮样式(完整版)
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 包,避免单独预优化
}

操作步骤

  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
  2. CodeMirror Discussion - Unrecognized extension value
  3. Vite Configuration - resolve.alias
  4. Vite Configuration - optimizeDeps

总结: 这是一个典型的"构建工具配置问题",而非代码逻辑问题。通过深入理解 Vite 的模块解析和预构建机制,使用 resolve.alias 强制单实例,成功解决了困扰已久的 CodeMirror 多实例问题。