如果你只是偶尔用 Markdown 写个 README,可能很难理解一个重度用户对编辑器的执念。我每天的工作流几乎被 Markdown 填满了:技术方案用 Markdown 写,会议纪要用 Markdown 记,博客初稿也是在编辑器里敲出大纲再慢慢扩写。正因为把太多时间花在编辑器上,我比任何人都清楚现有工具的问题——有的渲染漂亮但遇到大文档就卡,有的轻巧流畅但对 GFM 表格和数学公式的支持一塌糊涂。折腾几年之后,我干脆自己动手写了一个既好看又彪悍的 Markdown 编辑器。这篇文章就聊聊我在设计、开发和填坑过程中的真实思考,希望能给同样在寻找"完美编辑器"的朋友一点参考。
1. 为什么一个重度用户还要自己造编辑器?
1.1 我用过的 Markdown 工具和它们的痛点
长期写 Markdown 的人多半经历过一段"工具游牧"期。我用过 Typora,它那套所见即所得的渲染确实漂亮,但遇到上千行的技术方案或者复杂表格时,光标会明显迟滞,而且它对自动化操作的支持不够开放。我也试过 VS Code 加 Markdown 插件,轻量是真轻量,写代码顺手,但写长文时那种"沉浸式写作"的节奏很难找回来,预览窗口和编辑窗口来回切,总感觉思路被打断。还有一段时间用在线编辑器,浏览器里打开就能写,但一涉及本地图片、大文件、私有仓库,网络和权限问题立刻暴露。后来也用 Obsidian 整理知识库,双链功能很强,可它的定位毕竟是知识管理,对"编辑体验"本身的雕琢远远不够。
这些工具不是不好,而是没有一个完全长在我的使用场景上。我的使用场景是什么?技术方案动辄上千行,里面有 GFM 表格、任务列表、数学公式、代码块,还需要快速导出 PDF 或 Word 给同事评审。我要的不是一个"漂亮的阅读器",也不是一个"顺手的代码编辑器",而是一个专门为 Markdown 写作场景优化、渲染质量高、又能在细节上按我的习惯调整的编辑环境。既然现成的不完美,那就自己动手。
1.2 一个编辑器该解决的"非功能性需求"
动手之前,我先回顾了日常使用中那些真正影响心情的点。语法高亮和渲染只是基础,真正决定一个工具"能不能留下来"的,往往是几个不显眼的非功能性需求:启动速度要够快,打开一个大项目时不希望等半天;稳定性要足够好,写两个小时不保存忽然崩溃,这是最大的噩梦;可定制性要高,快捷键、主题、渲染规则都要能改,否则又只是另一个"别人家的编辑器";数据隐私要可控,所有文档都应该留在本地,没有账号体系,不上传云端。
把这四条当作约束条件,很多现成方案的取舍就清楚了。比如那些云编辑器,颜值高但数据在线,我不能接受;Electron 应用通常被诟病内存占用高,但只要控制好进程结构,启动速度和稳定性也可以做到可接受。所以,我最终决定走 Web 技术栈,做一款桌面编辑器,核心渲染走本地解析,不依赖任何在线服务。
2. 产品定位与功能边界:不要做了个四不像
2.1 目标用户画像与核心场景
做工具最怕什么都想塞进去,最后变成四不像。我把目标用户限定为两类人:一类是像我自己一样的文字工作者,以 Markdown 作为主要写作格式;另一类是开发者,需要快速记录技术文档并导出分享。核心场景有三个:写长文、整理笔记、输出文档。写长文强调流畅的输入体验和滚动中的即时预览;整理笔记要求快速搜索和清晰的标签管理;输出文档要求一键导出 PDF、Word 和 HTML,最好还能自定义样式。
这三个场景决定了编辑器必须支持"沉浸模式"和"源代码模式"两种视角。沉浸模式隐藏侧栏,让光标所在的行居中,熬夜赶文档时不刺眼;源代码模式则保留完整 Markdown 标记,方便处理复杂表格或嵌套列表时精确定位问题。一开始我也想做一个"永远所见即所得"的工具,但后来发现,某些场景下直接看源码反而更高效,所以两种模式要能一键切换,而不是二选一。
2.2 功能清单与刻意的减法
我最初列了一个很长的功能清单:文件树、多标签、全局搜索、图表实时预览、双链、插件市场……但这显然不现实,如果每一块都做,项目会拖到遥遥无期。最终我砍掉了双链和插件市场,只保留与 Markdown 编辑强相关的功能。原因很简单:双链是知识管理的活,插件市场是生态系统的活,这些如果做不深,只是给用户添乱。与其让用户在一个平庸的功能里失望,不如把核心体验打磨到极致。
保留的功能清单是:实时渲染预览,但不是逐键重排,而是节流后同步;GFM 支持,包括表格、删除线、任务列表、自动链接;扩展语法,包括数学公式、锚点目录、流程图;文件管理,支持打开文件夹、多标签页、文件树;导出,支持 PDF、Word、HTML;还有快捷键系统和自定义代码片段。这个清单看起来不大,但每项展开都是一堆细节。
2.3 功能优先级排序表
在开发排期上,我用一张表把功能按优先级排开。高优先级是渲染正确、输入流畅、自动保存;中优先级是文件树、多标签、导出;低优先级是主题市场、同步、插件 API。开发的时候严格遵守这个顺序,不然很容易陷入某个炫酷功能里出不来。这张表后来成了我的"防跑偏清单",每当我被一个新想法诱惑,就会回到表里问自己:这个功能属于哪个优先级?如果不重要,就先记在 backlog 里,绝不动手。
| 优先级 | 功能模块 | 说明 |
|---|---|---|
| P0 | Markdown 解析与渲染 | 一切体验的基础,正确性优先 |
| P0 | 编辑核心与自动保存 | 崩溃不能丢字 |
| P1 | 文件树、多标签 | 日常操作效率 |
| P1 | 导出 PDF/Word | 刚需,但不能拖垮核心 |
| P2 | 自定义主题、插件 API | 加分项,后续迭代 |
3. 技术选型:我为什么倒回了 Web 技术栈
3.1 Electron 与原生方案之争
本来想用原生技术写一个极速编辑器,但很快放弃了。Markdown 编辑器要处理的不只是文本,还有排版和渲染,原生开发在文本编辑、富文本显示、跨平台上都要重复造轮子,工作量太大了。权衡之后,我选了 Electron 作为外壳。很多人嫌 Electron 费内存,但只要做好进程管理,它带来的跨平台一致性和成熟的 DOM 渲染能力是值得的。我的方案是编辑器和预览各占一个渲染进程,主进程只负责窗口和文件读写,这样某个页面卡顿通常只影响局部,不会整个应用一起崩。
进程拆分只是一个开始。为了减少内存占用,我在主进程里禁掉了不必要的后台任务,比如自动更新、后台指标上报;渲染进程也严格控制了第三方依赖,能用原生 API 解决的绝不上库。用 Electron 不是为了偷懒,而是把省下来的精力投入到真正影响编辑体验的部分。
3.2 编辑器内核与渲染引擎的选择
编辑器核心我试过 CodeMirror 和 Monaco Editor。Monaco 是 VS Code 的内核,功能强大,但它的基因是代码编辑器,对中文输入法、排版段落、Markdown 软换行这些场景支持不够顺手。CodeMirror 更轻,API 也更加灵活。最终我选了 CodeMirror 6,因为它的模块化架构可以让我只装载需要的功能,同时它对中文输入事件的处理比 Monaco 更稳定。这个选择在后续开发中被证明是对的,尤其是处理中文标点、长句换行和 composition 事件时,CodeMirror 6 的灵活性帮了大忙。
渲染引擎方面,预览面板直接使用 React 来做 DOM 管理。很多人问为什么不用 Vue 或者 Svelte,其实选择 React 完全是因为我熟悉,而且在做虚拟 DOM 对比时更容易控制渲染频率。预览面板本质上是一个"从 Markdown token 树到 HTML 的映射器",用组件化思路可以把代码块、表格、引用等不同渲染单元拆成独立组件,后续扩展自定义渲染块时非常方便。
3.3 围绕 markdown-it 构建解析管线的理由
Markdown 解析器也经历了一轮选择。remark 生态很现代,基于 AST 容易做自定义;markdown-it 则胜在性能和插件生态成熟。我更看重速度和稳定性,所以选了 markdown-it,再加上 markdown-it-footnote、markdown-it-task-lists、markdown-it-katex 这些插件来覆盖扩展语法。解析结果是一棵 token 树,再由定时器节流后渲染到预览面板。核心代码大致是这样:
const MarkdownIt = require('markdown-it'); const md = new MarkdownIt({ html: true, linkify: true, typographer: true }); md.use(require('markdown-it-footnote')); md.use(require('markdown-it-task-lists')); md.use(require('markdown-it-katex'));这里要特别提一个设计:编辑器输入的源文本始终是唯一事实来源,预览 DOM 只是它的投影。所以光标滚动时,我可以快速计算当前光标对应预览内容的哪个位置,实现"双向同步定位",而不需要重排整篇文档。这是手写轮子最大的收获——我知道每一条数据的流动路径,而不是被框架的黑盒牵着走。
4. 从输入到预览,那些绕不开的格式细节
4.1 换行与段落:最容易被忽略的规则
Markdown 语法里最坑的不是表格,而是换行。标准 Markdown 里,单个换行在渲染时会被当作空格;要真正分段,得空一行。很多刚开始用 Markdown 的人在这里被折磨,所以我在编辑器里做了一个"换行友好"的选项:在编辑区按回车时自动插入一个空行,让源码里也能直观看到段落边界。同时在预览端,我保留了 GFM 的"换行即<br>"策略,但默认关闭,只对硬换行做行内换行渲染,避免表格和列表里的换行干扰布局。
这个细节看似简单,实际影响非常大。很多编辑器在渲染时会把每行都塞进一个<p>,导致整个文档变成一个巨大的段落,滚动时性能很差。我在渲染层做了"段落合并":连续的非空行先合并成一个逻辑段落,再交给 markdown-it 去解析,这样既符合 Markdown 的原始语义,也减少了 DOM 节点数量。实测下来,同样的文档,预览面板的 DOM 节点数减少了大约三分之一。
4.2 表格、任务列表、数学公式的兼容策略
GFM 表格是另一个重灾区。手写表格容易错位,尤其单元格里有竖线|时,必须转义成\|。我在编辑器里加了一个表格格式化命令,可以把选中的粗糙表格按列宽对齐。实现思路并不复杂:先按行拆分,再按未被转义的|切分单元格,计算每列的最大宽度后重新填充空格。这个命令成了我写技术方案时最高频的操作之一。
任务列表则依赖 checkbox 组件,点击后自动改写源文本里的[ ]和[x],实现真正的双向绑定。数学公式我用 KaTeX 渲染,速度比 MathJax 快很多,但行内公式与中文标点的粘连需要额外调整样式,否则会频繁出现公式被拆行的问题。比如$E=mc^2$后面紧跟中文逗号时,需要给公式元素加一点margin-right才能保证视觉不拥挤。这些样式上的细调,普通用户可能感知不到,但放在一篇满是公式的技术文档里,差别立刻就能看出来。
4.3 图片路径、资源管理与相对路径解析
图片是长文档里最容易出问题的环节。不同 Markdown 工具对图片路径的处理差异很大,有的基于文档目录,有的基于仓库根目录,导致同一份文档在不同工具里打开,图片时有时无。我在编辑器里做了两件事:一是提供插入图片命令,可以自动复制图片到当前文档目录下的assets文件夹,并生成相对路径;二是在预览时把图片根路径统一映射到文档所在目录,保证在任意位置打开文档,图片都能正常显示。
这里有一个细节值得分享:很多编辑器在预览时对本地图片用的是file://协议,但在 Electron 渲染进程里,file://会被安全策略拦掉。我的解决办法是在自定义协议层注册一个md-img://协议,专门负责读取本地图片并转换为可展示的 data URL 或 blob。这样既绕开了安全限制,又能在图片不存在时显示一个友好的占位提示,而不是让用户对着一张破图发呆。
4.4 从 Markdown 到 Word/PDF 的导出链路
导出功能是"看起来简单,做起来麻烦"的典型。我没有直接用 Electron 的打印接口,而是先转成 HTML,再通过 Pandoc 转成 Word;PDF 则优先走 Chrome 的无头打印,CSS 里专门写好@page规则,保证分页不把代码块截断。PDF 样式模板部分是这样的:
@page { size: A4; margin: 2cm 1.5cm; } pre { page-break-inside: avoid; background-color: #f6f8fa; padding: 12px; border-radius: 6px; }我踩过的一个坑是:直接用window.print()导出时,代码块的行号会被吃掉,切成无头 Chrome 打印之后才彻底解决。另外,导出的 Word 文档要想在同事电脑上不乱码,必须把中文字体嵌入或指定为通用字体,我在模板里统一设置了SimSun、Microsoft YaHei作为后备字体,经过几轮内测后基本没有收到排版错乱的反馈了。
5. 让"手感"变好的那些细节
5.1 快捷键体系与操作效率
对重度用户来说,快捷键是生产力。我除了支持常见的Ctrl+B加粗、Ctrl+K插入链接外,还加了不少"写作向"的快捷键:Ctrl+Shift+M插入数学公式,Ctrl+Shift+C在选中文字周围插入代码块,Ctrl+Alt+V粘贴为纯文本并自动转成 Markdown 引用。更关键的是整个快捷键表可以在设置界面里自由修改,允许用户把系统默认的Ctrl+Y重做改成自己习惯的Ctrl+Shift+Z。
快捷键不仅是按键映射,还关系到命令系统的设计。我把所有操作都抽象成命令,聚合到一个命令面板里,这样用户按Ctrl+Shift+P就能搜索所有可用操作。命令面板这个设计是从 VS Code 学来的,但我在里面加了"写作场景筛选":当正在编辑一个表格时,面板顶部会优先显示表格相关操作,而不是把所有命令都平铺出来。
5.2 自动保存、多光标和块级编辑
崩溃丢字是所有写作工具的原罪,所以我从第一版就加了自动保存:启动一个 5 秒间隔的定时器,把内容写入同目录下的.autosave.md文件,正常退出时再重命名回去。为了防止自动保存文件被同步盘或备份工具误处理,我在文件后缀里加了一段随机字符串,并且只在编辑器进程存活时才创建。这个机制看起来笨,但在一次 IDE 崩溃的场景里帮我找回了整整一下午的文档,从那以后我再没考虑过把自动保存关掉。
多光标虽然是代码编辑器带火的概念,但放在 Markdown 写作里也有用,比如批量给一组任务列表项前添加- [ ]。块级编辑则是按回车时自动延续列表、引用、代码块状态,省掉大量手动调整缩进的操作。最典型的例子是写有序列表时,如果中间插入一条引用,下一行的列表序号会自动重新排,而不是像普通编辑器那样需要自己手动改序号。
5.3 主题与外观:好看不是一个形容词
标题里说"既好看又彪悍",好看的关键是排版细节。我参考了常见阅读平台的排版参数,行高设为 1.75,段间距与行高分离,正文最大宽度控制在 720px,避免长行阅读疲劳。代码块、表格、引用都有独立的背景色和边框,亮色与暗色两套主题会跟随系统自动切换。为了让"好看"不是一句空话,我在设置里暴露了 CSS 变量,用户可以修改正文宽度、行高、字体族,甚至可以导入自己的 CSS 片段来覆盖默认样式。
有一次内测用户反馈说"暗色主题下代码高亮对比度太低",我去翻了一下 highlight.js 的默认样式,发现它自带的暗色主题确实对"终端绿"依赖过重。于是我换成了自己维护的语法配色,在色板选择上做了对比度检查,保证正文、注释、关键字之间的亮度差都达到无障碍标准。这个过程很细碎,但做完之后,我连续一星期都用暗色主题写作,眼睛的疲劳感确实下降了。
6. 踩过的坑、真实反馈与后续迭代计划
6.1 性能瓶颈:大文档渲染卡顿的排查过程
第一版做完,我觉得挺完美,直到有人放了一个 3MB 的 Markdown 文件进来,整个预览面板直接卡死。定位过程很典型:先用 Chrome DevTools 的 Performance 面板录制,发现长任务集中在 markdown-it 的parse阶段,而不是 DOM 渲染。于是我在解析层加了缓存,只对变更行所在的块做增量解析;同时在滚动预览时加了一个 IntersectionObserver,只渲染视口附近的区块。这一套组合拳下来,3MB 文件的滚动才恢复了 60 帧。
这个经历让我意识到,解析器的性能瓶颈不能靠堆硬件,要从算法上躲。增量解析的核心是维护一个"区块边界表":以空行为边界把文档拆成多个块,每次编辑时只重新解析光标所在块,再用一个基于版本号的缓存来判断哪些块的渲染结果可以复用。虽然实现起来比全量解析复杂,但对于长文档的收益是巨大的。现在编辑器里打开再大的文件,输入延迟也维持在可接受范围内。
6.2 用户反馈中最高频的三个问题
内测群里收集到的问题很有价值。第一个高频问题是"为什么我复制的表格没有对齐?"——原因是很多网页复制的表格其实是 HTML,不是 Markdown,我后来专门做了粘贴智能识别,自动把 HTML 表格转成 GFM 表格。这个转换并不只是替换标签,还要处理合并单元格、行内样式、嵌套标签,转换后可能丢失一部分样式,但至少保证了内容不丢。第二个高频问题是"导出 PDF 后代码块被分页切断",这直接推动了打印样式模板的开发。第三个问题是"输入中文时光标跳来跳去",这是 CodeMirror 6 的 composition 处理坑,翻了一晚上 issue 才找到正确的beforeinput事件处理方式。
真实反馈会告诉你,用户永远不在意你用了多先进的技术,只在意那些"不卡、不乱、不丢"的基本要求。所以我在做每个功能之前都会先问一句:如果这里出问题,用户会先骂我还是先理解我?凡是可能引发用户挫败感的地方,我都尽量用默认值把风险兜住,而不是把控制权抛给用户。
6.3 下一步想做的方向
编辑器目前还在持续迭代。我准备下一步做三件事:一是把导出流程从 Pandoc 依赖中解放出来,内置轻量转换逻辑,这样用户不需要额外安装命令行工具;二是提供一套真正的插件 API,允许用户注册自定义语法块,比如让团队内部的接口文档可以嵌入一个可折叠的请求示例;三是补上类似 Vim 模式的键位方案,照顾从代码编辑器迁移过来的用户。不过做这些事情之前,我会继续坚守那条原则:优先夯实编辑和渲染的稳定性,功能宁可少一点,也不能让用户感受到一次"不靠谱"。
在我自己的日常使用里,这个编辑器已经成了离不开的工具。它不是没有缺点,但它最大的价值在于:每一个让我不舒服的地方,我都能当天定位、当天修掉。做 Markdown 编辑器最大的乐趣,不是写出多少行代码,而是你亲手改掉了那些在别人工具里只能忍受的细节。如果你也被某个编辑器的小问题反复折磨,不妨也试着为自己做一个合适的东西,说不定写着写着,就变成了一款值得分享的作品。