自研Markdown编辑器:从需求拆解到性能优化的完整实战复盘

发布时间:2026/9/16 22:54:08
自研Markdown编辑器:从需求拆解到性能优化的完整实战复盘 先说一句我是一个连购物清单都想写成.md文件的人。这些年用过的 Markdown 编辑器从 Typora 到 VS Code从在线 Markdown 工具再到各种号称“专注写作”的 App数量不算少但几乎每一款都让我在某个时刻有一种“不够顺手”的别扭感。有的颜值在线但性能拉胯打开一个几百 KB 的长文档就卡成幻灯片有的功能够猛但界面停留在上一个十年代码块和表格的渲染效果让人完全不想看第二眼。所以我干脆自己做了一个。这篇文章是这大半年开发过程的完整复盘。我会从需求拆解、功能规划、技术选型、核心实现一直讲到具体踩过的坑分享怎么把一个 Markdown 编辑器从“能跑”做成“好用且好看”以及我在这条路上总结的实操心得。如果你也是 Markdown 重度用户或者正打算自己开发一款编辑器类工具这篇文章应该能帮你在动手之前避开相当一部分弯路。1. 为什么自研一个重度用户被逼出来的需求1.1 我的 Markdown 工作流和工具史我的日常写作量不算小技术博客、项目 README、产品需求文档、会议纪要、甚至个人知识库全部跑在 Markdown 上。Markdown 的好处不用我多说纯文本、可版本化、随处可编辑、格式转换链路齐全。但使用频率高了之后对编辑器的要求会变得非常“刁钻”。我最初用的是某款知名的所见即所得编辑器界面确实漂亮写作沉浸感很强。但用了半年后发现几个痛点很致命一是大文档明显卡顿写一本五六万字的电子书时输入一多整个渲染区都在抖二是有时候我想精确控制 HTML 输出比如加一个自定义的div容器或者内联样式它有自己的处理逻辑总要跟我对着干。后来我换到 VS Code插件装了一堆功能是强了但那种“编辑器”的味道太重——侧边栏、状态栏、命令面板全是开发环境的氛围写作时总有一种在写代码的错觉。再后来我也试过在线的 Markdown 编辑器比如各种基于浏览器方案的优点是零安装、跨平台缺点是文件系统操作受限、离线能力弱、而且数据都放在别人的服务器上对知识库类的内容我总是有些不放心。于是开发一个自己的 Markdown 编辑器这个念头在一次写长文被卡顿折磨到暴躁的深夜彻底落地了。1.2 好看与彪悍两个核心关键词的拆解“好看”和“彪悍”这两个词我在这款编辑器的立项文档里写得很直白它们分别对应不同的用户价值。“好看”不只是换一套主题、换几个字体那么简单。它意味着排版要有呼吸感标题层级、段落间距、引用块、代码块、表格的视觉权重需要精心调校它意味着暗色模式不是简单地把背景反色而是需要考虑对比度、代码高亮配色、选中态、光标色这些细节它还意味着编辑器的 UI 要克制工具栏该收就收别让一堆按钮抢走内容的注意力。我把好看这件事拆成了渲染审美和交互审美两个层面后面会详细展开。“彪悍”则指向硬实力。第一是性能打开几 MB 的文档、渲染几千行的表格不能有明显的卡顿感第二是功能覆盖标准 Markdown 语法只是底线数学公式、流程图、代码高亮、自定义容器、脚注这些扩展语法要能开箱即用第三是生态兼容我写的文档可能在任何平台打开所以输出格式要兼容 GitHub Flavored MarkdownGFM、CommonMark 等主流规范导出的 HTML、PDF、Word 也要质量过关不能一导出就乱。1.3 自研 vs 魔改现有工具为什么选了最累的路其实动手之前我也犹豫过市面上编辑器这么多与其从零写不如魔改一个现成的。比如基于 VS Code 做一个定制化发行版或者给开源编辑器写插件都能省不少力气。但我研究了一圈之后还是决定从底层开始搭建核心框架。原因有几个。第一我希望编辑器的工作台界面是完全按写作场景定制的不需要文件树、Git 面板、调试工具这些视觉噪音第二我希望渲染引擎和编辑核心是深度整合的而不是像很多插件方案那样用“预览窗口”或者“iframes 刷新”把两边拼起来第三我想自己掌控性能问题的源头比如当输入抖动发生的时候我能精确知道是解析器慢还是渲染层慢而不是在一堆第三方插件的黑盒里猜来猜去。当然这个决定也意味着工作量成倍增加。好在我对核心架构有个明确规划底层方案直接选择成熟的开源组件不在“重复造轮子”上浪费生命而是把精力集中在整合、打磨和功能创新上。2. 核心功能与场景设计从痛点倒推需求2.1 输入体验换行、补全、标记高亮写 Markdown 看起来是纯文本输入但重度用户会告诉你输入体验的细节多得惊人。我先说一个几乎所有新手都会遇到、但很多编辑器没处理好的问题Markdown 换行。Markdown 的换行规则本身就反直觉——在 Markdown 源码里按一次回车在渲染结果里经常不会换行要另起段落得空一行要生成软换行得在行尾敲两个空格。这对新手是噩梦对我这种老手来说也是机械且容易出错的操作。所以我在编辑器里做了一套“智能换行”策略按Enter直接开启新段落渲染结果和源码语义保持一致按Shift Enter插入br软换行源码层会提示添加行尾空格。这样既符合直觉又保留了对 Markdown 规范的控制力。同时我还在状态栏做了一个很不起眼但很实用的小提示显示当前行的换行方式避免用户搞不清自己写的是段落还是软换行。输入方面我还做了几个让日常操作变顺滑的设计。语法标记的即时高亮是基础光标所在行的标记和其它区域的标记在色彩上要有区分度这样你永远不会在长文档里迷失当前编辑位置。自动补全方面除了基础的引用语法、列表-这类我做了更聪明的上下呼应比如你输入一个[编辑器会根据上下文提示是加链接还是加图片语法输入之后会自动把语言类型补全菜单弹出来。代码块的 Tab 键缩进和 ShiftTab 反缩进逻辑我也专门对齐了 VS Code 的手感因为从那边转过来的用户肌肉记忆很顽固不要试图改变他们。2.2 内容承载数学公式、表格、代码块、图片路径重度用户写文档不可能永远停留在“标题加段落加列表”的层面。数学公式、复杂表格、代码块、图片管理这四件事是 Markdown 编辑器能不能“干活”的分水岭。数学公式方面我选择了 KaTeX 作为渲染引擎。相比 MathJaxKaTeX 的渲染速度要快几个量级这对输入流畅感非常重要。KaTeX 支持 LaTeX 语法的大部分常用宏行内公式用$...$块级公式用$$...$$。但在解析层有个坑$符号在普通文本里也经常出现比如金额“$99”所以需要用更严格的规则判断“这是公式开始”还是“这只是货币符号”。我最后采取了“行内公式需要闭合且内部无多余空格”等约束条件来减少误判这个细节后面踩坑部分还会讲。表格是另一个大工程。原生 Markdown 表格写起来非常痛苦一个四列十行的表格如果手工对齐竖线能让人写到怀疑人生。我在编辑器里内置了一个“表格格式化”操作选中任意一段表格源码一键按列宽自动对齐把管线排版交给程序处理。另一方面我实现了表格的“源码到可视化”双模式视图在编辑区里是规整的源码在预览区里是带边框、表头背景色、斑马纹的可读表格。光标悬停到预览表格上时还会浮现一个“以表格编辑器打开”的按钮可以切换到类似在线文档的行列编辑界面改完自动回写到 Markdown 源码。这个功能开发周期最长但也是用户反馈里好评度最高的功能之一。图片路径的管理我花了很多心思。Markdown 写图最烦的事情就是路径不统一本地相对路径在别的目录打开文档时挂掉绝对路径在另一台电脑上完全失效链接到云端又受网络限制。我的方案是分场景处理如果是粘贴剪贴板里的图片默认存到当前文档同级的assets目录下自动生成相对路径引用如果是拖动一张本地图片进编辑器会提示你“复制到当前文档目录”还是“保留原路径引用”同时支持file://协议的本地文件路径方便在非 Web 环境下使用。这套逻辑让图片引用崩溃的问题基本绝迹。2.3 输出能力PDF、Word、HTML 转换链路写 Markdown 很爽但交付给别人时PDF 和 Word 往往是刚需。市面上的做法五花八门有人用 VS Code 装插件导出 PDF结果发现还需要额外安装princexml之类的转换引擎折腾半天有人把 Markdown 内容复制到在线编辑器再导 Word格式总是跑偏还有人干脆 pandoc 一步到位但命令参数和模板定制又是一大学习成本。我的编辑器把输出能力做成了内置链路目标是“不依赖外部程序、不打开浏览器、一键出文件”。PDF 导出走的是 Electron 的printToPDF能力把渲染好的内容套用一套专门的打印样式按页面格式重新分页生成 PDF。这个方案的好处是所见即所得预览区什么样PDF 就基本什么样。我在这套打印样式里针对中文排版做了不少调整比如页面边距、段落首行缩进、标题分页控制确保生成的文件可以直接用于正式场合。Word 导出我用的是两条路径并存。第一条路径是内嵌的 HTML 转换逻辑先把 Markdown 渲染成结构化的 HTML再包装成.docx兼容的格式适合大部分普通文档需求切换速度极快也没有外部依赖。第二条路径是调用系统里已安装的 pandoc 做高质量转换适合包含复杂公式、交叉引用、自定义样式的文档。编辑器会自动检测 pandoc 是否存在并在设置面板里给出提示不需要用户在命令行里敲任何东西。HTML 导出则比较纯粹输出一个自带完整 CSS 的独立 HTML 文件图片会内嵌为 base64这样发给别人一个文件就能直接打开浏览不会因为图片路径问题变成“皇帝的新衣”。3. 实操实现核心环节怎么落地3.1 技术选型与架构设计技术选型我纠结了很久最终定下来的是 Electron CodeMirror 6 markdown-it KaTeX 这条组合路径下面逐个说说理由。Electron 作为跨平台桌面外壳虽然常被批评体积大、内存占用高但它的生态成熟度无可替代。浏览器内核意味着我可以直接使用 Web 技术栈做 UI 和渲染剪贴板、文件系统、系统菜单这些能力也都有成熟的 API。我后来还针对内存占用做了很多优化比如让闲置窗口进入低功耗模式、动态卸载不可见的预览资源等。如果你不需要桌面端的文件系统和系统集成能力其实也可以用 Tauri 这类更轻的方案但代价是你要把底层从 JavaScript 换到 Rust 技术栈生态成熟度也弱一些。核心编辑器组件我选了 CodeMirror 6。它最吸引我的是模块化架构——你可以只加载需要的语言包和扩展而不是像某些老牌编辑器一样捆绑一大堆用不上的功能。CodeMirror 6 的 decoration 机制非常强大可以做语法高亮、代码折叠、行内提示、选区装饰等精细控制性能和扩展性都很好。它的 Markdown 解析支持 GFM并且允许自定义语法扩展这正好匹配我前面说的“公式、表格、自定义容器”需求。渲染层用了 markdown-it。我没选择 unified/remark 那一套是因为 markdown-it 的插件生态更丰富、上手更直接而且它支持通过自定义规则做细粒度渲染控制。比如我在 markdown-it 上写了自定义容器插件对应::: tip这种块级语法、脚注插件、以及表格增强渲染插件这些都是基于它的 token 流 API 实现的扩展成本很低。3.2 实时渲染与滚动同步的核心实现做 Markdown 编辑器最大的体验分水岭就是“编辑”和“预览”之间的配合。很多传统的 Markdown 编辑器采用左右分栏模式左边源码右边预览但两个区域各滚各的非常割裂。我最终采用了双栏同步滚动的策略左侧是源码编辑器右侧是实时渲染的预览区两边保持位置联动。这个“滚动同步”的实现比想象中复杂。如果你的实现只是简单地按“滚动百分比”去映射你会发现源码里第 30 行的标题在预览区里的位置和百分比算出来的位置完全对不上——因为代码块、图片、表格在源码区占的字符数和在预览区占的像素数完全不成比例。我采用的方案是基于块级映射的同步算法。具体来说左侧源码区按“逻辑行”划分成块右侧渲染区按 DOM 元素划分成块中间通过一个“源码行号到渲染元素”的映射表关联。当一侧滚动时先找到当前视口顶部的块 ID再在另一侧定位到对应块并计算需要补偿的偏移量。比如源码区的标题在预览区可能只占 40 像素高度但它后面紧跟的图片占了 300 像素这时百分比映射就会错位而块级映射能例外处理这种场景。这个算法跑起来之后左右滚动的跟随体验非常顺滑。性能优化方面我做了两类处理。第一是解析渲染的节流用户在编辑时会连续触发输入事件如果每敲一个字符就全量重新解析文档大文档一定会卡。我设置了 60ms 的防抖窗口并且只重新渲染发生变化的段落块而不是全量构建整个预览区。第二是滚动事件的光标跟随策略同步滚动时不是每次都更新目标侧滚动位置而是通过 requestAnimationFrame 按帧合并更新这样高频滚动下也不会产生滚动抖动。3.3 表格编辑、数学公式与图片管理的细节处理表格编辑是我投入精力最大的模块这里展开讲讲。Markdown 表格源码长这样| 姓名 | 年龄 | 城市 | |------|------|--------| | 张三 | 28 | 北京 | | 李四 | 35 | 上海 |如果靠人肉对齐管线和内容写起来确实想砸键盘。我在编辑器里实现了“表格格式化”命令选中表格区域后按快捷键程序会根据每列内容的最大宽度自动补齐空格和对齐竖线。这个逻辑看起来简单但要注意几个边界情况单元格内容里包含|字符时需要用\|转义否则拆分就会出错表格的标题分隔行第二行在全对齐之后要保持---的合理长度如果表格里混入了不规范的格式比如某些行少了一列格式化工具要给出提示而不是静默吞掉数据。预览区的表格渲染我也做了很多细节表头加粗并带背景色、单元格内长文本自动换行、支持表格内的行内代码和链接、斑马纹让长表格更好读。另外我针对高频的“表格复制到 Excel”场景做了优化——在预览区选中任意一个表格复制时会自动生成一份 TSVTab 分隔值格式的副本放到剪贴板这样你直接粘贴到 Excel 或 Numbers 里就能呈现为结构化表格。这个功能源于我自己的真实需求我经常在 Markdown 里维护了一些配置清单需要同步到 Excel 做数据分析以前每次都要手动把竖线替换成 Tab有了这个功能之后一步到位。数学公式的处理主要是格式识别和渲染体验。KaTeX 的渲染速度足够快但你要在源码编辑器里给公式做高亮这就得保证解析器能准确判断 $ 符号的边界。我设置了一个规则$与内容之间不能有空格并且闭合符号之后必须是标点符号、空格或者行尾这样最大限度减少把美元金额误判成公式的概率。块级公式用$$独立成行内部不做段落解析保持 LaTeX 源码的原样展示。预览区里的公式采用 KaTeX 的displayMode渲染并且支持公式编号功能方便写论文或技术手册的场景。图片管理的实现分两大块粘贴处理和路径策略。粘贴剪贴板图片时编辑器先通过 Electron 的剪贴板 API 读取图片数据然后弹出一个轻量确认框是保存到当前文档的assets目录默认推荐还是保存到用户指定的绝对路径。确认后通过 IPC 信道把图片 base64 写到目标文件再在文档里插入相对路径或绝对路径引用。路径策略上我还在设置里增加了“自动将绝对路径转换为相对当前文档的路径”功能这样整个文档库挪到别的位置也不会断图。3.4 导出 PDF 和 Word 的完整方案我在导出这块做得比较细致因为这是很多 Markdown 编辑器最容易翻车的地方。先说说 PDF 导出。printToPDF虽然是 Electron 的内置能力但要导出排版精良的 PDF关键在于有没有一套专用的打印样式表。我的做法是把 Markdown 渲染出来的 HTML 嵌入到一个隔离的打印窗口中应用print.css。这套样式表里页面尺寸设为 A4页边距 2 厘米正文用了适合中英文混排的字体栈标题h1到h3分别设置了分页控制标题不孤悬在页面底部表格和图片设置了max-width: 100%防止溢出长代码块做了浅灰背景和细边框并且开启自动换行避免代码被截断。另外我在导出时增加了“自动生成目录页”选项通过解析文中的h1/h2标题生成带页码锚点的目录对大文档尤其友好。Word 导出的实现走的是两条路。内置的轻量转换方案是把渲染后的 HTML 转成 Word 兼容的格式。这里有个关键技巧Word 能识别mso-前缀的 CSS 属性所以我的转换样式里针对性书写了微软 Office 的兼容样式确保字体、间距、表格边框在 Word 里尽量接近编辑器预览效果。这个方案的优点是零依赖、速度快适合日常文档。如果文档里包含大量复杂公式、需要交叉引用或者学术排版要求编辑器会检测系统里安装的 pandoc调用它做高保真转换。我在设置面板里增加了 pandoc 路径检测和版本信息展示用户不用接触命令行一切都在界面里完成。4. 踩坑实录开发中遇到的问题与排查技巧4.1 大文档性能问题的定位与优化开发到中期我用一份十几万字的 Markdown 电子文稿做压力测试发现输入时明显有延迟打字快了还会出现“丢字”现象。让我列出排查过程这些步骤对写编辑器类工具的人很有参考价值。第一步是定位瓶颈在解析还是渲染。我先在源码编辑区输入发现光标响应正常只是预览区更新滞后那说明 CodeMirror 本身没问题瓶颈出在解析渲染链路。第二步是确认全量解析的开销。我用 markdown-it 的 benchmark 测了一下十几万字的文档全量 tokenize 大概要几百毫秒这如果在打字时频繁触发卡顿是必然的。定位到问题后我把渲染链路改成了分块增量更新文档被切分成多个区块按一级标题或者空行分组解析时只重新解析发生变化的区块和受影响的关联区块其余部分直接用缓存。这个优化让输入延迟从几百毫秒降到了 20 毫秒以内体感终于从“能忍受”变成了“顺滑”。另一个被忽略的性能点是预览区的 DOM 数量。长文档渲染出的 DOM 节点可能上万个浏览器在布局和绘制时的压力很大。我的解法是窗口化渲染——只渲染可视区域附近的元素视口之外的区块占位显示。这个做法借鉴了虚拟列表的思想但实现要更复杂一些因为要保证滚动映射的正确性。不过做完之后即使面对几十万字的文档预览区都不会有明显的滚动掉帧。4.2 剪贴板图片和路径兼容性的问题图片粘贴功能上线后测试时发现一个诡异的 Bug在 Windows 上从微信截图粘贴到编辑器图片能插入但使用时其他电脑上路径失效。排查后发现两个问题叠加在一起。第一个问题是路径分隔符的差异。Windows 用反斜杠\macOS 和 Linux 用正斜杠/。我最初在生成相对路径时直接用了系统默认分隔符结果文档传到 macOS 上Markdown 渲染器不认识反斜杠路径。解决方案很简单所有写入 Markdown 源码的路径统一转成正斜杠/格式这也是 Markdown 规范推荐的做法。第二个问题是文件名编码和特殊字符。Windows 里的中文文件名在某些编码环境下会转成乱码截图软件的临时文件名还可能包含空格和特殊符号这些都会导致 Markdown 链接解析错误。我的方案是粘贴图片后给保存的文件自动生成一个规范的文件名例如按“时间戳 序号”命名同时保留原始文件名作为备选信息这样既保证路径稳定又避免特殊字符干扰。4.3 中文输入法与快捷键的冲突这是桌面编辑器的一个老大难问题。开发测试中我发现在中文输入法处于开启状态时按Ctrl B这类快捷键会被输入法截获导致编辑器收不到按键事件加粗操作失灵。更麻烦的是输入法候选框弹出的过程中如果用户按方向键选择候选词这些按键事件有可能被编辑器误判为光标移动造成编辑状态混乱。我通过几个层面的措施缓解了这个问题。首先是在编辑器层面监听compositionstart和compositionend事件在输入法组合期间屏蔽掉所有编辑快捷键和光标移动命令避免冲突。其次是在渲染层设置ime-mode相关属性和输入区域的样式降低输入法在代码编辑区域的行为异常概率。最后我在编辑器的快捷键管理器里增加了一层“输入法状态感知”组合期间按快捷键会弹出轻提示告诉用户当前处于输入法状态而不是静默失败。虽然不能 100% 解决系统级输入法的兼容问题但实测常见的搜狗输入法、微软拼音、macOS 自带输入法稳定性都提升了好几个量级。4.4 表格格式化与复制的边界情况表格格式化命令写完后我拿真实世界的 Markdown 文档测试发现了一个翻车场景有些从网页直接复制的表格源码并不规范比如某些单元格里包含多行文本或者分隔行写的是| --- | --- |而不是|------|------|格式化后反而把结构弄乱了。我调整了表格格式化策略先做结构校验如果发现表格行数或列数不一致就中止格式化并给出错误提示而不是强行操作。同时支持“宽松模式”在用户明确选择的情况下按每列最大单元格数量补空位保证表格能被解析器正确处理。另外如果单元格内有多行文本格式化时会将多行统一合并到一行并用br标记软换行这样预览区的渲染结果能保持一致。这些边界情况的处理让格式化工具从一个“玩具”变成了真正可日常使用的功能。5. 后续规划与个人心得5.1 还有哪些可以继续扩展的方向编辑器的主体功能已经完成了但距离我理想中的形态还有不少路要走。我最想做的下一步是知识库联动能力目前的编辑器围绕单个文档工作流很顺手但一旦涉及多个文档之间的交叉引用、反向链接和全文检索就需要构建一个文档级别的索引体系。我打算引入类似 Zettelkasten 的双链笔记能力Markdown 源码里支持[[ 文档名 ]]语法编辑器的知识图谱面板可以实时展示文档关联。另一个方向是开放插件系统。不少用户反馈希望自己写插件扩展编辑器功能比如自定义一个代码高亮主题、添加一个文档模板引擎、或者接入 ChatGPT 做写作辅助。我计划参考 VS Code 的插件模型做一套轻量的插件 API让社区可以围绕编辑器构建生态。插件系统一旦开放编辑器就不只是我一个人的工具而是能长成伴随更多人写作习惯的产品。5.2 开发过程中最大的收获与建议回头看这大半年的开发经历我最大的感触是做工具类产品最难的不是功能实现而是对“好的体验”的持续打磨。很多功能的实现逻辑其实并不复杂难点在于把无数个小细节反复调整到让人心生好感的状态。比如滚动同步算法第一版能跑的时候我自己用都觉得别扭又花了两周时间调块映射、做按帧更新才最终达到“丝滑”的体验。如果你也想做类似的项目我的建议是第一不要在底层重造轮子Electron、CodeMirror、markdown-it 这些方案已经足够成熟把精力放在整合和差异化功能上第二从自己最痛的那一个点切入比如对我来说是表格编辑和导出质量先把一个点做到极致再逐步扩展第三一定找真实用户在早期测试只有真实使用场景才能暴露那些你想象不到的边界问题。这个编辑器现在已经成为我每天最常用的工具。我可以一边写博客一边顺手把表格复制到 Excel 统计又或者一键导出给同事审阅。也许在别人眼里它只是众多 Markdown 编辑器中的又一个选择但对我来说它是我对“好看”和“彪悍”这两个词的理解的最终呈现。