Slidev 可写 Monaco 编辑器:用 {monaco-write} 在幻灯片中现场改码并直接保存回源文件
【免费下载链接】slidevPresentation Slides for Developers项目地址: https://gitcode.com/GitHub_Trending/sl/slidev
可写 Monaco(Writable Monaco)是 Slidev 代码块增强体系中非常特别的一个能力:它把「展示代码」升级为「编辑真实文件」。通过在代码块上追加{monaco-write}指令,幻灯片内会渲染一个与磁盘文件双向关联的 Monaco 编辑器,演讲者可以在演示(live coding)过程中直接修改代码并按下Ctrl/Cmd + S将内容实时写回源文件。读完本文,你将掌握<<<导入语法与{monaco-write}的组合用法、可叠加的全部选项、其仅在开发服务器下生效的边界条件,以及从 Markdown 解析到 WebSocket 写盘这一完整链路的底层实现原理。
一、这是什么:从"只读展示"到"真写盘"
默认情况下,无论是```ts {monaco}内联代码块,还是<<<导入的代码片段,幻灯片里的 Monaco 编辑器都只是「读取文件内容后展示」,编辑内容不会触碰原始文件,刷新页面即还原。
{monaco-write}指令打破了这条边界。它的核心语义由官方文档定义为三条:
- 将 Monaco 编辑器链接到文件系统上的真实文件(Links Monaco editor to actual file on filesystem);
- 编辑器内的修改直接保存到该文件(Changes are saved directly to the file);
- 该能力专为现场编码演示设计(Useful for live coding demonstrations)。
对应的功能说明见 docs/features/monaco-write.md,该特性以since: v0.49.5版本引入。结合 import-snippet 导入语法(since: v0.47.0),即可让一段代码同时具备「高亮展示 + 实时可编辑 + 可持久化」三重身份,非常适合技术演讲中的即兴改码、培训实操以及录屏演示。
二、快速上手:最小示例与运行前提
{monaco-write}并不能直接作用于```ts这种内联代码块,它必须搭配代码片段导入语法使用。在任意一张幻灯片里写入:
<<< ./some-file.ts {monaco-write}然后以开发模式启动演示即可:
npm run dev打开对应幻灯片,你会看到一个完整的 Monaco 编辑器,其中的代码正是some-file.ts的当前内容。直接修改任意代码,按Ctrl/Cmd + S,改动会被写回./some-file.ts;刷新浏览器、切换到其他幻灯片再回来,内容保持一致。
运行前提与生效范围
需要特别强调的是,这个特性只在开发服务器模式下生效,原因有三个,均可以从源码确认:
- 负责写盘的 Vite 插件
slidev:monaco-write声明了apply: 'serve',即只挂载到开发服务器;构建/导出(slidev build、导出 PDF 等)产物中不存在该写盘通道。见 monacoWrite.ts。 - 客户端保存动作通过 HMR 通道
import.meta.hot.send(...)发送,未运行 dev server 时不存在 HMR socket。见 Monaco.vue。 - 客户端可写开关本身由
__DEV__编译宏约束:writable && !readonly && __DEV__三者同时满足才允许保存。见 Monaco.vue。
因此,{monaco-write}是为演讲现场(本地slidev开发预览)准备的,不要指望它作用于构建后的静态部署页面。
三、语法全解:路径、语言、region 与可叠加选项
由于{monaco-write}建立在<<<代码导入语法之上,它天然继承了 snippet 导入的全部书写能力。以下形态均可用:
1. 路径写法
- 相对路径:
<<< ./some-file.ts {monaco-write},相对于当前slides.md所在目录解析; - 根目录别名:
<<< @/snippets/snippet.js {monaco-write},@指向幻灯片项目的根目录。官方建议把可写片段放在@/snippets下,便于与 Monaco 编辑器配合。
2. 显式指定语言
导入路径的扩展名会被自动识别为高亮语言,也可以手动覆盖:
<<< @/snippets/code.js ts {monaco-write}3. 只取文件的某一段:region 过滤
如果源文件很大,只想让某一 region 出现在编辑器中(保存时仍写回整个文件),可以使用 VS Code 风格的#region标记:
<<< @/snippets/snippet.js#region-name {monaco-write}解析器会从文件中裁剪出#region region-name到#endregion region-name之间的内容作为编辑器初始代码,支持嵌套 region 与多种语言的注释风格(//、/* */、#、<!-- -->、--、#pragma、(* *)等),对应实现见 snippet.ts 中的 markers 与 findRegion。
4. 与其他代码块特性叠加
{monaco-write}会从 meta 中被单独剥离,其余代码块选项原样传给 Monaco 组件,因此可以自由组合:
<<< @/snippets/snippet.js {monaco-write}{lines:true}{height:auto}{lines:true}:为编辑器开启行号(其余行为遵循全局lineNumbersheadmatter);{height:'auto'}:编辑器高度随输入内容自动增长,也可用{height:'300px'}、{height:'100%'}等 CSS 单位固定高度;- 更多编辑器层面的配置可参考 配置 Monaco 编辑器。
叠加顺序不限:{monaco-write}被识别后即从 meta 中移除,剩余 JSON 通过v-bind绑定到<Monaco>组件,见 snippet.ts 的实现。
5. 与普通 Monaco 的对照
```ts {monaco}:只读编辑体验,改动不落盘,用于展示与现场尝试;```ts {monaco-diff}:双代码块差异对比编辑,用~~~分隔原始与修改版本;<<< path {monaco-write}:真正的"改真文件"模式,三种能力的对比如下表:
| 指令 | 来源 | 可编辑 | 保存回文件 | 适用场景 |
|---|---|---|---|---|
{monaco} | 内联代码块内容 | 是(仅内存) | 否 | 现场演示、代码讲解 |
{monaco-diff} | 内联代码块内~~~两侧 | 是(仅内存) | 否 | 展示改动前后差异 |
{monaco-write} | <<<导入的真实文件 | 是 | 是 | 现场改码、边讲边改并落地 |
四、底层原理:从 Markdown 行到磁盘写入的完整链路
理解{monaco-write}的完整行为,需要沿一条调用链走通四个环节,对应仓库中的三处核心源码。
第 1 步:MarkdownIt 插件识别并改写代码块
Markdown 解析阶段由 markdown-it 插件MarkdownItSnippet处理(snippet.ts)。当某行匹配^<<<导入语法(正则RE_SNIPPET_IMPORT)时,插件先按规则解析出文件路径、region、语言与 meta;随后判断 meta 中是否包含{monaco-write}:
- 若包含,则执行三件事:把该文件路径登记进
monacoWriterWhitelist白名单;将文件内容经lz-string的compressToBase64压缩编码;最后向文档流插入一段自定义 HTML token:<Monaco writable="..." code-lz="..." lang="..." v-bind="..." />。也就是说,{monaco-write}实际是语法糖,最终会编译为一个携带可写文件路径的 Monaco 组件。
对应实现见 snippet.ts。代码以 Base64 压缩串内联进 HTML 是刻意的设计——编辑器即使尚未与文件系统交互,也能立即渲染初始内容,不依赖额外请求。
第 2 步:客户端 Monaco 组件注册保存动作
浏览器侧,内置组件 Monaco.vue 负责渲染编辑器并接管交互:
- 组件 Props 中包含
writable?: string(要写回的文件路径)与readonly; - 挂载后,编辑器注册了一个名为
slidev-save的动作,绑定Ctrl/Cmd + S快捷键(Monaco.vue); - 触发保存时,若
isWritable(即writable && !readonly && __DEV__)不成立,会输出警告this monaco editor is not writable, save action is ignored并忽略本次操作; - 条件满足则通过 HMR 通道向开发服务器发送自定义消息:
import.meta.hot.send('slidev:monaco-write', { file: writable, content: editor.getValue() }),其中file是 slides 里书写的文件路径,content是编辑器当前全部文本。
需要留意编辑器自动高度与行号等选项同样作用于该组件,见 Monaco.vue 的 Props 定义与默认值。
第 3 步:服务端插件校验并写盘
开发服务器一侧,Vite 插件 monacoWrite.ts 中名为slidev:monaco-write的插件通过configureServer监听每个 WebSocket 连接上的消息(monacoWrite.ts):
- 白名单校验:消息必须携带
{ file, content },且file必须出现在monacoWriterWhitelist中——该集合只由解析阶段使用过{monaco-write}的文件填充,从而保证只有幻灯片里显式声明过的文件才允许被写入,非法文件会被拒绝并打印[Slidev] Unauthorized file write; - 越界防护:将路径
path.resolve(userRoot, file)解析后,用path.relative(userRoot, filepath)判定是否仍在项目根目录内;若rel以..开头或为绝对路径(即解析后逃逸出项目根目录),则拒绝写入并打印Refusing monaco write outside project root; - 真实写盘:通过校验后打印
[Slidev] Writing file: <path>日志,并以utf-8编码调用fs.writeFile将内容写回磁盘。
第 4 步:插件注册与生命周期
createMonacoWriterPlugin被统一挂载在 vite/index.ts 返回的插件数组里,与 Monaco 类型加载、远程资源、HMR 补丁等插件并列。由于插件设置了apply: 'serve',因此只参与 dev server 的插件管线——这再次印证了第一节提到的"仅开发模式生效"约束。
五、安全须知与实用建议
写入是立即且直接落盘的
与「保存到内存、刷新即丢」的普通 Monaco 不同,{monaco-write}一旦保存成功,源文件立刻被覆盖,不经过暂存区,也没有撤销通道。官方文档给出的警告非常明确:使用前务必备份相关文件(Back up files before using - changes are saved directly)。对于参加现场演示的版本库,建议:
- 演示前对要修改的文件建立副本或提交一次 git commit,便于一键还原;
- 演示用的片段与正式代码尽量隔离,例如单独维护
@/snippets/下的演示文件; - 明确告知听众/协作者保存动作会真实改动工作区,避免误操作污染共享分支。
服务端做了哪些保护(可以从源码确认)
- 白名单机制:只有写入了
{monaco-write}指令、并成功解析出的文件才可被写入,见 monacoWrite.ts 与 snippet.ts; - 目录逃逸防护:写盘目标被强制约束在
userRoot(项目根)之内,无法通过路径拼接写任意位置; - 保存被
readonly覆盖:若同一代码块同时带只读选项,isWritable计算会将其关闭,保存动作被静默忽略(仅在控制台告警)。
常见问题排查
- 按
Ctrl/Cmd + S没有反应:确认正在运行slidev开发服务器(非构建产物预览)、编辑器未处于只读状态、且代码块确实通过<<<语法导入而非内联```书写; - 想让它自动保存而无需按键:当前实现为手动
Ctrl/Cmd + S触发,源码中没有自动定时写回逻辑,如需此能力可在社区讨论或自行扩展客户端 action; - 只想展示、禁止误改:去掉
{monaco-write}改回{monaco}即可恢复只读内存态。
六、总结
{monaco-write}是 Slidev 把「编辑器」与「代码仓库」打通的关键特性,适合一切需要真实改码的演示与教学场景。它由三部分协作完成:MarkdownIt 阶段的<<<导入与白名单登记、客户端 Monaco 的Ctrl/Cmd + S保存动作与 HMR 消息发送、服务端插件的白名单/越界校验与最终落盘。理解这条链路,你就能在幻灯片里安全、可靠地做 live coding,同时清楚何时不可用(构建产物)、如何防误改(备份与只读)。
延伸阅读(仓库内):可写编辑器的功能说明见 docs/features/monaco-write.md;<<<导入语法全解见 docs/features/import-snippet.md;普通 Monaco 与 diff 编辑器见 docs/features/monaco-editor.md;编辑器全局配置见 docs/custom/config-monaco.md。
【免费下载链接】slidevPresentation Slides for Developers项目地址: https://gitcode.com/GitHub_Trending/sl/slidev
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考