首页
/ Slidev Monaco Editor 详解:把代码块变成可编辑编辑器与 Diff 对比视图

Slidev Monaco Editor 详解:把代码块变成可编辑编辑器与 Diff 对比视图

2026-09-07 17:46:14作者:魏侃纯Zoe

本文基于 Slidev 官方文档 Monaco Editor 展开,系统讲解如何在幻灯片的 Markdown 代码块中通过 {monaco} 属性启用完整的 Monaco 编辑器、使用 {monaco-diff} 生成代码差异对比,以及如何调整编辑器高度、行号等细节;并结合 Slidev 源码中的代码块转换器与内置组件实现,解释这些语法背后的处理流程、monaco 相关 headmatter 配置项的默认值与生效机制,帮助你在演讲场景中直接"现场改代码"并深入理解其工程实现。

基本用法:{monaco} 属性

当你希望在演示过程中对代码进行实时修改时,只需在代码块的语言标识后追加 {monaco} 属性,该代码块就会变成一个功能完整的 Monaco 编辑器(即 VS Code 内置的浏览器版编辑器内核):

```ts {monaco}
console.log('HelloWorld')
```

语法解析发生在构建阶段的代码块转换器中。从源码 monaco.ts 可以看到,转换器用正则 ^([\w'-]+)?\s*\{(monaco[\w-]*)\}\s*(\{[^}]*\})?(.*)$ 匹配代码块 info 字符串,依次提取语言标识、Monaco 变体(monaco / monaco-diff / monaco-run 等)以及可选的属性对象:

// packages/slidev/node/syntax/codeblock/monaco.ts
const RE_MONACO = /^([\w'-]+)?\s*\{(monaco[\w-]*)\}\s*(\{[^}]*\})?(.*)$/

原始代码会先经过 lz-string 的 compressToBase64 压缩编码,然后被转换为内置组件的声明:

<Monaco v-bind="{...}" lang="ts" code-lz="压缩后的代码" />

也就是说,{monaco} 并不是在运行时"动态注入"编辑器,而是在编译期把静态代码块替换为携带压缩代码载荷的 <Monaco> 组件,真正的编辑器在客户端懒加载时才被创建(见 Monaco.vueonMountedawait import('../setup/monaco') 的懒加载逻辑),因此 Monaco 只会在你实际使用时才被打包。

按环境启用与禁用

自 v0.48.0 起,Monaco 编辑器默认启用、按需打包。如果你希望关闭它,可以在幻灯片 frontmatter 中将 monaco 设为 false,也可以设为 devbuild 来按环境条件启用:

---
monaco: false # 也可以设为 `dev` 或 `build` 来条件启用
---

这与转换器中的启用判断逻辑一致(见 monaco.ts):config.monaco === true || config.monaco === mode,其中 mode 即当前运行环境(开发/构建)。frontmatter 中该字段完整的类型定义见 frontmatter.tsmonaco?: boolean | 'dev' | 'build',默认值为 true

Diff 编辑器:{monaco-diff}

Monaco 还可以生成两段代码之间的差异视图。使用 {monaco-diff} 将代码块变成 Monaco diff 编辑器,并用 ~~~ 分隔线区分原始代码与修改后的代码:

```ts {monaco-diff}
console.log('Original text')
~~~
console.log('Modified text')
```

在源码层面,转换器检测到 monaco-diff 变体后,会用 /^\s*~~~\s*\n/m 把代码拆分为 originalmodified 两部分:原始代码进入 code-lz 属性,修改后的代码以 diff-lz 属性单独携带(同样经过 lz-string 压缩):

// packages/slidev/node/syntax/codeblock/monaco.ts
if (monaco === 'monaco-diff') {
  const [original, modified] = code.split(RE_DIFF_SEPARATOR, 2)
  encoded = lz.compressToBase64(original)
  diff = modified === undefined ? '' : `diff-lz="${lz.compressToBase64(modified)}"`
}

客户端 Monaco.vue 解压缩出两份代码后,分别创建 original / modified 两个 model,并通过 monaco.editor.createDiffEditor 渲染双栏对比视图;两侧内容尺寸变化都会触发高度重算,保证 diff 面板随内容自适应。如果只写了 ~~~ 而没有后半部分代码,diff-lz 属性会被省略,编辑器按普通模式渲染原始代码。

这一转换过程有集成测试覆盖:integration.test.ts 验证了形如 <<< @/snippets/snippet.ts#snippet {monaco}{propA:1} 的导入代码块同样会被转换为携带 v-bindcode-lz<Monaco> 组件,说明 {monaco} 属性对普通代码块和导入片段都适用。

编辑器高度:{height}

默认情况下,Monaco 编辑器的高度基于初始内容固定(对应组件内的默认值 height: 'initial')。如果从一个空代码块或很小的代码块开始,又希望编辑器随你现场敲入的代码自动长高,可以设置 {height:'auto'}

```ts {monaco} {height:'auto'}
// The editor will automatically grow as you type more code
console.log('Hello, World!')
```

也可以使用 CSS 单位指定具体高度,如 {height:'300px'}{height:'100%'}

Monaco.vue 的实现看,高度是一个响应式计算:

const height = computed(() => {
  if (props.height === 'auto')
    return `${contentHeight.value}px`   // 跟随内容实时变化
  if (props.height === 'initial')
    return `${initialHeight.value}px`  // 锁定首次渲染时的高度
  return props.height                   // 直接透传 '300px' / '100%' 等
})

其中 contentHeight 由编辑器的 onDidContentSizeChange 事件持续更新,并在变化后调用 layout() 重新排版;initialHeight 只在首次取值。这就解释了"默认高度固定、auto 模式动态生长"的行为差异。

行号:全局 lineNumbers{lines}

与其他代码块一样,Monaco 编辑器遵循全局的 lineNumbers headmatter 设置。针对单个编辑器,也可以用 {lines:true} 在保持可编辑的前提下单独开关行号:

```ts {monaco} {lines:true}
console.log('HelloWorld')
```

在组件属性上,lines 的默认值来自全局配置 configs.lineNumbers(即不显式指定时继承全局行为),最终映射为 Monaco 的 lineNumbers 选项,后者还支持 'on' | 'off' | 'relative' | 'interval' 等更细粒度的取值(见 Monaco.vue 的 props 定义)。

传递编辑器选项:editorOptions

如果需要自定义某个编辑器的行为,可以在代码块上直接内联一个符合 Monaco IEditorOptions 定义的 editorOptions 对象:

```ts {monaco} { editorOptions: { wordWrap:'on'} }
console.log('HelloWorld')
```

转换器会把代码块 info 中 {...} 形式的属性对象原样生成为 v-bind="..." 绑定到 <Monaco> 组件上,因此 heightlineseditorOptions 等都是走同一条通道。

如果希望某些选项对所有 Monaco 实例生效,则在 ./setup/monaco.ts 中通过 defineMonacoSetup 返回:

// ./setup/monaco.ts
import { defineMonacoSetup } from '@slidev/types'

export default defineMonacoSetup(() => {
  return {
    editorOptions: {
      wordWrap: 'on'
    }
  }
})

优先级可以从 Monaco.vuecommonOptions 的展开顺序确认:内置默认值(tabSize: 2fontSize: 11.5、禁用 minimap、fontFamily: var(--slidev-code-font-family) 等)→ setup/monaco.ts 返回的 editorOptions → 代码块上内联的 props.editorOptions,后者优先级更高,保证"单块覆盖全局"的语义。完整的配置说明参见 Configure Monaco

TypeScript 类型与主题

类型自动加载

当 Monaco 编辑器中使用 TypeScript 并导入本地安装的依赖时,Slidev 会自动为这些依赖安装类型,让编辑器的智能提示开箱即用:

```ts {monaco}
import { ref } from 'vue'
import { useMouse } from '@vueuse/core'

const counter = ref(0)
```

前提是 vue@vueuse/core 等包已作为本地依赖安装,Slidev 会负责剩下的类型接线;作为 SPA 部署时这些类型也会被一并打包以支持静态托管。

实现链路由两部分组成:

  • 虚拟模块 monaco-types.tslocal 模式下会收集幻灯片代码块中扫描到的依赖(data.features.monaco.types),合并 frontmatter 中 monacoTypesAdditionalPackages 显式声明的额外包,过滤掉 monacoTypesIgnorePackages 命中的包,并额外把 snippets/ 目录下的 .ts/.mts/.cts 文件注册为类型来源;
  • Vite 插件 monacoTypes.ts:将上述解析结果转成实际的 import 语句,按包的 dependencies 递归展开,逐文件 addFile 注入 Monaco 的 TS 语言服务。

相关 frontmatter 配置项(类型定义见 frontmatter.ts):

---
monacoTypesAdditionalPackages:
  - lodash-es
  - foo
---
配置项 取值 默认值 说明
monaco false / true / 'dev' / 'build' true 启用 Monaco,可按环境条件启用
monacoTypesSource 'local' / 'cdn' / 'none' 'local' 类型来源;cdn 模式由 @typescript/ata 驱动,完全在客户端运行
monacoTypesAdditionalPackages string[] [] 显式追加需要加载类型的包
monacoTypesIgnorePackages string[] [] 忽略的包,支持模糊匹配(见 options.ts 的匹配规则)
monacoRunUseStrict boolean true {monaco-run} 可运行代码是否以严格模式执行

关于 monacoTypesSource: 'cdn':客户端 setup/monaco.ts 会据此调用 @typescript/atasetupTypeAcquisition,从 CDN 按需拉取类型并注入 TS 语言服务,整个流程运行在浏览器端。

主题与 Shiki 复用

自 v0.48.0 起,Monaco 会复用你在 Shiki 配置文件中设定的主题,由 @shikijs/monaco 完成桥接,与其余代码块保持一致的视觉风格,无需额外配置。在 setup/monaco.ts 中可以看到:先通过 shikiToMonaco(highlighter, monaco) 把 Shiki 高亮器接入 Monaco,若主题配置为对象(区分深色/浅色),则监听暗色模式切换、在 themeOption.dark/light 之间调用 monaco.editor.setTheme 动态换肤。

此外,客户端初始化还会设置 TypeScript 语言默认的编译选项(strict: truemodule: ESNextmoduleResolution: NodeJs),并在编辑器获得焦点时锁定 Slidev 的全局快捷键(lockShortcuts),避免演示翻页与编辑输入互相干扰。

相关能力与源码索引

Monaco 属性族还有两个兄弟变体,与本特性共享同一套 info 字符串解析机制(转换器中 monaco === 'monaco-run' ? 'runnable' : ''):

  • {monaco-run}:把代码块变成可运行的编辑器,配套文档见 Configure Code Runners
  • {monaco-write}:允许通过 Ctrl/Cmd + S 把编辑内容写回本地文件,客户端在 Monaco.vue 中注册了 slidev-save 动作并通过 HMR 通道发送 slidev:monaco-write 事件,服务端插件 monacoWrite.ts 以白名单 + 路径校验(拒绝项目根目录之外的写入)落盘。

关键源码索引:

文件 作用
packages/slidev/node/syntax/codeblock/monaco.ts {monaco} / {monaco-diff} 等 info 字符串解析与组件生成
packages/client/builtin/Monaco.vue 编辑器/ diff 编辑器渲染、高度自适应、选项合并
packages/client/setup/monaco.ts Worker 注册、Shiki 主题桥接、ATA 类型获取
packages/slidev/node/virtual/monaco-types.ts 本地类型扫描与虚拟模块生成
packages/slidev/node/vite/monacoTypes.ts 依赖类型递归解析的 Vite 插件
docs/custom/config-monaco.md setup/monaco.ts 与全部 Monaco 配置项文档

综合来看,Slidev 的 Monaco 集成遵循"编译期标记 + 客户端懒加载"的架构:Markdown 属性只负责声明意图并把代码压缩编码进组件属性,真正的编辑器实例、类型服务与主题都在客户端按需初始化,配合 monaco 开关可按环境裁剪,使"可编辑代码块"成为演示文稿中一个既即开即用、又可深度定制的能力。

登录后查看全文
热门项目推荐
相关项目推荐