ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

从零打造一款高性能Markdown编辑器:技术选型与实战踩坑记录

从零打造一款高性能Markdown编辑器:技术选型与实战踩坑记录 我写了六年代码其中有五年半的时间几乎每天都在和 Markdown 打交道。写博客、记技术笔记、维护项目文档、甚至给团队写周报我都用 Markdown。这期间我几乎把市面上叫得上名字的 Markdown 编辑器都试了个遍纯文本编辑器、双栏预览式、所见即所得式、套壳网页版……真是没有一款能让我完全满意。有的长得好看但功能太弱有的性能彪悍但界面还停留在十年前有的什么都好却偏偏在中文输入上卡顿。后来我实在忍不了了决定自己动手写一个。这个项目从设计到第一版可用断断续续花了四个月时间。如今它已经能稳定承担我日常所有的写作工作在 GitHub 上也有了不少 Star。这篇文章我想把整个项目从需求分析、技术选型到核心功能实现、踩坑排查的完整过程讲透重点是分享一些在正式文档里不会写的东西——那些只有真正写过一个编辑器才会懂的细节。1. 为什么放着现成的编辑器不用偏要自己造轮子动手之前我先把自己过去几年的使用痛点和真实需求全列了出来。如果你没有类似经历可能很难理解为什么有人会放着 Typora、VS Code 这些成熟工具不用非要自己折腾一个。但当你每天跟 Markdown 打交道超过五个小时你会发现这些痛点是真的能磨掉人的耐性。1.1 重度用户的日常我已经被现有工具折磨了五年先说 Typora。它的所见即所得确实很惊艳直到现在很多人在乎的“沉浸式写作”体验它都做得很好。但它的问题也很明显它没有一个真正意义上的“插件生态”当你需要自定义一套写作工作流——比如自动把笔记归类、批量处理文档头信息、或者对接自己的图床服务——你几乎插不进手。Typora 的源码模式又比较弱一旦文档渲染出现异常排查起来特别痛苦。然后是 VS Code。VS Code 当然很强大它也确实是很多技术写作者的主力工具。但它的 Markdown 预览本质上是个“侧边栏浏览器”你编辑的时候依然是“左边源码、右边渲染”的两栏逻辑两边经常不能同步滚动。更让我崩溃的是我用 VS Code 打开一个一万多行的 Markdown 文件时输入响应有时候会掉到几百毫秒——大概率是插件冲突和语法高亮插件在大文档上的性能问题。对普通用户来说几百毫秒没什么感觉对我这种几乎每分钟都在码字的人这种卡顿就像吃饭的时候不停有苍蝇落在筷子上。还有一类纯 Web 编辑器比如语雀、Notion。它们的美观度和功能都做得很好但这类在线文档的核心是“云端协作”它们把 Markdown 当作一种输入方式而不是真正的文件格式。我想要的是一个本地文件放在我的磁盘上打开秒开离线可用不依赖任何服务端我想全文搜索时是在整个目录里搜我想写一篇文章写到一半关掉电脑下次打开还是原来的状态而不是从云端同步拉一个可能冲突的新版本。把这些需求列完我发现说白了我要的很简单一个长得好看、打开快、输入不卡、支持大文档、能版本回溯、还兼容标准 Markdown 语法的编辑器。但市面上偏偏没有完美满足这些条件的。于是我决定自己造一个。1.2 需求清单我把“好看”和“彪悍”分别拆成了什么在正式动手之前我把所有需求明确拆成了“好看”和“彪悍”两个维度每个维度下又细分出具体指标好看默认主题要符合现代审美不是那种花哨的彩色风格而是像 iA Writer 那样干净、有阅读感的排版支持多主题切换尤其是暗色模式且暗色不是简单反色而是重新调整过对比度和背景层次编辑和预览要能真正同步滚动比例定位要准确不是粗略对齐中英文混排时要自动处理行间距、字间距标点不被裁到行首行尾彪悍打开 5MB 以上的 Markdown 文件不白屏、不卡顿输入延迟稳定在 50ms 以内自动保存是增量式的每 500ms 保存一次不阻塞用户输入本地保留完整的历史版本可以随时回溯到任意时间点的内容快捷键完全可自定义支持类似 Vim 的键盘操作模式方便双手不离开键盘彻底离线可用不向任何服务器发送文档内容这份需求清单后来成了我整个开发过程的“宪法”。每当我面对技术选型纠结的时候就回到这份清单问自己这个方案能不能满足这些指标不能满足就换绝不妥协。2. 内核设计从渲染引擎到编辑器框架的取舍有了需求清单下一步就是技术选型。这一节是整个项目最关键的部分开发时我在这里纠结了最久。因为 Markdown 编辑器的技术栈选择直接决定了后面所有功能的天花板。2.1 编辑器核心框架为什么选中 CodeMirror 6 而不是 Monaco 或 ProseMirror做编辑器第一大选择是“底层编辑器引擎”。市面上的成熟方案主要有三个MonacoVS Code 用的编辑器、CodeMirror 6、ProseMirror。我逐一分析过Monaco 功能强大有完整的 IDE 级能力但它的问题也很明确体积巨大光是编辑器核心的包就有几 MB而且它的架构是为“代码编辑”服务的。Markdown 写作场景需要的很多能力比如“软换行但保留段落结构”“渲染光标所在段落的大纲信息”Monaco 并没有很好的原生支持定制起来会很费劲。ProseMirror 是真正的 WYSIWYG所见即所得方案Typora 一类的产品就是这类技术路线。它的思想是直接在文档模型上做富文本编辑。好处是渲染几乎实时排版呈现就是最终结果。但坏处是我最担心的Markdown 源码和渲染文档之间的对应关系很难完全保真。你会发现用 ProseMirror 做的编辑器经常出现“把 Markdown 粘贴进去再复制出来格式变了”的问题。对我来说Markdown 的源码可读性、可移植性是不能放弃的底线所以我排除了 ProseMirror。最后剩下 CodeMirror 6。它是 CodeMirror 5 的完全重写版本模块化做得特别彻底核心包只有约 300KBgzip 后约 80KB扩展机制非常优雅。它在底层是一个增量文档树支持自定义语言解析器、自动补全、多视图拆分最关键的是它的渲染性能很好——代码编辑领域对性能的要求其实比 Markdown 写作更苛刻所以它能稳定支撑超长文档。而且它原生就支持从左到右、从右到左的排版中文输入法组合输入的处理也很成熟。最终我选定了 CodeMirror 6 作为编辑器核心。2.2 Markdown 解析层自研 AST 解析器而不是直接套用 markdown-it 的完整方案编辑器核心定了之后第二个关键选择是 Markdown 解析器。主流的做法是直接使用 markdown-it、remark 或者 marked 这类库。但做编辑器应用和做普通的“把 Markdown 转换成 HTML”不太一样我需要的不只是最终的 HTML 字符串我还需要精确的 AST抽象语法树信息。比如当前光标所在的文本在 AST 里属于哪个标题层级、哪个列表、哪段代码我需要根据这个信息才能实现文档大纲、丰富状态栏信息、以及“预览区定位到当前光标位置”等功能。如果我完全基于 markdown-it 的第二阶段插件去拿 AST也不是不行但会有性能损耗。在用户输入的每一毫秒里我都要重新解析整个文档然后把 AST 做增量对比这在大文档上会很吃力。所以最后我自研了一个轻量级的 Markdown AST 解析器核心思路是按块block拆分文档每个块独立解析解析结果缓存。当用户输入修改了某个块的内容解析器只重新解析该块以及可能受影响的相邻块其余全部走缓存。这个增量解析设计保证了 99% 的情况下键盘输入后 5ms 内就能拿到最新的 AST。一个关键决策是纯文本格式解析我不做严格到连空隙字节都卡死的 CommonMark 完整规范实现。我实现的是超集既支持标准 CommonMark 语法也支持 GFMGitHub 风格 Markdown的表格、任务列表、删除线、自动链接额外还增加了对 LaTeX 数学公式、Mermaid 流程图仅预览不编辑的支持。2.3 存储与架构为什么整个项目选择了浏览器本地优先方案第三个核心选择是应用形态。我最终做的是一个纯前端 Web 应用可以离线运行也支持安装为 PWAProgressive Web App。为什么选这个因为我想让自己在任何一台电脑上打开浏览器就能用不用装一堆 Electron 包也省去跨平台适配的所有麻烦。但纯浏览器应用有个“老问题”我没办法直接访问用户磁盘上的任意文件。为此我用的是 File System Access API——它允许浏览器在用户明确授权下对一个目录进行读写操作。用户只需要把存放 Markdown 文件的文件夹“授权”给编辑器编辑器就能像本地应用一样直接读写目录中的文件。对于不想用这个 API 的用户我同时还实现了一个“文档库”模式文档默认存储在浏览器的 IndexedDB 中适合纯笔记场景。数据流上整个应用分为四层文件访问层负责与文件系统或 IndexedDB 交互封装读、写、重命名、删除操作文档模型层维护当前打开文档的状态包括源码内容、AST 树、光标位置、历史版本记录编辑视图层CodeMirror 6 实例负责渲染源码、语法高亮、输入响应预览渲染层自研的渲染管线消费 AST 树产出 DOM 节点负责样式布局这四层之间严格单向依赖上层只能调用下层的接口下层不能反向依赖上层。这个架构让我在后续加功能时省了很多力气比如后来我加“导出 PDF”的功能时只需要在预览渲染层上再包一个打印视图完全不动编辑器核心。前端部分我选的生态是 TypeScript Vite React。React 只用来管理编辑器之外的 UI 壳子侧边栏、状态面板、设置页编辑器核心本身不用 React 组件。这是个重要的经验CodeMirror 6 这类高性能编辑器最好不要把每个状态更新都塞进 React 的渲染循环里否则频繁的重渲染会让性能大打折扣。3. “好看”绝不是换个皮肤那么肤浅排版引擎和阅读体验很多人觉得“好看”就是配色好看、加个圆角、做点毛玻璃效果其实真做一个编辑器就会发现真正的“好看”是阅读体验的极致优化。这一节我拆解几个我花时间最多、也最见功底的部分。3.1 编辑器与预览双区最核心的体验同步滚动算法我做的编辑器默认是“源码 实时预览”双栏模式。左边是你正在编辑的 Markdown 源码右边是渲染好的最终效果。但双栏模式的体验好坏完全取决于同步滚动做得好不好。很多编辑器的同步滚动只是“粗略单点对齐”——左边你滚动到第 N 行右边就滚动到第 M 个元素的顶部。这么做的直接后果是你刚编辑到段落中间右边预览滚动的目标却跑到了上一个标题的位置阅读体验非常割裂。我实现的是“基于可见区域比例的增量同步”算法。核心思想是左右两侧建立映射关系左侧的每个可视行都对应右侧 AST 节点的一个矩形区域。当用户滚动左侧时计算左侧视口中心线对应的 Markdown 行号通过 AST 映射找到右侧对应的 DOM 节点然后滚动右侧视口使该节点出现在视口的等比例位置。反方向滚动右侧时同步左侧也一样。这个算法初版跑起来性能也还行但在大文档里频繁滚动时偶尔会出现“跳动”——右侧内容一直在抖动。后来我追问到一个根因右侧 DOM 节点有个图片加载或 LaTeX 渲染的过程渲染完成后节点高度发生变化导致之前计算的映射关系失效。我最终的解决方案是给同步计算加了一个“几何回退校验”如果当前节点在上一帧的高度与当前帧相差超过 20 像素就先不滚等下一帧几何稳定后再继续。实测下来即使在图片密集的文档里同步滚动的准确率也稳定在 98% 以上。3.2 沉浸式写作的人性化细节打字机模式与焦点模式“好看”还有一个容易忽略的维度就是专注于写作本身。我做了两个出声入化的细节功能很多用户反馈说这个编辑器的“手感”就是被这两个功能救回来的。第一个是打字机模式Typewriter Mode。开启后当前光标所在行始终固定在屏幕纵向大约 70% 的位置而不是任其飘走。这样你长时间写作时视线不会跟着文字移动越来越低、越来越偏减少颈椎和眼球的疲劳。实现时需要注意一个坑直接用 scrollTo 把光标行滚到固定位置的话CodeMirror 6 的滚动是按像素计算的而光标位置是按行计算的。需要结合 CodeMirror 的coordsAtPos拿到光标像素坐标再计算偏移量。我用的是滚动动画加 16ms 节流保证在持续输入时不会抖动。第二个是焦点模式Focus Mode。开启后当前光标所在的段落正常亮度渲染其它段落整体调暗到 60% 透明度。注意力会自然集中在正在写的段落上减少大纲内容对视线的干扰。这个功能实现起来靠 CodeMirror 的 decoration API按行添加透明度样式更新频率不高性能无压力。3.3 主题系统不是套一层 CSS 变量那么简单多主题支持这件事我从一开始就极力避免“主题 改几个 CSS 变量”这种肤浅做法。因为预览区的阅读排版涉及非常多的细节正文字号、行高、字间距、段落间距、标题的字重层级、引用块的颜色、代码块的背景对比度、表格隔行变色、暗色模式下图片的亮度补偿……如果只是改几个颜色变量做出来的暗色模式往往要么对比度过高刺眼要么文字和背景拉不开层次。我自己设计了一套三层主题系统第一层是语义化颜色变量如 background、foreground、accent、code-background、blockquote-border 等约 40 个第二层是排版变量如 base-font-size、line-height、heading-scale、paragraph-spacing 等约 15 个第三层是组件级主题每个 Markdown 元素标题、列表、表格、引用、代码块可以单独覆盖默认样式我预置了四套主题日光白默认暖白背景模拟纸张质感、石墨黑真正的暗色不是简单反色、海盐灰中性灰调适合阳光直射环境、深林绿低刺激护眼绿。所有的主题都经过我自己连续几小时写作测试调整过无数轮对比度和行距参数。特别是暗色模式下的代码块高亮配色我最终还是手动调整了 Tokens 的色值没有直接套 GitHub 的暗色方案——因为 GitHub 的暗色是偏蓝黑色在长时间阅读场景下略冷、略累。3.4 中英文混排体验字间距、换行与标点挤压这是很多欧美开发者做的 Markdown 编辑器最容易翻车的地方。英文排版和中文排版完全是两套逻辑英文按空格分词中文按字分词中英文混排时中文和英文之间最好有微小的间距但中文和中文之间又不能有空格标点不能出现在行首避头尾规则英文单词如果不换行又会导致太长的空白间距两端对齐会很难看。我的排版引擎在渲染预览区时做了一个专门处理中英混排的“自动间距算法”在中文和英文字符、中文和数字、中文和半角符号如括号、引号之间自动插入 0.08em 的间距但这个间距是用 CSSword-spacing模拟的而不是真的往文本里塞空格。这样即保证了视觉上的清爽又不污染你原始 Markdown 文件的内容。换行断词也用到了 CSS 的line-break: strict和word-break: normal组合。标点避头尾由于 CSS 原生支持有限hanging-punctuation兼容性不够我在渲染管线里做了一个轻量级的文本清理步骤在生成每个段落的文本节点时如果段落开头第一个字符是标点就把它和前一个段落的最后一个字符调换位置保证标点永远避免出现在行首。严格来说这算不上完美的排版引擎但实际体验已经让我非常满意了。4. “彪悍”藏在看不见的地方性能与可靠性如果说好看是外表彪悍就是内功。一个 Markdown 编辑器如果只是好看但打开大文件卡成幻灯片那它基本没有可用性。我在性能优化上花的时间占了整个项目的一半以上。下面这些指标和优化手段是我花了无数个晚上反复压测和调优的结果。4.1 打开大文档的极限压测5MB 文件秒开我自己平时最长的一篇文档大约是 300KB约一万多行 markdown。但既然要做“彪悍”我决定把目标提上去——对标 5MB 以上的超大型 Markdown 文件。这类文件通常是不太合理的谁会用 Markdown 写 5MB但真遇到合并文档、导出的日志、或者把一本电子书转成 Markdown 时编辑器必须顶得住。首版在打开 5MB 文件时白屏时间高达 3 秒。我把性能消耗最大的两个点找出来后分别做了优化第一个瓶颈是语法高亮。CodeMirror 6 默认的 Markdown 解析器会用 stream-parser 对整个文档构建语法树5MB 文档的解析时间约 1.2 秒。我的优化手段是采用渐进式解析先只解析前 2000 行用于初始渲染其余行标记为“未解析”在用户往下滚动的时候再增量解析可视区域。配合 CodeMirror 6 自带的 viewport 机制最终实现了无论文档多大首屏渲染时间恒定在 200ms 以内。第二个瓶颈是 AST 构建。自研的增量解析器在这一点发挥了巨大作用首次打开 5MB 文档时AST 构建还是需要跑一遍全量耗时约 800ms。但它做完之后就缓存了后续用户编辑任何位置都只触发局部解析。实测在 5MB 文档里连续输入AST 更新平均耗时不超过 6ms完全感觉不到。压测数据5MB 文档首次打开到完整可交互约 1.1 秒后续任何编辑操作均无明显卡顿。一万行文档滚动帧率稳定在 55fps 以上不丢帧。测试项300KB 文档5MB 文档首次打开白屏时间150ms1.1s首次 AST 构建90ms820ms连续输入平均响应延迟15ms18ms全文搜索单关键词30ms420ms4.2 自动保存与版本回溯本地 Git 是怎么实现的保存是一个写作软件好好坏坏的分水岭。很多编辑器是“手动保存”或者“无限自动保存”前者丢数据后者则积攒了一堆没用的中间版本。我的方案是“节流自动保存 版本快照”用户输入时编辑器每 800ms 检查一次是否有修改有的话就把完整文件内容写入本地IndexedDB同时把这次修改作为一个版本快照存起来。但这里有个问题完整快照存太多会占空间。5MB 的文档如果每秒存一个快照不要说 IndexedDB什么数据库都吃不住。我借鉴了 Git 的“增量对象”思路每次保存时不是存全量内容而是只记录本次修改的行号变化和变更后的那一段文本每经过 30 分钟或用户手动触发“里程碑保存”时才存一次全量快照。读取历史版本时从最近的全量快照出发按时间顺序应用增量变更即可重建任意时刻的文档状态。这个方案把 5MB 文档的日常版本占用控制在了一次全量 若干小增量的量级空间占用比全量快照方案节省了 80% 以上。版本回溯面板就是你最喜欢的“历史时间轴”可以直观看到每个时间点的文档长度、修改行数、附带关键词摘要点击任意时间点即可在预览区展示该版本的内容可以一键“恢复此版本”或“另存为新文件”。4.3 全套快捷键与 Vim 模式双手不离键盘的极致我本人也是个 Vim 老手写 Markdown 的时候经常想用 Vim 的按键来做精确操作。所以我在编辑器里内置了完整的 Vim 模式不是简单的“hjkl 移动”那种皮毛实现而是用 CodeMirror 6 的 keymap 机制接入了一个简版 Vim 状态机支持普通/插入/可视/操作符等待模式、文本对象ciw、di(、yap等、宏录制、跳转到指定字符f/F/t/T、撤销树回跳u/C-r等。虽然做不到完整 Vim 的全部功能但日常 80% 的编辑操作都没问题。Vim 模式之外我还设计了一套可完全自定义的快捷键系统。默认键位会尽量与 Typora、VS Code 保持一致CtrlB加粗、CtrlK插入链接、CtrlShiftK插入代码块但任何键位都可以在设置界面里重新绑定。设置面板会侦听你的按键组合并实时显示冲突检测防止你把同一个快捷键绑定给两个互相冲突的操作。我遇到过一个很离谱的反馈有个用户把CtrlS设置成“插入表格”用了两周后才发现自己再也没保存过文件。虽然自动保存兜底了但这个教训让我加了一个“系统级快捷键保护”功能与保存、另存为、打印、全屏相关的系统级快捷键只能被增强不能被覆写。4.4 可扩展的插件机制API 设计的思路一个编辑器的“彪悍”还体现在它的插件扩展能力。我设计了一套轻量级插件 API核心是生命周期钩子onBeforeParse在文档解析之前对原始文本做转换比如自定义短代码转换onAfterParse拿到 AST 后可以修改或补充节点比如给某个特定类型节点加自定义渲染样式onExport导出前对素材做处理比如把本地图片转成 base64 嵌入onKeymap向编辑器注册自定义快捷键组合在做这个 API 设计时我一直在提醒自己别把 API 设计得太复杂。插件用户里很多是写作者他们可能不是专业程序员。所以我把常用的几个场景封装成了“配置文件 函数钩子”的简洁结构。比如一个最简单的“把::note转换成高亮段落”的插件只需要十几行配置就能搞定。这个设计思路让后来的插件社区涌现了一批有意思的创作比如“番茄钟写作流”、“每天随机切换主题”、“自动同步到某个代码托管平台”等都让我挺惊喜的。5. 开发中踩过的几个深坑排查链路全记录这个项目开发过程远不是一帆风顺的有好几个问题差一点就让我放弃了。下面这几个坑每个都花了少则半天、多则两三天的时间排查。记录下完整的排查链路一是给自己留个纪念二是如果你也在做类似的项目可以少走很多冤枉路。5.1 中文输入法正在输入的拼音字符会突然消失这是我在做编辑器第一周遇到的最打击信心的 Bug用中文输入法打字时正在输入的拼音字母经常在切换状态时消失或者拼音还挂在屏幕上但候选词失焦了。我一开始以为是渲染层的问题花了一整天在控制台里打日志发现 CodeMirror 6 的文档模型在输入法组合状态composition下的行为非常特殊。当输入法组词时字符串插入是以一个“非提交状态”进行的CodeMirror 6 需要配合特定的配置项才能正确处理。排查后找到了关键原因CodeMirror 6 在 6.x 版本中对 composition 事件的处理默认是“每次 commit 时把整个 composition 中的文本替换成最终候选词”。但我的编辑器在预览区做了实时解析解析管线的异步渲染回调偶尔会在 composition 的 commit 之前触发一次 DOM 更新这个更新会打断 CodeMirror 6 内部的 composition 追踪导致最终候选词没有正确提交。解决方案也很干脆解析渲染管线里加了一个isComposing状态标记在 composition 开始到结束的整个过程中暂停花哨的 DOM 更新操作等 composition 事件compositionend触发后再执行一次“立即解析渲染”。这个约束以后成了所有涉及文本更新的插件的默认规范——写文档说“在输入法组合期间不要改动文档内容”。加上这个保护后中英文输入切换再也没有出现字符串丢失的问题。5.2 大文档中出现的“滚动抖动”问题上面在同步滚动算法里我提到了“几何回退校验”其实那个方案是经历了完整的问题排查链路才确定的。一开始我注意到在 5000 行以上的文档中连续滚动时左右区域的同步滚动偶尔会像“拉锯战”一样来回抖动尤其是在文档中含有很多标题和行的场景中。我初期的假设是“滚动事件节流不够”于是把滚动监听的节流时间从 16ms 调高到 50ms。结果抖动减少了但是同步的跟手性大大降低——左侧滚了 100 像素右侧要迟 50ms 才跟上体验变得很钝。这个假设被推翻了原因方向是错的。然后我换了检查方向是不是 CodeMirror 6 的滚动行为和普通 DOM 元素滚动行为不完全一致我打印了两侧视口顶部元素的几何信息发现一次滚动过程中右侧视口在我设置目标位置后发生了一次二次位移而这个二次位移的来源是渲染层。追踪后发现右侧预览渲染时图片默认是懒加载lazy loading滚动到某个包含未加载图片的区域时图片加载完成后 DOM 高度会变化进而影响右侧文档总高度导致之前设置的目标位置已经不在原来的相对位置了。这才是一个完整的成因链路懒加载图片 高度变化 映射偏移 滚动抖动。最终“几何回退校验”方案只对原因链做了精确修复在设置滚动目标之前对目标节点的几何位置做一次校验发现高度变化超出阈值就等一帧再滚。这次排查给我最大的启发是做 UI 性能优化时问题表象往往不是问题本质把滚动事件节流调来调去只会在错误的维度上原地打转。5.3 IndexedDB 并发写入导致的版本丢失自动保存版本系统上线后我收到少量用户反馈文档版本历史里偶尔会出现“空快照”——某个时间点的版本内容是空的。这个问题在小文档上偶尔能遇到但在大文档上出现的概率更高。我一开始怀疑是版本快照的序列化过程出了问题于是在本地复现盯着 IndexedDB 的事务状态一直打日志。日志中确实看到了一个诡异的模式某次自动保存的“写入完成”回调还没有触发紧接着用户又做了一次手动保存两个保存请求竟然在同一个 IndexedDB 事务里被阻塞了最终版本列表里前一条的写入被后一条事务给遮蔽了。这就是 IndexedDB 的同事务覆盖问题。我的版本快照写入是异步的但业务上没有保证严格的串行化。修复方案是加了一个“版本写入队列”所有的版本快照写入操作都进入一个 FIFO 队列一次只能有一个写事务在运行其余排队等待且每个写事务都带一个自增的序列号。写入完成后读取方只会取最大的序列号作为最新版本基线。这个队列在极端情况下有轻微的性能开销但换来了稳定性的显著提升。5.4 自动保存与用户正在输入的内容打架还有一个很有意思的 Bug自动保存触发时如果用户刚好在输入保存操作会把当前内容读出来但读取的瞬间文档内容正在更新中导致保存进去的内容和实际的编辑状态不一致有时甚至会多出一两个字符或者是丢失最后几个字符。这个问题的根因在于CodeMirror 6 的文档更新dispatch是同步的但自动保存函数是通过定时器触发的定时器回调执行的时机可能会落在两次 dispatch 之间。我在自动保存逻辑里加了“写入前锁”当文档正在 dispatch 更新时保存请求会等当前更新完成后再进行快照。具体做法是监听 CodeMirror 6 的updateListener记录一个“文档版本号”每次 dispatch 后版本号自增。自动保存函数在执行前读取当前版本号执行 IndexedDB 写入时把这个版本号作为一个参数传进去写入完成后再次比较版本号如果不一致说明写入过程中又有更新发生那就再触发一次延迟保存。这样保证数据库里存的内容总是和文档最终状态一致不会丢字节。6. 用数据说话第一版发布后的真实表现与迭代第一版发布后我在自己的博客上公布了一个“alpha 测试邀请”很快有几百个用户参与了测试。让我特别开心的是大量用户端反馈集中在我忽略的一些使用场景上。比如“你们的离线支持太好了我在高铁上把整本开源书的 Markdown 手册都打开了一点不卡”“暗色主题做得很舒服连续看两小时文档眼睛不累”“我拿它当每次写作的启动工具因为启动只需要 0.3 秒比打开一个 Electron 应用快太多了”。6.1 真实用户数据比我自己预想的还要好我统计了前三个月匿名化的使用遥测数据只记录功能使用次数不记录任何文档内容平均单次会话时长约 45 分钟最高会话时长接近 8 小时约 60% 的用户开启了 Vim 模式这让我有点意外原本以为只是小众需求自动保存功能在崩溃恢复场景下恢复成功率 100%通过保留的版本快照恢复最大的单个 Markdown 文档在真实使用中达到 18MB用户反馈“打开约 3 秒之后输入很流畅”还有一个让我挺欣慰的数据版本回溯功能的使用频率比预期高很多有用户专门用历史版本来反悔“误删了整段内容”而不是靠编辑器自身的 Undo。因为 Undo 只保留有限步数历史上滚得深一点就找不回来了。版本回溯因为有全量快照和增量存储几乎是无限的。6.2 性能调优的复盘哪些优化性价比最高回头看所有性能优化工作性价比最高的是“增量解析”和“懒加载渲染”这两项。它们直接决定了编辑器能否扛住超大文档。有趣的是一些做得最多的工作——微调主题色值、打磨同步滚动算法——虽然性能指标上没有体现但它们决定了用户打开编辑器第一眼的“感觉”。一个编辑器的“好看”和“彪悍”从来不是两个独立维度它们互相成就。优化项投入精力效果回报说明增量 AST 解析高极高从根源上消除了大文档输入卡顿语法高亮渐进式渲染中高首屏速度提升数倍感知最直接同步滚动几何校验中中解决的是体验细节拉升整体质感版本存储增量快照中高让空间占用大幅下降功能得以保留输入法组合保护低极高若不解决中文用户根本无法使用6.3 后续迭代路线我理想中的 Markdown 编辑器还差什么这个项目做到现在核心功能已经稳定但离我理想中的“完美编辑器”还有不少路要补。我自己规划了三个迭代方向第一是更智能的文档结构理解比如自动识别文档中的“决策记录”、“待办清单”、“项目里程碑”等语义化结构并生成可交互的侧边栏第二是嵌入脚本执行能力比如在 markdown 里写小段 JavaScript、Python 代码片段直接运行并把结果渲染成输出块这会让它变成真正的“可执行文档”第三是多方同步插件化把不同同步盘的对接做成官方插件而不是绑定特定的服务。这些想法我会按自己的时间和精力来排优先级。毕竟这项目的初衷是服务我自己的写作需求我不能让它变成一个被维护任务绑架的重型工程。保持“小而美”和“快而强”比堆砌功能更重要。最后关于编辑器的几条个人心得如果看到这里你也想动手做一款自己的编辑工具或者正在用类似的思路开发其他写作类软件我想把几条我踩坑之后最深的体会分享给你第一永远优先考虑“输入环节”的稳定和低延迟。编辑器的一切功能和界面都应该建立在绝不干扰用户输入这个绝对底线之上。输入法组合保护、保存锁、增量更新核心都是为了这个底线服务的。第二“好看”这件事要站在排版和阅读的维度去打磨不要陷在 UI 皮肤里。一个编辑器好不好看用户第一次打开看的是配色和布局但真正用起来之后他们感受到的是行距是否松弛、标题和正文的视觉层级是否清晰、代码和表格是否容易扫读。这些细节才是长期写作中“耐看”的关键。第三做工具类项目时不要贪功能。每加一个功能就多一条需要长期维护的路径。我每设计一个新功能前都会问自己这个功能我自己每周会用超过一次吗如果不是经常需要就坚决不做。产品可以被想象得很大但务实的工具往往是做减法做出来的。现在这个编辑器已经成为我所有写作工作的主力工具。它跑在我家里和办公室的电脑上也跑在我随身平板的浏览器里所有文档放在本地配合版本回溯我比以往任何时候都更有记录的安全感。写这篇文章的目的不是劝所有人都要去折腾一个自己的工具而是想告诉你如果你在某类工具的长期使用中积累了大量真实痛点并且这些痛点反复出现在你的工作流里那么自己动手解决它不一定是浪费时间它可能会成为你这段时间最有成就感的项目。
返回列表