作为一个把 Markdown 当日常书写工具的骨灰级用户,我对编辑器的要求其实早就超过了「能写字」这个层面。这几年市面上的 Markdown 编辑器我基本都试过一轮,要么界面花哨但不实用,要么功能够强但实在谈不上好看,真正能做到鱼和熊掌兼得的很少。折腾到最后,我决定自己动手,做一个既好看又彪悍的 Markdown 编辑器。
这篇文章不写什么宏大的愿景,就从一个 Markdown 重度用户的角度,把我从需求分析、方案选型到核心功能实现、美化思路、踩坑排查的完整过程记录下来。如果你也是一个被编辑器「逼疯」过的 Markdown 用户,或者正在考虑自己造轮子,这里面的取舍逻辑和实践经验,应该能帮你少走不少弯路。
1. 需求分析与产品定位:为什么我决定自研编辑器
1.1 从重度用户视角出发的核心痛点
Markdown 语法最大的优点是简单,最大的缺点也是简单。用习惯了之后,你的书写效率会变得很高,但对编辑器的期待也会水涨船高。我用过的不少 Markdown 编辑器,写短小的笔记没问题,一旦文档变长、章节变多,问题就全出来了。
首先是性能瓶颈。几万字的文档,在部分编辑器里输入一个字符都要卡半秒,滚动页面时明显掉帧。其次是排版审美问题,很多编辑器渲染出来的预览效果,跟发布到博客平台后的样子差距太大,字间距、行高、代码块样式都差点意思,写的时候还挺满意,导出到别处一看完全不是那么回事。再就是功能缺失,频繁插入图片、管理本地文件、快速搜索替换这些基本操作,在某些工具里做得非常反人类,你需要反复切换鼠标和键盘,手感极其断裂。
正是这些痛点让我意识到,我需要的是一个能覆盖「本地写作、实时预览、文档管理、发布输出」全流程的工具。既要有原生桌面应用的手感和性能,又要具备 Web 技术带来的颜值灵活性,还要在长期使用中扛得住超大文档和复杂排版。
1.2 方案选型:为什么用 Electron 而不是纯 Web 或 Tauri
技术选型是项目开始时第一个绕不开的决策。我考虑过三个方向:纯 Web 应用、Electron 桌面应用和 Tauri 桌面应用。
纯 Web 应用天然避开了安装分发的成本,但文件系统访问受限,你很难像本地编辑器一样自由读写磁盘上的 Markdown 文件,离线体验也糟糕。Tauri 是后起之秀,打包体积小、内存占用低,但它的前端与后端通信链路在频繁读写文件、高频渲染的场景下,会有明显的性能瓶颈。综合权衡下来,我选择了 Electron。理由很直接:Node.js 环境下文件读写毫无阻碍,Chromium 内核的渲染能力和调试工具成熟,社区生态极其庞大,踩到坑能快速找到解决方案。
当然 Electron 的槽点也很清楚,打包体积大、内存占用高。但作为一款面向创作者的工具型产品,稳定和功能完整比省那几百兆内存重要得多。实际开发中我用 Vite 管理渲染进程,配合 electron-builder 打包,整体工程结构不算复杂。
1.3 面向的使用场景与目标人群
这个编辑器我定位为「给创作型选手的本地写作工具」,目标用户是 Markdown 重度用户、技术文档写作者、博客维护者以及喜欢用纯文本管理知识的笔记党。核心场景有三个:长文写作、内容整理、多端同步。
长文写作场景要求编辑器具备流畅的滚动、稳定的光标定位和快速响应的输入体验;内容整理场景需要你有强大的文件树导航、全文搜索和标签能力;多端同步场景依赖对本地 Markdown 目录的直接监控和友好处理。说白了,我不打算做一个功能臃肿的「全家桶」,而是把高频刚需做到极致,让每个细节都经得起推敲。
2. 编辑器整体架构设计与核心模块拆解
2.1 基于 CodeMirror 6 构建编辑内核
编辑内核是整个项目的灵魂,我选的是 CodeMirror 6。你可能会问,为什么不用 Monaco 或 ProseMirror?Monaco 是 VS Code 的内核,功能很强,但它本质上为大型 IDE 场景服务,体积大、定制成本高,作为一个专注于 Markdown 的编辑器有些杀鸡用牛刀。ProseMirror 是富文本编辑器框架,虽然能直接操作文档树,但 Markdown 的纯文本输入模型与它并不完全匹配。
CodeMirror 6 恰好站在一个很好的平衡点上,它提供的是一个高度模块化的编辑器核心,把文档状态、视图渲染、输入处理拆成了清晰的模块,你可以按需组合,也可以轻松扩展。它默认就是增量渲染和虚拟滚动,几千行文档滑动起来依然流畅。这套架构让「好看」和「彪悍」都有了底层保障,前端的任何美化都不会拖累编辑性能。
2.2 解析渲染链路:从 Markdown 源码到预览视图
Markdown 的预览体验要想好,核心是解析渲染链路不能断。我的实现方式是在编辑器监听文档变更,将 Markdown 文本实时交给 marked 库解析成 HTML,再用 DOMPurify 做一轮安全过滤,最后注入到预览面板中。
这套链路看似简单,但实际开发中要处理三个细节。第一是滚动同步,编辑区和预览区必须保持对应关系,我采取的方式是监听编辑区的滚动位置,根据行号与渲染节点的映射关系,计算出预览区对应内容的偏移量。第二是图片路径解析,当文档中插入时,需要自动拼接当前文件的目录路径,让预览区直接显示本地图片。第三是防抖策略,输入时不立即解析,而是等 300ms 停顿后再重新渲染,避免大文档下每次击键都触发全量解析导致的卡顿。
2.3 文件树与多目录工作区设计
Markdown 写作者的文件管理习惯往往是「一个目录就是一个项目」,所以我做了一个多目录工作区的设计,可以同时把多个文件夹挂载进来,每个目录独立展开文件树,支持新建、重命名、删除、拖拽移动等基本操作。关键在于文件监控。
我使用 Node.js 的 fs.watch 递归监听目录变化,当外部新增或修改了 Markdown 文件时,文件树和编辑器标签页会同步更新。这里有一个非常容易踩的坑:fs.watch 在不同操作系统上的行为不完全一致,Linux 下目录监听容易丢事件,解决办法是用 chokidar 这个库替换原生监听。起初我在 Linux 测试环境上频繁遇到文件变更丢失,换成 chokidar 之后基本稳了,这个细节直接决定了工具在多平台下的可靠性。
2.4 数据持久化与自动保存策略
对于写作工具,数据安全永远是第一位的。我做了三层防护:第一层是常用编辑器的自动保存,文档内容变化后 800ms 内写入磁盘;第二层是自动备份,每隔 5 分钟把当前打开文件的上一版本复制到.backup目录;第三层是退出拦截,如果检测到未保存的更改,会提示用户确认。
自动保存看似简单,但要处理好一个冲突场景:当外部修改了文件,而编辑器内部也有未保存的内容时,直接覆盖会把外部修改弄丢。我的处理策略是,检测到文件变更时先读取磁盘内容,与编辑器当前内容比对,如果内容不一致就在编辑器内弹出冲突提示,让用户选择保留哪个版本。这个细节做不好,写作者辛辛苦苦写的内容可能一眨眼就没了。
3. 「好看」的落地:外观设计与主题系统
3.1 设计原则:内容优先与克制的视觉层级
好看这件事,见仁见智,但我始终认为编辑器的好看应该服从于一个目标:让内容本身成为视觉焦点。我的设计原则有两条,一是内容区必须干净通透,二是功能区域必须安静不抢眼。
编辑器整体采用三栏布局,最左侧是文件树,中间是编辑区,右侧是预览区。文件树底色比主区深一档,编辑区和预览区保持近乎纯白的底色,让视线一打开应用就会落到文字上。界面上尽量不出现无意义的装饰线、渐变和投影,宁可用留白来区分区域,也不用阴影强刷存在感。这些设计决策看起来不难,实际上要把「克制」落到每个像素上,需要不断打磨。
3.2 字体与排版:中文写作的细节打磨
Markdown 的大段文字阅读体验,很大程度上取决于字体和排版。正文我最推荐「思源宋体」与「霞鹜文楷」的组合,西文搭配 Inter 或 Source Serif,代码字体用 JetBrains Mono。行高设置在 1.7 到 1.8 之间,段落间距控制在 1.2em,最大宽度限制在 780px,避免长行文本造成阅读疲劳。
中英文混排的处理是很多编辑器不太注意的细节。我手动实现了中文与英文、数字之间的自动间距调整,也就是「盘古之白」,让文字在视觉上更加透气。代码块的样式同样做了精细处理,背景色要比正文底色深一档但不发黑,圆角控制在 6px,代码字号略小于正文字号,避免代码喧宾夺主。这些细节单独看都不起眼,组合在一起就是所谓的高级感。
3.3 主题系统:CSS 变量驱动的动态换肤
主题系统的高频操作是换肤,我的实现思路是用 CSS 变量统一管理所有颜色,切换主题时只需要替换一组变量即可。比如定义--bg-primary、--bg-secondary、--text-primary、--text-secondary、--accent-color等十几个核心变量,预览区的所有样式都引用这些变量。
这样做的好处是,用户完全可以不写一行代码,只需要编辑一个简单的 JSON 配置文件,就能定制出属于自己的主题。内置了亮色、暗色、护眼模式三套默认主题,暗色模式下所有颜色都经过了对比度校验,保证长时间写作不会刺眼。护眼模式则是把背景色调成淡绿色,适合夜晚或长时间写作的场景。
3.4 预览区的排版即最终发布效果
预览区的渲染效果直接决定了「所见即所得」的可信度。我不仅把 Markdown 转换为 HTML,还引入了一份精心调校的 GitHub Markdown 风格样式,并在此基础上做了一些中文排版的优化:标题自动加上分隔线、引用块改用左侧色条而不是背景色、表格做 zebra striping、图片默认 max-width 为 100% 并加圆角。
预览的字体和间距与常见博客平台高度接近,这样你在编辑器里看到的效果,基本就是发布后的效果,不会出现「编辑器里美如画,平台上一团糟」的割裂感。为了进一步还原发布环境,我还支持在设置里调整预览区的最大宽度和字体大小,适配不同的平台排版习惯。
4. 「彪悍」的支撑:核心功能实现与性能优化
4.1 大文档性能:从卡顿到流畅的关键优化手段
作为重度用户,大文档的性能是我的底线。几万行、几十万字的 Markdown 文档,放入编辑器后如果输入迟滞、滚动卡顿,那其他功能再好看也没用。CodeMirror 6 本身的增量渲染机制已经解决了大部分问题,但我还需要处理预览区和校验逻辑的性能瓶颈。
我做三件事:解析预览的防抖、大文档的虚拟滚动、以及逻辑分块渲染。解析防抖上面提过了,滚动方面预览面板在文档超过 200 行后自动启用 IntersectionObserver 进行懒加载,看不到的内容不渲染。编辑器输入监听则通过 requestIdleCallback 调度,确保核心输入操作永远优先响应。实际测试中,一份 5000 行左右的 Markdown 文档,输入延迟稳定在 20ms 以内,滚动全程不掉帧。
4.2 行内样式与语法高亮的实现细节
Markdown 语法高亮看似简单,实际上坑很多。最常见的是嵌套语法,比如一段加粗文字内部又有行内代码,或者标题行内包含链接,正则表达式处理起来非常痛苦。我采用的做法是基于 CodeMirror 6 的 lezer 语法解析器,为 Markdown 编写了完整的语法规则文件,让编辑器真正「理解」Markdown 的结构,而不是靠正则猜。
代码块的语法高亮我用的是 Lezer 搭配 language 包,支持常见编程语言。这里有一个值得注意的细节:高亮的颜色一定要在不同背景下都有足够对比度,亮色模式下代码高亮用了深色调,暗色模式下降,则自动切换为亮色调,保证两种主题下的可读性。
4.3 全文搜索与替换:编辑器效率的试金石
本地写作工具的搜索体验,直接影响日常效率。Markdown 编辑器里的搜索有几种层次:普通字符串搜索、大小写敏感切换、正则搜索、以及跨文件搜索。我全部实现了。
跨文件搜索的底层逻辑很直接:遍历当前工作区内所有 Markdown 文件,使用 ripgrep 这个命令行工具做快速匹配,把结果按文件路径和行号聚合,点击结果时直接打开对应文件并跳转到指定行。相比纯 Node.js 实现,ripgrep 的搜索速度几乎是秒出,上千个文件的目录也毫无压力。这个选型实测下来,搜索体验非常接近 VS Code。
4.4 扩展能力:自定义渲染器与命令面板
一个真正「彪悍」的编辑器不能让用户只能用它内置的能力,还得支持使用者自己扩展。我预留了两类扩展点:自定义渲染器和命令面板。
自定义渲染器是这么运作的:你可以提供一个函数,接收 Markdown 解析出来的 AST 节点,返回自定义的 HTML 字符串。比如你可以在文档里写一段特殊的代码块语法,渲染器识别后把它变成一个包含交互元素的组件。命令面板则是类似 VS Code 的 Ctrl+Shift+P,把所有功能暴露成可搜索的命令条目,提高键盘流用户的操作效率。这些扩展让一个通用编辑器变成了「你的」编辑器。
5. 常见问题与调试实录:你大概率也会踩的坑
5.1 中文输入法导致的组合输入异常
这是我开发过程中遇到的最折磨人的问题。在 Chromium 内核中,通过中文输入法打字时,编辑器会先接收到一个 compositionstart 事件,此时输入框里的内容处于「组合中」状态,如果编辑器在这个时候去做文档内容校验或格式化,会把组合中的文字弄乱。
解决办法很标准但必须写对:在 composition 事件序列中,不要触发任何 doc 变更监听逻辑,所有格式化、自动补全在这段时间内挂起。等 compositionend 事件触发后,再统一执行一次完整的解析。很多编辑器在这块处理得不够细致,用户用中文输入时会出现字符丢失、光标乱跳的问题,我这个编辑器从根上避免了。
5.2 大文件打开慢与内存溢出的排查实录
有大文件必然有内存压力。有一次我测试一份约 2 万行的 Markdown 文档,打开时编辑器卡了好几秒,内存直接飙到 800MB。排查后发现两大元凶:一是打开文件时一次性读了整个文件,又同时做了重复的语法树解析;二是预览区对全文做了 HTML 渲染,字符串拼接大量临时对象撑爆了内存。
事后我做了两个调整:读取文件后先快速渲染前 100 行给用户即时反馈,再在 requestIdleCallback 里做全量解析;预览区则改成按需渲染,只有滚动到对应区域时才生成对应 DOM 节点。优化后同样的文档打开时间降到 1 秒以内,内存占用稳定在 200MB 左右,观感好了非常多。
5.3 Windows 与 macOS 的文件路径兼容性问题
文件路径兼容性是个下来极其烦人的问题。Windows 系统使用反斜杠作为路径分隔符,macOS 和 Linux 使用正斜杠,很多字符串操作在处理路径时容易出 bug。我在插入图片、导出文件、文件关联等模块都遇到了这类坑。
最终解决方案是统一封装了一个路径处理工具函数:在读取路径时统一转换成/分隔的格式,写入磁盘时再根据当前平台转换回来。对于 Markdown 文档中的相对路径引用,也一律使用正斜杠写入,这样用户在 Windows 和 macOS 之间来回切换时,文档内容不会因为路径分隔符而报错。这个小细节,重度用户跨平台写作时会非常感谢。
5.4 实时预览与光标定位的同步修复方案
编辑区滚动到某个位置,预览区也要跟着走,这看起来简单,实现起来涉及到滚动容器高度、文档偏移量、渲染节点位置三者之间的换算,很容易出现「差一行」或者「跳来跳去」的问题。
我的方案是给每个标题元素生成一个唯一的锚点 ID,在 Markdown 渲染时把它们映射到对应的行号。编辑区的光标位置变化时,反向查找它所属的标题锚点,然后让预览区滚动到该锚点。这样可以保证滚动同步是稳定的,而不是通过估算行高换算出来的。因为标题在整个文档中的位置是明确对应的,很少出现偏差。
6. 发布与后续规划:从自用工具到开源项目
6.1 打包分发与用户反馈收集
项目打磨到一定阶段后,我把它打包成 Windows、macOS、Linux 三个平台的安装包。用 electron-builder 做多平台构建,过程中遇到最多的还是图标尺寸和安装包签名问题。macOS 的签名需要开发者证书,对个人开发者来说成本比较高,我先以未签名版本提供给社区测试,用户自行右键打开即可。
发布后最惊喜的是收到不少真实用户的反馈,比如有人提出预览区的代码行号、有人希望支持 Emoji 快捷键、还有人需要 vim 模式。这些来自真实使用场景的诉求,比自己闭门造车高效得多。我有选择地吸收这些建议,把高价值的合并进迭代计划。
6.2 未来迭代方向:插件系统与移动端适配
目前编辑器的扩展能力还停留在「配置文件驱动」的阶段,下一步我计划做一个完整的插件系统,让用户可以通过 npm 安装第三方插件。插件可以注册新的渲染器、添加命令面板条目、甚至修改编辑器的菜单栏。
移动端适配则是另一个话题。平板和手机上的 Markdown 写作需求其实在增长,但编辑器的交互模型需要大幅调整,目前主要聚焦在文件同步和预览阅读体验上,完整的移动端输入方案还在调研中。无论后续怎么发展,核心原则不会变:好看和好用,缺一不可。
最后再分享一个小技巧。如果你也打算开发自己的编辑器,别一开始就纠结「大而全」,先把你最高频的 20% 功能做到极致,剩下的用插件或扩展去补齐。一个编辑器让人愿意天天打开,靠的不是功能列表有多长,而是每一次敲击键盘的手感、每一眼看到排版时的舒适度。我在这个项目里最大的收获,就是明白了这个道理。