如果你只是偶尔用 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 里绝不动手。优先级功能模块说明P0Markdown 解析与渲染一切体验的基础正确性优先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 快很多但行内公式与中文标点的粘连需要额外调整样式否则会频繁出现公式被拆行的问题。比如$Emc^2$后面紧跟中文逗号时需要给公式元素加一点margin-right才能保证视觉不拥挤。这些样式上的细调普通用户可能感知不到但放在一篇满是公式的技术文档里差别立刻就能看出来。4.3 图片路径、资源管理与相对路径解析图片是长文档里最容易出问题的环节。不同 Markdown 工具对图片路径的处理差异很大有的基于文档目录有的基于仓库根目录导致同一份文档在不同工具里打开图片时有时无。我在编辑器里做了两件事一是提供插入图片命令可以自动复制图片到当前文档目录下的assets文件夹并生成相对路径二是在预览时把图片根路径统一映射到文档所在目录保证在任意位置打开文档图片都能正常显示。这里有一个细节值得分享很多编辑器在预览时对本地图片用的是file://协议但在 Electron 渲染进程里file://会被安全策略拦掉。我的解决办法是在自定义协议层注册一个md-img://协议专门负责读取本地图片并转换为可展示的 data URL 或 blob。这样既绕开了安全限制又能在图片不存在时显示一个友好的占位提示而不是让用户对着一张破图发呆。4.4 从 Markdown 到 Word/PDF 的导出链路导出功能是看起来简单做起来麻烦的典型。我没有直接用 Electron 的打印接口而是先转成 HTML再通过 Pandoc 转成 WordPDF 则优先走 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 快捷键体系与操作效率对重度用户来说快捷键是生产力。我除了支持常见的CtrlB加粗、CtrlK插入链接外还加了不少写作向的快捷键CtrlShiftM插入数学公式CtrlShiftC在选中文字周围插入代码块CtrlAltV粘贴为纯文本并自动转成 Markdown 引用。更关键的是整个快捷键表可以在设置界面里自由修改允许用户把系统默认的CtrlY重做改成自己习惯的CtrlShiftZ。快捷键不仅是按键映射还关系到命令系统的设计。我把所有操作都抽象成命令聚合到一个命令面板里这样用户按CtrlShiftP就能搜索所有可用操作。命令面板这个设计是从 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 编辑器最大的乐趣不是写出多少行代码而是你亲手改掉了那些在别人工具里只能忍受的细节。如果你也被某个编辑器的小问题反复折磨不妨也试着为自己做一个合适的东西说不定写着写着就变成了一款值得分享的作品。