
用了六年 Markdown我写长文、记笔记、写周报、存技术文档几乎所有的文字产出都靠它。可越用越觉得不对劲好看的编辑器通常功能单薄功能扎实的又大多丑得让人提不起力气写东西。折腾了十几个工具之后我决定自己动手干脆做一个既好看又彪悍的 Markdown 编辑器出来。这个想法不是一时冲动。Markdown 语法本身并不复杂难的是怎么把语法背后的写作体验做好。市面上很多编辑器要么在排版上偷懒直接把黑色等宽字体往屏幕上一扔要么功能堆得乱七八糟打开主界面先弹一堆弹窗。我想要的编辑器必须打开就能写、写起来不打断思路、写完能直接交付成品同时在屏幕上每一处细节都让人觉得舒服。这篇文章就把我从需求梳理、设计思路、核心功能实现到踩坑排查的完整过程聊透希望能给同样对 Markdown 工具不满意的朋友一些参考也算是一次真实的全栈开发记录。1. 从一个重度用户的怨念说起为什么市面上没有一款真正顺手的 Markdown 编辑器1.1 我用过的那些 Markdown 编辑器问题出在哪先说背景。我曾经是各种主流 Markdown 编辑器的长期用户。桌面端用过老牌的 Typora、开源界口碑不错的 Mark Text也用过 VS Code 里的插件方案网页端试过语雀、Notion 这类带 Markdown 支持的在线文档终端里还折腾过 Vim 插件配合即时渲染的 Markdown 预览。工具换了一轮又一轮但总觉得差一口气。Typora 的实时渲染确实舒服打字时没有左边源码、右边预览的分裂感。可当文档写到上百 KB或者贴进几张稍微大点的图片它的表现就会肉眼可见地变慢对很多人来说闭源本身也是顾虑想自定义点行为都找不到入口。Mark Text 是开源项目理念很好但长期停留在较早期的功能状态插件体系几乎没有表格编辑、脚注支持这些快捷键和解析能力都比较粗糙。VS Code 插件本质上是在代码编辑器里模拟写作环境那排侧边工具栏、那种文件树气质怎么看都还是在写代码而不是在写文章。网页端在线文档的 Markdown 支持就更分裂了。有些产品把 Markdown 语法藏在富文本内核之上按回车后语法被立刻转换成富文本你想检查一下原始强调标记都没法看另一些产品支持非常有限表格、公式、脚注、目录这些扩展一个都不给图表、居中、自定义容器这些就更不用想。最让我抓狂的是导出环节很多在线编辑器根本不提供 Markdown 导入导出每次换工具都在数据迁移上耗费大量精力。说到底市面上的编辑器分成两个阵营一类把 Markdown 当作文本语法来支持功能弱、视觉糙另一类把编辑器包装成漂亮的笔记应用但 Markdown 只是其中一种输入法你无法完全掌控文档的底层结构。而重度用户的真实需求其实是两者结合既有高质量的渲染排版又有完整、标准的 Markdown 语法能力还能方便地导入导出、扩展中高级功能。1.2 下定决心自己写我到底想要一个什么样的编辑器判断一个工具能不能满足重度需求我通常会问自己三件事日常写作流程会不会被打断遇到奇怪的格式需求能不能解决换工具、换电脑时内容能不能无损带出来顺着这三个问题我给自己定义了一个需求清单。第一条是沉浸感。打开编辑器后界面要尽量干净没有多余按钮和弹窗。工作区就是正文像白纸一样。所有功能调用都应该用快捷键或者命令面板完成而不是靠工具栏点来点去。第二条是排版美。Markdown 编辑器的核心价值就是把轻量级语法变成看得舒服的排版字体、行高、字距、标题层级、代码块、引用样式每一项都要认真调。第三条是能力完整。GFMGitHub Flavored Markdown规范要覆盖包括表格、任务列表、删除线、自动链接更高阶的脚注、目录、数学公式、图表 mermaid、代码高亮也得支持。第四条是性能。编辑大文件、实时渲染、图片粘贴处理都不能有明显卡顿。最后是数据自由所有文档必须是我自己的普通 Markdown 文件无锁定随时能迁移。这个清单看起来很长但放到开发里其实就是一句话我做的是一个本地优先的 Markdown 写作工具用 Electron 技术栈实现跨平台桌面窗口用 Node.js 处理文件系统和中间数据渲染层完全由 HTML/CSS 控制。本地优先保证了速度和隐私文件系统暴露了文档目录让用户拥有全部数据而真正的难点在于把 Markdown 解析、实时渲染、编辑交互和界面美学这几层的东西缝合成一个流畅的整体。2. 整体设计思路好看是皮囊彪悍是内核2.1 好看从排版引擎到配色方案的细节打磨好看不是换一套主题皮肤那么简单它建立在一套完整的排版约束上。我用了自己维护的一版 CSS 排版系统把正文区域设计成类似阅读纸书的版心宽度默认 720px特别宽的表会允许横向扩展。字号基准设为 15px正文行高 1.75中英文混排时依赖font-family: Iowan Old Style, Palatino Linotype, Source Han Serif SC, serif这类字体栈做优雅回退代码块则使用JetBrains Mono和等宽回退字体。标题体系故意拉开差距。一级标题不只是字体更大还在下方增加了较粗的分隔线视觉上像文档的章节封面二级到四级标题通过字号递减和颜色变浅形成层级感。引用块左侧用了四像素的暖色条同时把背景压暗让引用和正文一眼就能分开。代码块用深色背景加细微阴影行号在窗口宽度足够时才显示窄屏下自动隐藏。这个版式的关键不是某个单独的元素而是所有元素共用一个 8px 间距系统一个间距值衍生出块间距、标题间距、图片边距等整个排版会显得特别统一。配色方案我做了三套亮色、暗色、护眼的暖纸色。暖纸色不是简单的米黄色而是带一点低饱和的棕色底配上墨蓝色正文长时间盯着屏幕也不会刺眼。每套主题都严格定义了语法高亮的 token 颜色比如强调文字用深红色加斜体链接用青色加下划线行内代码用赭色背景。源码区和预览区在暗色主题下也要保证对比度达到可读标准这对夜间码字的朋友很重要。很多人说编辑器好看是一个主观感受实际上当排版密度、字体层级、间距和配色都到位时主观感受就会朝着“舒适”收敛。2.2 彪悍不止编辑更是写作操作系统好看负责留住用户功能才真正决定一个工具能不能扛住日常工作流。我在设计功能时始终用“写作流程是不是被打断”来检验每个按钮该不该存在。于是我把功能分成三层第一层是编辑本身的体验包含快捷键、自动补全、多光标、自动保存第二层是 Markdown 扩展能力包含脚注、目录、数学公式、甘特图、流程图第三层是文档管理能力包含目录树、全文搜索、标签、附件归类。这三个层次合起来编辑器不再是一个输入框而是一套围绕写作的场景化工作台。一个容易被忽视的设计是命令面板。我参考了代码编辑器里Ctrl/Command Shift P的交互把几乎所有功能统一收进一个可搜索的弹窗。比如想插入表格不用找菜单按快捷键输入“table”回车即可。这个设计把工具栏压缩到只剩几个基础图标让整个界面保持干净同时又保证了功能可被发现、可被调用。对重度用户来说把大脑里“做什么”直接映射到“按什么键”是写作不卡壳的重要保障。自动保存也不只是定时写文件。我在程序里实现了保存队列对内容做 diff 式的增量写入同时保留.bak备份文件。工作过程中每次停顿超过 1.5 秒就触发一次保存加上防抖机制基本做到边写边存。崩溃恢复功能会检测上次退出时的未保存状态打开文件时如果发现备份文件和当前文件不一致会弹出恢复提示。这些细节初看并不起眼但事故遇到一次就懂它的价值。3. 核心功能与关键实现把体验做到极致3.1 双栏实时预览与滚动同步是怎么做的实时预览有两种常见方案。一种是像 Typora 那样把源码视图和渲染视图合二为一编辑的同时就地渲染另一种是左边源码、右边预览的双栏模式。我最终选择做双栏不是因为这个方案更高级而是从工程可控性考虑双栏模式下的解析、渲染和编辑逻辑可以完全解耦出了问题也容易定位。但在交互上我做了大幅优化两栏之间可以拖动调节宽度也可以一键隐藏左栏或右栏让双栏模式拥有接近沉浸式编辑的体验。双栏模式下最影响使用感受的是滚动同步。我的实现思路并不依赖第三方库而是自己维护一个位置映射表。每当 Markdown 解析器生成渲染后的 HTML 时我会记录每一行的源码位置和对应 DOM 节点的位置。正文滚动时计算当前视口在源码中的百分比再根据这个百分比找到对应的渲染节点然后用scrollIntoViewIfNeeded平滑滚动到目标位置。这个方案看起来简单实际实现时要处理插入和删除造成的偏移因为一旦文档变化旧的映射就失效了。我采用了一个成本很低的方法在映射表中给每一行附加一个稳定标识比如标题文本或段落 hash滚动时优先按稳定标识定位找不到再按百分比补偿定位。预览区域的渲染则采用“全文档渲染 渐进式更新”的策略。文档内容变化时不是每次重新解析全文而是把变化点附近的文本段提取出来重新渲染成局部 DOM 片段替换掉旧的节点。为了保证代码块、表格这类容器在更新时不闪烁我用requestAnimationFrame把 DOM 更新延迟到下一帧统一执行大幅减少了闪烁和跳动。3.2 语法高亮与格式化Markdown 解析器的选型与调优解析器的选择几乎决定了编辑器的能力上限。我调研过 remark、marked、markdown-it、markdown-wasm 等主流方案最终选了markdown-it作为核心解析引擎。原因很简单它的插件生态成熟GFM、脚注、目录、自定义容器这些都能用现成插件扩展同时它支持同步渲染性能足够我在每次键入时快速产出预览。为了让解析结果在源码和预览中保持一致我关闭了typographer自动替换标点这类默认改写行为避免渲染结果不经用户同意就偷偷变化。真正的难点不是选解析器而是把解析器嵌进编辑器后如何保证输入过程中的体验。用户输入时Markdown 语法是半成品的比如刚刚输入**强调标记时预览区应该显示什么我选择使用“容忍模式”渲染解析器对未闭合标记不做错误处理而是用 CSS 高亮提示未闭合状态例如把未闭合的强调标记显示为红色波浪线同时在预览区暂时按纯文本对待。这样用户既能看出语法有问题又不会看到奇怪的乱码。代码高亮我在 markdown-it 的 fence 规则里做了定制用highlight.js识别语言但只对超过 3 行的代码块启用完整高亮短代码块直接渲染成深色背景加浅色文字。这个微小的决策极大减少了小代码片段反复执行高亮造成的 CPU 浪费实测在连续输入短代码的场景下输入延迟降低了接近 40%。此外我还在源码区用 CodeMirror 6 的 StreamLanguage 能力对 Markdown 做了轻量高亮让源码模式和预览模式的视觉风格保持一致。3.3 图片粘贴与文件管理解决写作最后一公里的痛点写作时贴图片是一个巨大的痛点。传统 Markdown 编辑器粘贴图片往往变成一堆 Base64 字符串塞入源码文件立刻膨胀或者直接丢弃剪贴板里的图片让人不得不手动保存再引用路径。我做了三层处理。第一层监听paste事件如果剪贴板里有图片文件就先把图片转成 Blob再写入当前文档所在目录下的assets子目录文件名按日期时间戳加随机数生成避免重名。第二层写入成功后自动在光标位置插入标准 Markdown 图片语法并把相对路径作为链接。第三层如果图片已经存在于磁盘上我就建立一个小型图片索引在渲染时通过自定义渲染规则把宽高属性自动补上方便排版控制。文件管理上我一开始想过自己做格式化的数据库索引但后来放弃了因为对用户来说最有价值的是“目录即仓库”。编辑器启动时会读取用户指定的项目目录把目录树展示在侧边栏每个文件夹对应一个主题文件夹里的 Markdown 文件会被扫描出标题和标签生成可搜索的全文索引。附件、图片、导出的 PDF 文件都会按规则收纳进assets或exports文件夹避免散落得到处都是。这些设计并不花哨却能让使用者建立一个“我的所有文字都安放在磁盘上某处”的安全感。3.4 导出能力从单文件到稿件交付的完整链路Markdown 编辑器如果没有好的导出能力就像刀没有柄。很多文档最终要交付给非技术同事、编辑或者客户PDF、Word、HTML 是刚需。我的导出链路是这样设计的先用 markdown-it 渲染出完整的 HTML再套上我那套排版 CSS形成“预览即导出”的视觉一致性。导出 PDF 时我调用 Electron 的webContents.printToPDF接口利用 Chromium 的打印引擎直接生成 PDF页边距、页眉页脚、背景图形都能精确定制。导出 Word 的场景我做了两条路。第一条是把 HTML 用html-docx-js转成 docx第二条是直接输出带.md附件的 HTML 包给需要二次编辑的人。实践下来html-docx-js生成的 docx 在复杂表格、脚注上偶尔会丢样式所以我更推荐用户用 HTML 包方案那个格式保真度最高。导出这套流程里最值得反复检查的是代码块换行和长 URL 折行。Markdown 里的长链接如果直接在 PDF 里硬折会破坏 URL 可读性。我用了word-break: break-all配合overflow-wrap: anywhere并给链接加了一层内边距阴影让折行后的链接仍然看得出是一个整体。这个细节看起来很小但交付文档时整洁的排版是专业度的一部分。4. 实操过程从原型到第一个可用版本的开发记录4.1 技术选型与项目结构项目从立项到第一个可自举版本即编辑器能用自己写出的 Markdown 编辑自己的项目文档大概花了三周业余时间。技术选型方面桌面壳子用 Electron编辑器核心用 CodeMirror 6解析和渲染用 markdown-it界面 UI 我用原生 DOM 操作加简单状态管理没有引入沉重的视图框架。之所以不引入 React/Vue是因为编辑器场景大量依赖底层 DOM 节点的精细控制和键盘事件处理直接操作 DOM 反而更可控状态管理也确实复杂但用一个事件总线加几个 store 就够了。项目结构按职责拆成五块core负责文档解析、状态管理、事件派发editor封装 CodeMirror 6 的初始化、扩展、快捷键renderer负责预览区渲染、滚动同步、主题样式file管理文件系统、目录扫描、自动保存export负责 HTML/PDF/docx 导出链。分开之后任何一个模块要改不会牵动整座屎山。特别是渲染层和编辑层的解耦让我在调试滚动同步时省了非常多时间。4.2 从原型到能用的关键节点命令面板、快捷键与自举式开发最早期的原型其实很简单只用 CodeMirror 和 markdown-it 搭了一个能打字、能渲染的最小页面。但“能用”和“好用”之间隔着一大堆交互。第一个里程碑是命令面板。我实现了一个通用的命令注册系统每个功能声明id、title、keywords、handler面板根据输入模糊匹配。为了让匹配手感接近代码编辑器我用了前缀匹配加子串匹配的混合算法再按关键词权重排序。快捷键系统也接入同一套注册机制避免命令重复维护。这个阶段我坚持“自举开发”所有项目文档、需求记录、BUG 列表全部用这个编辑器来写。好处非常直接——每写一句话都是在真实压力下测试解析器。写目录时发现 TOC 插件在二级标题嵌套三级标题时缩进丢了一级写表格时发现 GFM 表格的转义竖线处理有 bug写数学公式时发现行内公式和中文之间如果没有空格会被识别成普通文本。这些问题要不是真刀真枪用起来靠写测试用例根本覆盖不完。4.3 性能优化输入不卡顿的调优日志从原型到第一个可用版本性能优化是我投入时间最多的部分。初始版本有个很大的缺陷每输入一个字符markdown-it 就会同步解析全文预览区整段重新渲染。文档三千字时还好一旦超过五千字键盘输入到预览更新的延迟会飙到几百毫秒。我做的第一轮优化是上面提到的局部渲染只渲染变化段落。实现策略是维护一个行首偏移表监听 CodeMirror 的update事件拿到变更范围和变更文本再定位到对应 DOM 节点做替换。第二轮优化是引入“空闲渲染”。预览区的刷新不完全跟随输入事件而是把任务放进一个调度队列如果在 50ms 内又有新的输入就取消上一次未执行的任务只执行最新的一次。这样极大减少了无效渲染。第三轮优化是降低代码高亮的运算成本。前面提到的短代码块不高亮和本地缓存哈希优化都是在排查输入卡顿时做的。优化完成后我在一份一万五千字的文档里连续输入奇怪嵌套语法预览刷新延迟稳定在 16ms 内体感上已经完全无感。5. 常见问题与排查技巧实录5.1 大文件卡顿问题从死锁到渐进式渲染开发过程中遇见的第一个大问题是打开大文件时界面卡死。起初我以为是解析全文导致的正常现象后来发现卡死发生在 Node 层的文件读取和渲染层之间如果用户打开一个 5MB 的纯文本文件fs.readFile会在主进程占住大量内存再把内容传递给渲染进程时序列化大字符串本身就耗时严重。更隐蔽的是我用自研的全文索引模块扫描整个目录时如果目录下恰好有非 Markdown 大文件索引线程会阻塞事件循环导致编辑器全局无响应。排查的时候我先用性能分析工具看到了一个明显的“时间块”JSON.stringify和fs.readdirSync占用了整体运行时间的大半。解决思路分两条大文件读取改为流式分段读取先渲染第一屏再在requestIdleCallback里逐步补全后半部分全文索引改用子进程 worker 处理并将扫描结果用异步队列写回渲染进程。这样处理后哪怕是 10MB 的日志文件打开时也只是首屏稍慢界面不会冻结。5.2 光标位置错乱与滚动漂移的根源双栏模式下光标位置错乱是一个很经典的问题。我最初在源码区监听光标位置时直接用行号去映射预览区但一旦源码中出现代码块、表格这样占据多行的渲染元素行号并不是线性对应的。后来我改用“文档偏移量”映射也就是把源码里的字符位置作为真值通过 markdown-it 给定的 token 位置信息计算每个 token 在渲染 HTML 中的锚点。这个方案比行号映射稳定得多但 token 的map属性只覆盖块级元素像段落里的行内元素还需要额外处理于是我在 token 流中特别注入了零宽字符作为标记再在渲染时把标记转换成带>