首页
/ Slidev {monaco-write} 可写 Monaco 编辑器:幻灯片内编辑代码并保存回文件的完整链路

Slidev {monaco-write} 可写 Monaco 编辑器:幻灯片内编辑代码并保存回文件的完整链路

2026-09-07 17:52:43作者:何将鹤

本篇讲解 Slidev 的「可写 Monaco 编辑器」(monaco-write)功能:通过在代码导入语法后追加 {monaco-write} 指令,将幻灯片中的 Monaco 编辑器与磁盘上的真实文件绑定,直接在演示中修改代码并通过快捷键把变更写回文件。读完本文,你将掌握该功能的完整用法、适用限制,以及从 Markdown 解析、白名单注册到 WebSocket 落盘的保存回写机制。

功能定位:只读代码块升级为「文件级编辑器」

普通代码块和 {monaco} 代码编辑器都只操作内存中的文本,演示结束后内容即丢失。{monaco-write} 解决了「边讲边改、改完即存」的场景:它复用 Slidev 的 Import Code Snippets 语法把文件内容载入编辑器,并额外把该文件注册为「可写目标」,使 Monaco Editor 从展示工具变成真正的文件编辑器。

根据该功能文档的 frontmatter,此特性自 v0.49.5 起提供,标签为 codeblockeditor(见 docs/features/monaco-write.md)。

基本用法

在导入语法的元信息中加入 {monaco-write}

<<< ./some-file.ts {monaco-write}

此时幻灯片会渲染出一个 Monaco 编辑器,其初始内容来自 ./some-file.ts 的真实文件内容。在编辑器中修改代码后,按 Ctrl+S(macOS 为 Cmd+S) 即可将编辑器当前内容写回该文件。

由于导入语法本身支持的能力,以下用法同样有效(详见 docs/features/import-snippet.md):

<<< @/snippets/snippet.ts {monaco-write}          # 使用 @ 指向包根目录
<<< @/src/app.ts ts {monaco-write}              # 显式指定语言标识
<<< @/src/app.ts {monaco-write}{lines:true}     # 叠加行号等其它代码块特性

其中 @ 对应项目的根目录。官方文档建议把片段文件放在 @/snippets 下,以便与 Monaco 编辑器配合使用;也支持相对路径导入。

警告(原文档强调):修改会直接保存回文件本身,因此使用 monaco-write 之前务必备份文件。

编辑器端的保存动作

保存行为由内置 Monaco 组件注册的一个编辑器 Action 实现。在 Monaco.vue 中:

editableEditor.addAction({
  id: 'slidev-save',
  label: 'Save',
  keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS],
  run: () => {
    if (!isWritable.value || !import.meta.hot?.send) {
      console.warn('[Slidev] this monaco editor is not writable, save action is ignored.')
      return
    }
    import.meta.hot.send('slidev:monaco-write', {
      file: props.writable!,
      content: editableEditor.getValue(),
    })
  },
})

三个关键事实可以从这段源码确认:

  1. 保存走的是 Vite HMR WebSocketimport.meta.hot.send('slidev:monaco-write', ...) 依赖 Vite 开发服务器的 HMR 通道,消息体是 { file, content }
  2. isWritable 有三重约束:在 Monaco.vueconst isWritable = computed(() => props.writable && !props.readonly && __DEV__)。即只有「带 writable 路径属性 + 非只读 + 开发环境(__DEV__)」同时满足时,Ctrl+S 才生效;生产构建或非 serve 场景下,按保存键只会输出一条控制台警告。
  3. writable 属性就是目标文件路径:该属性由服务端解析阶段注入(见下一节),编辑器本身并不知道文件在哪,只知道往谁身上写。

服务端解析:白名单注册与 Token 生成

{monaco-write} 的处理发生在 Markdown → Vue 的解析阶段。snippet.ts 中的 markdown-it 插件拦截 <<< 导入语法后:

if (meta.includes('{monaco-write}')) {
  monacoWriterWhitelist.add(filepath)
  lang = lang.trim()
  meta = meta.replace('{monaco-write}', '').trim() || '{}'
  const safeFilepath = JSON.stringify(filepath).slice(1, -1)
  const encoded = lz.compressToBase64(content)

  const token = state.push('html_block', '', 0)
  token.content = `<Monaco writable="${safeFilepath}" code-lz="${encoded}" lang="${lang}" v-bind="${meta}" />\n`
}

这段逻辑说明了完整的「装配」过程:

  • 文件路径被登记进白名单 monacoWriterWhitelist(一个模块级 Set<string>),这是后续服务端授权写盘的前提;
  • 文件内容用 lz-string 压缩为 Base64code-lz),避免特殊字符破坏 HTML 属性,也减小模板体积;
  • 路径经过 JSON.stringify(...).slice(1, -1) 转义safeFilepath),防止引号等字符破坏属性语法;
  • {monaco-write} 从 meta 中剥离,剩余 meta(如 {lines:true})原样透传给 Monaco 组件;
  • 最终输出的是 <Monaco writable="..." code-lz="..." lang="..." v-bind="..." /> 组件调用——writable 属性正是编辑器端判断「能否保存」的依据。

落盘链路:Vite 插件 + 双重校验

真正执行写文件的是 monacoWrite.ts 中定义的 Vite 插件 slidev:monaco-write(在 vite/index.ts 注册):

export function createMonacoWriterPlugin({ userRoot }: ResolvedSlidevOptions): Plugin {
  return {
    name: 'slidev:monaco-write',
    apply: 'serve',

    configureServer(server) {
      server.ws.on('connection', (socket) => {
        socket.on('message', async (data) => {
          // ...
          if (json.type === 'custom' && json.event === 'slidev:monaco-write') {
            const { file, content } = json.data
            if (!monacoWriterWhitelist.has(file)) {
              console.error(`[Slidev] Unauthorized file write: ${file}`)
              return
            }
            const filepath = path.resolve(userRoot, file)
            const rel = path.relative(userRoot, filepath)
            if (rel.startsWith('..') || path.isAbsolute(rel)) {
              console.error(`[slidev] Refusing monaco write outside project root: ${file}`)
              return
            }
            console.log('[Slidev] Writing file:', filepath)
            await fs.writeFile(filepath, content, 'utf-8')
          }
        })
      })
    },
  }
}

从源码结构可以确认以下安全与边界设计:

  • 仅限开发模式apply: 'serve' 表明插件只在 slidev dev 启动的 Vite 开发服务器中生效,slidev build 产物不具备写回能力——与编辑器端 __DEV__ 的约束相互印证。
  • 白名单校验:消息中的 file 必须精确命中解析阶段登记的 monacoWriterWhitelist,否则直接拒绝并打印 Unauthorized file write。客户端伪造一个任意文件路径是无效的,只有幻灯片里实际用 {monaco-write} 导入过的文件才能被写。
  • 路径穿越防护:即使路径在(理论上)通过了白名单,path.resolve(userRoot, file) 后若相对结果以 .. 开头或是绝对路径,同样被拒绝,保证写入永远落在项目根目录之内。
  • 落盘是全文覆盖fs.writeFile(filepath, content, 'utf-8') 直接用编辑器当前全文替换文件内容,没有 diff 或合并逻辑。这也解释了为何原文档反复强调先备份。

使用限制与注意事项

结合文档说明与源码事实,使用 monaco-write 前需要明确的前提与限制:

  1. 只适用于 slidev dev(serve 场景):写入插件 apply: 'serve',编辑器端又要求 __DEV__ 与 HMR 通道,因此 slidev build、导出 PDF 或部署后的静态页面中,编辑器会退化为只读展示。
  2. 写回是整文件覆盖fs.writeFile 直接以编辑器内容替换磁盘文件,无版本管理、无撤销。原文档明确建议「使用此功能前务必备份文件」。
  3. 与 region 导入组合要格外小心<<< file.ts#region {monaco-write} 只会把 region 内的代码载入编辑器,但保存时写入的是整份文件内容。从源码链路(内容解析见 snippet.ts 的 region 截取逻辑,写盘见 monacoWrite.ts)可以推断,这样组合会导致 region 之外的原有代码被覆盖,不建议使用。
  4. 编辑器可叠加普通 Monaco 配置{monaco-write} 之外的 meta 原样透传,例如 Monaco Editor 文档 中的 {height:'auto'}{lines:true} 等均可继续生效;编辑器选项层面的定制可参考 配置 Monaco
  5. 只读模式下保存被忽略:若代码块带有 readonly 标记,isWritable 为 false,Ctrl+S 会打印 this monaco editor is not writable, save action is ignored. 警告而不做任何事。

保存动作的完整链路总结

把前后端两侧串起来,一次 Ctrl+S 保存的完整调用链为:

  1. 解析阶段:<<< ./some-file.ts {monaco-write} 被 markdown-it 插件识别,./some-file.ts 登记进 monacoWriterWhitelist,文件内容经 lz-string 压缩后生成 <Monaco writable="..." code-lz="..."> 组件调用(snippet.ts);
  2. 编辑阶段:Monaco.vue 懒加载 monaco,用 codeLz 解压初始化 model,并注册 slidev-save 快捷键动作(Monaco.vue);
  3. 发送阶段:Ctrl+S 触发 import.meta.hot.send('slidev:monaco-write', { file, content }),经 Vite HMR WebSocket 到达开发服务器;
  4. 校验落盘:slidev:monaco-write 插件校验事件类型 custom、白名单命中、路径未越出项目根目录后,fs.writeFile 覆盖写回文件(monacoWrite.ts)。

这条「白名单 + 路径校验 + serve-only」的设计,在让幻灯片具备真实代码编辑能力的同时,把写盘权限严格限定在幻灯片作者显式声明的文件集合内。对于「live coding」式的技术演示,这正是既安全又实用的实现方式。

相关功能

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

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.13 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
529
593
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
915
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.35 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
515
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388