JSPM

  • Created
  • Published
  • Downloads 278
  • Score
    100M100P100Q90455F
  • License MIT

A Vue library for integrating Monaco Editor with Shiki syntax highlighting, supporting real-time updates.

Package Exports

  • vue-use-monaco

Readme

vue-use-monaco

npm license

项目简介

vue-use-monaco 是一个结合 Vue、Monaco 编辑器和 Shiki 语法高亮的组合式函数库,专为流式输入更新和高效代码高亮而设计。它提供了完整的 Monaco 编辑器集成方案,适用于需要实时代码编辑和高亮的场景。

IMPORTANT: Since v0.0.32 the library enables a default time-based throttle for updateCode (updateThrottleMs = 50) to reduce CPU usage under high-frequency streaming. Set updateThrottleMs: 0 in useMonaco() options to restore previous behavior (only RAF-based coalescing).

特性

  • 🚀 开箱即用 - 基于 Vue 3 组合式 API 设计
  • 🎨 Shiki 高亮 - 使用 Shiki 实现高效的语法高亮,支持 TextMate 语法和 VS Code 主题
  • 🌓 主题切换 - 自动监听 isDark 模式变化,智能切换明暗主题
  • 📝 流式更新 - 支持流式输入更新,实时响应代码变化
  • 🔀 Diff 编辑器 - 一行 API 创建 Monaco Diff Editor,支持流式/增量更新 original/modified
  • 🗑️ 内存管理 - 自动销毁编辑器实例,避免内存泄漏
  • 🔧 高度可配置 - 支持所有 Monaco 编辑器原生配置选项
  • 🎯 TypeScript 支持 - 完整的 TypeScript 类型定义

快速 API 概览

本库现在在包根导出了若干与主题/高亮器相关的辅助函数,便于高级用法:

  • registerMonacoThemes(themes, languages): Promise<Highlighter> — 使用 shiki 创建或获取高亮器并把主题注册到 Monaco,返回解析为 shiki highlighter 的 Promise,便于复用(例如渲染页面片段)。 getOrCreateHighlighter(themes, languages): Promise<Highlighter> — 直接获取或创建一个 highlighter(并受内部缓存管理)。如需直接控制 shiki highlighter(例如调用 codeToHtmlsetTheme),请使用此方法并自行处理加载/错误逻辑。

注意:如果你只使用 Monaco 编辑器并在 createEditor 时传入了全量 themes,通常只需调用 monaco.editor.setTheme(themeName) 即可。

配置:useMonaco() 不会自动同步 Shiki highlighter;如果你需要在切换主题时同步页面中独立的 Shiki 渲染,请手动使用 getOrCreateHighlighter(...) 并调用高亮器实例的 setTheme

安装

使用 pnpm 安装:

pnpm add vue-use-monaco

使用 npm 安装:

npm install vue-use-monaco

使用 yarn 安装:

yarn add vue-use-monaco

基础使用

简单示例

<script setup lang="ts">
import { onMounted, ref, watch } from 'vue'
import { useMonaco } from 'vue-use-monaco'

const props = defineProps<{
  code: string
  language: string
}>()

const codeEditor = ref<HTMLElement>()

const { createEditor, updateCode, cleanupEditor } = useMonaco({
  themes: ['vitesse-dark', 'vitesse-light'],
  languages: ['javascript', 'typescript', 'vue', 'python'],
  readOnly: false,
  MAX_HEIGHT: 600,
})

// 创建编辑器实例
onMounted(async () => {
  if (codeEditor.value) {
    await createEditor(codeEditor.value, props.code, props.language)
  }
})

// 监听代码和语言变化
watch(
  () => [props.code, props.language],
  ([newCode, newLanguage]) => {
    updateCode(newCode, newLanguage)
  },
)
</script>

<template>
  <div ref="codeEditor" class="monaco-editor-container" />
</template>

<style scoped>
.monaco-editor-container {
  border: 1px solid #e0e0e0;
  border-radius: 4px;
}
</style>

完整配置示例

<script setup lang="ts">
import type { MonacoLanguage, MonacoTheme } from 'vue-use-monaco'
import { onMounted, ref } from 'vue'
import { useMonaco } from 'vue-use-monaco'

const editorContainer = ref<HTMLElement>()

const {
  createEditor,
  updateCode,
  setTheme,
  setLanguage,
  getCurrentTheme,
  getEditor,
  getEditorView,
  cleanupEditor,
} = useMonaco({
  // 主题配置 - 至少需要两个主题(暗色/亮色)
  themes: ['github-dark', 'github-light'],

  // 支持的语言列表
  languages: ['javascript', 'typescript', 'python', 'vue', 'json'],

  // 编辑器最大高度
  MAX_HEIGHT: 500,

  // 是否只读
  readOnly: false,

  // 是否在创建前清理之前的资源
  isCleanOnBeforeCreate: true,

  // 创建前的钩子函数
  onBeforeCreate: (monaco) => {
    // 可以在这里注册自定义语言、主题等
    console.log('Monaco editor is about to be created', monaco)
    return [] // 返回需要清理的 disposable 对象数组
  },

  // Monaco 编辑器原生配置
  fontSize: 14,
  lineNumbers: 'on',
  wordWrap: 'on',
  minimap: { enabled: false },
  scrollbar: {
    verticalScrollbarSize: 10,
    horizontalScrollbarSize: 10,
    alwaysConsumeMouseWheel: false,
  },
  // 当使用流式追加(append)或频繁更新时,可通过 revealDebounceMs 合并多次自动滚动请求
  // 默认值:75(毫秒)。设置为 0 可关闭合并逻辑(立即 reveal)。增大到 150+ 可进一步减少滚动频率。
  revealDebounceMs: 75,
})

onMounted(async () => {
  if (editorContainer.value) {
    const editor = await createEditor(
      editorContainer.value,
      'console.log("Hello, Monaco!")',
      'javascript',
    )

    console.log('Editor created:', editor)
  }
})

// 主题切换
// 主题切换(示例:异步等待与强制重应用)
// setTheme 返回一个 Promise,resolve 表示主题已经应用到 Monaco(并在可能的情况下同步到 shiki highlighter)
// 如果希望即便当前主题相同也强制重新应用(例如强制重新渲染页面中的 shiki 片段),传入第二个参数 true
async function switchTheme(theme: MonacoTheme) {
  // 普通调用(自动跳过与当前相同的主题)
  await setTheme(theme)

  // 强制应用示例:
  // await setTheme(theme, true)
}

// 语言切换
function switchLanguage(language: MonacoLanguage) {
  setLanguage(language)
}

// 更新代码
function updateEditorCode(code: string, language: string) {
  updateCode(code, language)
}

// 获取当前主题
const currentTheme = getCurrentTheme()
console.log('Current theme:', currentTheme)

// 获取 Monaco 静态 API
const monacoEditor = getEditor()
console.log('Monaco editor API:', monacoEditor)

// 获取编辑器实例
const editorInstance = getEditorView()
console.log('Editor instance:', editorInstance)
</script>

<template>
  <div>
    <div class="controls">
      <button @click="switchTheme('github-dark')">
        暗色主题
      </button>
      <button @click="switchTheme('github-light')">
        亮色主题
      </button>
      <button @click="switchLanguage('typescript')">
        TypeScript
      </button>
      <button @click="switchLanguage('python')">
        Python
      </button>
    </div>
    <div ref="editorContainer" class="editor" />
  </div>
</template>

Diff 编辑器使用

快速开始

<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { useMonaco } from 'vue-use-monaco'

const container = ref<HTMLElement>()

const {
  createDiffEditor,
  updateDiff,
  updateOriginal,
  updateModified,
  getDiffEditorView,
  cleanupEditor,
} = useMonaco({
  themes: ['vitesse-dark', 'vitesse-light'],
  languages: ['javascript', 'typescript'],
  readOnly: true,
  MAX_HEIGHT: 500,
})

const original = `export function add(a: number, b: number) {\n  return a + b\n}`
const modified = `export function add(a: number, b: number) {\n  return a + b\n}\n\nexport function sub(a: number, b: number) {\n  return a - b\n}`

onMounted(async () => {
  if (!container.value)
    return
  await createDiffEditor(container.value, original, modified, 'typescript')
})

Shiki 高亮器(高级说明)

如果你在页面上除了 Monaco 编辑器外还使用 Shiki 的 highlighter 单独渲染代码片段(例如静态 HTML 片段),推荐的做法是:

  • 调用 registerMonacoThemes(themes, languages) 在应用启动或编辑器创建前预注册需要的主题和语言,函数会返回一个解析为 shiki highlighter 的 Promise,便于你直接复用高亮器实例。
  • 在切换主题时,先调用 monaco.editor.setTheme(themeName) 更新编辑器,然后显式调用 highlighter 的 setTheme(themeName) 或使用 codeToHtml 重新渲染页面片段。错误与加载状态应由调用方自行处理。

示例:

import { registerMonacoThemes } from 'vue-use-monaco'

// 在应用启动或创建编辑器前一次性注册全部 themes & langs
const highlighter = await registerMonacoThemes(allThemes, allLanguages)

// 创建编辑器

浏览器级基准(更接近真实 Monaco)

仓库内还提供了一个 Playwright 脚本 scripts/playwright-bench.mjs,它将在 headless Chromium 中加载 Monaco(通过 CDN)并运行高频更新,从而测量真实编辑器下的耗时与 long-task 计数。

安装并运行(本地):

pnpm add -D playwright
# 若初次安装,请按 Playwright 指示安装浏览器二进制
npx playwright install

# 运行脚本(可指定参数 updates freqHz,第三个参数传 'append' 则使用 append 路径)
pnpm run bench:playwright -- 2000 200
pnpm run bench:playwright -- 2000 200 append

注意:该脚本会从 CDN 加载 Monaco(需网络),并在本地 headless Chromium 中执行,适合用于在本机或 CI(带浏览器支持)上做真实性能评估。 const { createEditor, setTheme } = useMonaco({ themes: allThemes, languages: allLanguages })

// 当你切换主题时: setTheme('vitesse-dark') try { await highlighter.setTheme('vitesse-dark') // re-render snippets using highlighter.codeToHtml(...) } catch (err) { // handle fail-to-load or other errors }


## 性能与流式更新建议

在 0.0.32 之后的版本引入了对高频流式更新的更细粒度控制:

- `updateThrottleMs`(number): 控制 `updateCode` 的时间节流窗口(ms)。默认值为 50ms。将其设为 0 表示仅使用 RAF 合并(原始行为)。
- `minimalEditMaxChars`(number): 控制在尝试“最小替换”之前允许的最大字符总和(prev.length + next.length)。超过该值将直接使用全量 `setValue`。可通过 `useMonaco({ minimalEditMaxChars })` 覆盖。
- `minimalEditMaxChangeRatio`(number): 当变更比例(|new-prev|/maxLen)超过此阈值时,放弃最小替换,改为全量替换。

示例:

```ts
useMonaco({
  updateThrottleMs: 50, // 推荐:30~100ms,根据场景调优
  minimalEditMaxChars: 200000,
  minimalEditMaxChangeRatio: 0.25,
})

运行时调整节流:

const { setUpdateThrottleMs, getUpdateThrottleMs } = useMonaco()

// 临时关闭时间节流(仅 RAF 合并)
setUpdateThrottleMs(0)

// 恢复为 50ms
setUpdateThrottleMs(50)

console.log('current throttle', getUpdateThrottleMs())

快速 benchmark:仓库内提供了一个轻量脚本 scripts/stream-benchmark.mjs,用于在 Node 环境下模拟高频 updateCode 场景(不依赖真实 Monaco,只模拟 wrapper 行为)。运行:

pnpm run bench
# 可指定参数:pnpm run bench -- 5000 200 50
# 参数含义:updates freqHz throttleMs

该脚本输出 JSON,包含总用时、平均每次更新耗时和最终文本长度,便于对比不同 updateThrottleMs 下的表现。

// 批量(同帧)更新,两侧同时变化时更方便 function pushNewDiff(newOriginal: string, newModified: string) { updateDiff(newOriginal, newModified, 'typescript') }

// 仅更新其中一侧(即时增量) function pushModifiedChunk(chunk: string) { updateModified(chunk) }


### 流式追加 + 语言切换(快速示例)

### 自动滚动配置说明

下面是与自动滚动行为相关的可配置项及推荐值:

- `revealDebounceMs` (number, ms)
  - 说明:在流式追加或短时间内多次更新时,会把多次 reveal 请求合并成一次。减少滚动频率与抖动。
  - 默认:75
  - 建议:流式输出时保留 50-150,静态或实时编辑可设为 0 以禁用合并。

- `revealBatchOnIdleMs` (number | undefined)
  - 说明:如果设置为正数(例如 200),系统会在最后一次追加后等待该毫秒数再执行一次“最终”滚动。这适合大量小片段追加后一次性滚动到底部。
  - 默认:undefined(禁用)

- `revealStrategy` ("bottom" | "centerIfOutside" | "center")
  - 说明:控制使用哪种 reveal API。
    - `bottom`:使用 `revealLine`(靠近底部,变化明显)
    - `centerIfOutside`:使用 `revealLineInCenterIfOutsideViewport`(默认,更温和,只在目标不在视口内时居中)
    - `center`:使用 `revealLineInCenter`(总是居中)

这些选项已添加到 `useMonaco()` 的配置中,并可通过 TypeScript 的 `RevealStrategy` 枚举(库导出)进行引用。

```vue
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { useMonaco } from 'vue-use-monaco'

const el = ref<HTMLElement>()
const { createEditor, appendCode, setLanguage, cleanupEditor } = useMonaco({
  themes: ['vitesse-dark', 'vitesse-light'],
  languages: ['markdown', 'typescript'],
  readOnly: false,
  MAX_HEIGHT: 360,
})

let i = 0
let timer: any

onMounted(async () => {
  if (!el.value)
    return
  await createEditor(el.value, '# Stream start\n', 'markdown')
  // 模拟流式输出
  timer = setInterval(() => {
    i++
    appendCode(`- line ${i}\\n`)
    if (i === 5)
      setLanguage('typescript') // 动态切换语言
    if (i >= 10) {
      clearInterval(timer)
    }
  }, 300)
})
</script>

<template>
  <div ref="el" />
  <button @click="cleanupEditor">
    Dispose
  </button>
  <p>前 5 行为 Markdown,随后切换为 TypeScript。</p>
  <p>当内容接近底部时自动滚动(可通过 autoScroll* 选项进行控制)。</p>
  <p>若是纯末尾追加,内部会走追加快路径,避免全量替换。</p>
</template>

更多完整示例请见 examples/ 目录。

行为说明(增量与 RAF)

  • updateDiff 使用 requestAnimationFrame 合并同一帧内的多次调用,减少重排与布局开销。
  • 当新内容以旧内容为前缀时,采用“仅追加”的策略,避免全量替换带来的性能损耗。
  • 其他情况下执行“最小中段替换”,在模型上计算公共前后缀,只替换中间变化段,减少编辑器刷新范围。
  • updateOriginal / updateModified 为即时增量更新,适合单侧独立流式场景。
  • 可通过 options.diffAutoScroll 关闭 Diff 编辑器 modified 侧的自动滚动;默认开启以保持与单编辑器一致的体验。

显式流式追加(推荐)

当你是标准的“持续在末尾追加”场景,建议直接使用显式追加 API,可减少 diff 计算并获得最佳实时性:

const {
  createDiffEditor,
  appendOriginal,
  appendModified,
} = useMonaco({ themes: ['vitesse-dark', 'vitesse-light'], languages: ['typescript'] })

await createDiffEditor(container, '', '', 'typescript')

// 只向 original 侧持续追加
appendOriginal('line 1\n')
appendOriginal('line 2\n')

// 只向 modified 侧持续追加
appendModified('out 1\n')
appendModified('out 2\n')

提示:在 updateDiff/updateOriginal/updateModified 中,当检测到“语言未变且严格前缀追加”时,内部也会自动走“立即追加”的快路径;否则进入 requestAnimationFrame 合并 + 最小替换。

视图模式切换与模型访问

你可以获取 Diff 的两个模型来做更底层控制,或切换视图模式:

const { createDiffEditor, getDiffEditorView, getDiffModels } = useMonaco({
  themes: ['vitesse-dark', 'vitesse-light'],
  languages: ['typescript'],
})

await createDiffEditor(container, left, right, 'typescript')

// 切换为内联模式
getDiffEditorView()?.updateOptions({ renderSideBySide: false })

// 获取模型:你可以自行订阅内容变化等底层行为
const { original, modified } = getDiffModels()
original?.onDidChangeContent?.(() => { /* ... */ })
modified?.onDidChangeContent?.(() => { /* ... */ })

API 参考

useMonaco(options?)

参数
参数 类型 默认值 描述
MAX_HEIGHT number 500 编辑器最大高度(像素)
readOnly boolean true 是否为只读模式
themes MonacoTheme[] ['vitesse-dark', 'vitesse-light'] 主题数组,至少包含两个主题
languages MonacoLanguage[] 见默认语言列表 支持的编程语言数组
theme string - 初始主题名称
isCleanOnBeforeCreate boolean true 是否在创建前清理之前注册的资源
onBeforeCreate function - 编辑器创建前的钩子函数
autoScrollOnUpdate boolean true 更新内容时若接近底部则自动滚动
autoScrollInitial boolean true 是否默认启用自动滚动
autoScrollThresholdPx number 32 自动滚动的像素阈值
autoScrollThresholdLines number 2 自动滚动的行数阈值
diffAutoScroll boolean true 是否启用 Diff modified 侧自动滚动
返回值
方法/属性 类型 描述
createEditor (container: HTMLElement, code: string, language: string) => Promise<MonacoEditor> 创建并挂载编辑器到指定容器
createDiffEditor (container: HTMLElement, original: string, modified: string, language: string) => Promise<MonacoDiffEditor> 创建并挂载 Diff 编辑器
cleanupEditor () => void 销毁编辑器并清理容器
updateCode (newCode: string, codeLanguage: string) => void 更新编辑器内容和语言(RAF 合并、增量优化)
appendCode (appendText: string, codeLanguage?: string) => void 在编辑器末尾追加文本
updateDiff (original: string, modified: string, codeLanguage?: string) => void 批量更新 Diff 内容(RAF 合并、增量优化)
updateOriginal (newCode: string, codeLanguage?: string) => void 仅更新 original(即时增量)
updateModified (newCode: string, codeLanguage?: string) => void 仅更新 modified(即时增量)
setTheme (theme: MonacoTheme) => void 切换编辑器主题
setLanguage (language: MonacoLanguage) => void 切换编辑器语言
getCurrentTheme () => string 获取当前主题名称
getEditor () => typeof monaco.editor 获取 Monaco 的静态 editor 对象
getEditorView () => MonacoEditor | null 获取当前编辑器实例
getDiffEditorView () => MonacoDiffEditor | null 获取当前 Diff 编辑器实例
appendOriginal (appendText: string, codeLanguage?: string) => void 在 original 末尾追加(显式流式)
appendModified (appendText: string, codeLanguage?: string) => void 在 modified 末尾追加(显式流式)

支持的主题

包括但不限于:

  • vitesse-dark / vitesse-light
  • github-dark / github-light
  • dracula / dracula-soft
  • one-dark-pro / one-light
  • tokyo-night
  • material-theme 系列
  • catppuccin 系列
  • 以及更多...

支持的语言

包括但不限于:

  • javascript / typescript / jsx / tsx
  • vue / html / css / scss / less
  • python / java / csharp / cpp / rust / go
  • json / yaml / toml / xml
  • markdown / dockerfile
  • 以及 100+ 种语言...

最佳实践

1. 性能优化

// 只加载需要的语言,减少包体积
const { createEditor } = useMonaco({
  languages: ['javascript', 'typescript'], // 只加载必要的语言
  themes: ['vitesse-dark', 'vitesse-light'],
})

2. 内存管理

<script setup>
import { onUnmounted } from 'vue'

const { createEditor, cleanupEditor } = useMonaco()

onUnmounted(() => {
  cleanupEditor()
})
</script>

3. 主题跟随系统

import { useDark } from '@vueuse/core'

const isDark = useDark()

const { createEditor, setTheme } = useMonaco({
  themes: ['github-dark', 'github-light'],
})

// 主题会自动跟随 isDark 状态切换

故障排除

1. 打包后编辑器无法显示

确保正确配置了 Monaco Editor 的 Web Workers(参考上面的 Vite/Webpack 配置)。

2. 主题不生效

检查主题名称是否正确,确保主题已在 themes 数组中注册。

3. 语言高亮不工作

确保语言已在 languages 数组中包含,并且 Shiki 支持该语言。

贡献

欢迎提交 Issue 或 PR 来改进此项目!

开发

# 克隆项目
git clone https://github.com/Simon-He95/vue-use-monaco.git

# 安装依赖
pnpm install

# 启动开发服务器
pnpm dev

# 构建
pnpm build

buy me a cup of coffee

License

MIT

Sponsors

致谢

Clearing shiki highlighter cache

The library caches shiki highlighters internally to avoid recreating them for the same theme combinations. In long-running applications that may dynamically create many distinct theme combinations, you can clear the cache to free memory or reset state (for example in tests or on app shutdown):

  • clearHighlighterCache() — clears the internal cache
  • getHighlighterCacheSize() — returns number of cached entries

Call clearHighlighterCache() when you are certain highlighters are no longer needed (for example during teardown), otherwise leaving the cache enabled provides a performance benefit by reusing previously-created highlighters.