Slidev {monaco-write} 可写 Monaco 编辑器:幻灯片内编辑代码并保存回文件的完整链路
2026/9/8 22:28:10 网站建设 项目流程

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

【免费下载链接】slidevPresentation Slides for Developers项目地址: https://gitcode.com/GitHub_Trending/sl/slidev

本篇讲解 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.vue 中const 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」式的技术演示,这正是既安全又实用的实现方式。

相关功能

  • Import Code Snippets:<<<文件导入语法,是monaco-write的语法基础;
  • Monaco Editor:{monaco}/{monaco-diff}编辑器及其高度、行号等选项;
  • Configure Monaco:通过项目配置定制 Monaco 编辑器行为。

【免费下载链接】slidevPresentation Slides for Developers项目地址: https://gitcode.com/GitHub_Trending/sl/slidev

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询