自研Markdown编辑器:从需求到实现的技术全解析
2026/9/15 3:04:46 网站建设 项目流程

作为 Markdown 重度用户,我每天泡在编辑器里的时间比睡觉还长。这些年试过 Typora、VS Code、Obsidian,各有各的好,也各有各的让人抓狂。最要命的是,没有一个能同时满足我对“好看”和“彪悍”的执念——要么界面美滋滋但功能软绵绵,要么强得离谱却丑得伤心。折腾到最后,我索性自己动手写了一个 Markdown 编辑器。这篇文章聊聊这个项目的来龙去脉、技术选型、核心功能和踩坑实录,给同样动了自研念头或者正在纠结选型的朋友一点参考。

先说结论:这个编辑器我现在每天都在用,用来写技术文档、博客草稿、读书笔记,甚至周报。整个过程从零开始,前后迭代了快半年。我会把从需求梳理到架构设计,再到几个最棘手问题(长文档性能、中文输入法、图片路径、表格编辑)的排查思路都摊开讲,不藏着掖着。

1. 为什么一个 Markdown 重度用户会赌气写自己的编辑器

1.1 我的一天,几乎全泡在 Markdown 里

工作日的典型状态是这样:早上打开电脑,先写当天的 TODO,Markdown;开会记笔记,Markdown;写接口文档,Markdown;下午整理调研资料,Markdown;晚上写公众号草稿,还是 Markdown。算下来一天至少四五个小时是在 Markdown 文档里度过的。

这种使用频率下,编辑器的一点小毛病都会被无限放大。比如某个渲染效果差几个像素、某个操作要多点两下鼠标、某个大文档滚动起来掉帧,都会变成每天都要忍受的刺。很多人觉得编辑器能用就行,但重度用户不一样,我们是真的会因为一个光标定位的小问题就烦躁一整天。

而且 Markdown 的生态有个很有意思的现象:语法极其简单,但每个人用到的子集很不一样。程序员爱写代码块和表格,写作的人爱用引用和加粗,学生党离不开数学公式。大众编辑器为了照顾所有人,往往做成“什么都有但什么都不精”,这恰恰是我受不了的根源。

1.2 市面编辑器各有各的刺,列个清单

我把自己真实用过的几款编辑器做了一次复盘,不吹不黑,把痛点列在下面:

编辑器优点让我崩溃的点
Typora界面干净,实时预览体验好闭源且收费,自定义能力有限,大文件偶尔卡
VS Code + 插件功能强大,插件生态丰富本质上还是代码编辑器,写作体验和排版质感差一些
Obsidian双链笔记逻辑好,插件多太重,启动慢,不写笔记光写文档时有点杀鸡用牛刀
Notion页面漂亮,协同强不是纯 Markdown,导入导出经常走样
各种在线编辑器免安装要登录,要联网,隐私上不踏实

你发现没有,这些产品的痛点不是单独存在的,而是“好看”和“彪悍”互相打架。Typora 算是最接近我理想的,但它不是开源软件,我想加一个自定义渲染规则、想改某个主题细节,都无从下手。Obsidian 功能确实猛,但那个启动速度和插件加载的体感,始终让我觉得自己不是在写作,是在运行一个大型软件。

1.3 一个需求清单:到底什么才是“好用”

在动手写代码之前,我花了两周时间列需求清单。没错,不是先写代码,是先在纸上想清楚自己要什么。我给自己设了一个标准:如果这个功能不能让我的写作体验产生“可感知的提升”,就不做;如果三个功能之间有重叠,只保留最核心的那个。

最终清单被压缩成下面几条:

  • 外观足够好看,排版接近印刷品质感,长时间看屏幕不累
  • 实时渲染,也就是“所见即所得”,但也要能一键切到纯 Markdown 源码模式
  • 支持数学公式、代码高亮、目录大纲、全文搜索这些高频硬需求
  • 图片粘贴要自动落盘成文件,绝对不能偷偷变成 base64 塞进文档里
  • 单个文档几十 KB 甚至几百 KB 时,编辑和滚动都不能有明显卡顿
  • 自动保存要聪明:不闪存、不丢数据、不把磁盘写爆
  • 不搞私有格式,文章就是一个普通的.md文件,我用 Git 管理也行,用网盘同步也行

这份清单后来成了整个项目的“宪法”,每次纠结要不要加功能,就拿出来对照一下。事实证明这个动作省掉了大量不必要的返工。

2. “好看”不是玄学:界面、主题与排版工程

2.1 设计目标:让人忘记界面存在

我很喜欢一句话:好的工具设计,是让工具消失。写作的时候,你的注意力应该全部在文字上,而不是在那个界面边框、那个按钮、那条滚动条上。所以我给这个编辑器的视觉设计定了一个方向——低存在感。

所谓低存在感,落实到界面就是:默认主题不适合太花哨,颜色对比度要克制,字体和间距要以阅读舒适为最高优先级。很多编辑器喜欢在侧边栏、状态栏、按钮上加各种渐变和阴影,这些在我眼里全是噪音。

哪怕是功能面板,我要求它们默认收起来。打开软件见到的是一个完整的书写页面,不是一堆工具按钮。需要目录大纲就按一下快捷键呼出,用完自动隐藏。这种“用完即走”的交互看似简单,实际上是从 Obsidian 那种“什么都在桌上摆着”的模式里学到的反面教训。

2.2 从设计令牌到主题系统

主题系统是“好看”的骨架。我做的第一件事不是直接调 CSS,而是把所有可变的视觉参数抽象成设计令牌(Design Token)。简单说,就是定义一堆 CSS 变量,界面里所有颜色、字体、间距、圆角都引用这些变量,不出现任何硬编码值。

:root { /* 背景层次 */ --bg-primary: #fefefe; --bg-secondary: #fafafa; --bg-tertiary: #f0f0f0; /* 文字层次 */ --text-primary: #1f2328; --text-secondary: #57606a; --text-tertiary: #8c959f; /* 强调色 */ --accent: #0969da; --accent-soft: rgba(9, 105, 218, 0.12); /* 排版 */ --font-serif: "Source Han Serif SC", "Noto Serif CJK SC", serif; --font-sans: "Source Han Sans SC", "Noto Sans CJK SC", sans-serif; --font-mono: "JetBrains Mono", "Fira Code", monospace; --line-height: 1.75; --content-width: 720px; }

暗色主题不是简单把背景变黑、文字变白,而是重新设计一整套对比度关系。亮色主题里最深的背景色是#fefefe,暗色主题里最浅的文字色就要换成#e6edf3,强调色也要从深蓝变成更亮的蓝。只有把握好这三四个层次的配比,暗色主题才不会有“发光”感,盯久了才不会累。

主题切换我直接用了监听系统偏好和手动切换两套逻辑,系统偏好变化时自动响应。这个在 CSS 里就是一行prefers-color-scheme,配合我的设计令牌,几乎不需要写额外的切换代码。

2.3 排版实验:一屏文字如何不累眼

“好看”的另一个核心是排版。Markdown 编辑器本质上是把结构化的文本渲染成带样式的页面,排版质量直接决定观感。我花了很多时间在几个细节上面。

正文区域宽度限制在 720px 左右。这不是拍脑袋定的,而是参考了大量阅读类产品的经验。太宽了,行太长,眼睛扫到下一行容易混;太窄了,分段太频繁,翻页速度过快。720px 适合中文阅读,默认字号 16px,行高 1.75,字间距加了一点点,段落间距用 margin 控制而不是空行。

标题的层级感靠“减法”:H1 用字号加粗加大,H2 字号略小但加一个底部边框,H3 之后只靠字号区分,不再增加装饰。引用块用左边框加柔和底色,不搞大色块;代码块用等宽字体加圆角和浅灰底;行内代码用强调色背景加圆角,但不用加粗。

为了让排版在不同平台上保持一致,我还在字体回退栈上做了大量工作。Windows 上优先用微软雅黑,macOS 上优先用苹方,Linux 上回退到思源字体。如果你跨平台用过同一个编辑器就会懂,Windows 和 macOS 渲染同一段中文,行高差异能直接导致滚动错位和光标跳动。后面有一章我会细讲这个坑。

3. “彪悍”的核心:自研实时排版与渲染管线

3.1 文档的抽象语法树与双向映射

外观只是皮,真正让编辑器“彪悍”的是底层那套排版和渲染机制。我没打算真的从零写一个排版引擎,而是站在巨人的肩膀上:用成熟的 Markdown 解析库把文本解析成抽象语法树(AST),然后自己写渲染器把 AST 渲染成界面。

整个过程分三层:

Markdown 源文本 ↓ 解析器(parser) Abstract Syntax Tree(AST) ↓ 渲染器(renderer) DOM 节点(所见即所得视图)

为什么要中间加一层 AST?因为这样我就能做到“双向映射”:用户在界面上改一个字,我能知道这个字对应的是 AST 里哪个节点的哪段文本;反过来,用户改源码,我能立刻知道要重新渲染哪一块区域。这个能力是增量更新和大文档性能优化的基础。

解析器我选了 remark 系列,生态成熟,AST 规范,插件也多。渲染器我自己写,因为只有自己写才能完全控制输出样式。比如我要给标题加锚点、给代码块加复制按钮、给链接加外部跳转图标,这些统统可以在渲染器里注入。

3.2 数学公式、代码高亮与表格编辑

数学公式是很多编辑器翻车的地方。我的方案是 KaTeX,选它不是因为它渲染效果最精致,而是因为它的速度和体积最优。LaTeX 公式在界面上必须先经过一遍 KaTeX 渲染成 HTML,再嵌入到文档流里。实时输入场景下,用户每敲一个字符,公式都要重新渲染一次,如果这一步慢,整体输入体验会立刻变得黏黏糊糊。

代码高亮我对比过三种方案:Prism、Highlight.js、Shiki。Shiki 用的是 VS Code 的 TextMate 语法,高亮效果最接近 IDE,但那套 JSON 语法文件加载起来实在太重,光是加载语言定义就得好几百 KB。最终我选了 Prism,自己定制了一套颜色变量,让它跟亮色、暗色两套主题联动。

表格是最麻烦的。大多数 Markdown 编辑器的表格都是渲染成静态 HTML 表格,点击单元格后整个切到源码模式编辑,体验很割裂。我做了个“双击进入网格编辑”的模式:双击表格区域后,每个单元格变成一个输入框,撑开一个类 Excel 的编辑网格,失焦后自动写回 AST 并重新渲染。虽然工作量比想象中大,但这个功能一出来,整个编辑器的“彪悍”名号就立住了。

3.3 图片:从粘贴落盘到导出

图片处理是 Markdown 编辑器最容易被忽视但又最容易翻车的功能。截图之后直接 Ctrl+V,这是写作场景里最高频的操作。很多编辑器默认把图片以 base64 的形式填入文本,看着很方便,实际上埋了巨大的雷。

我的方案是:捕获粘贴事件,判断剪贴板里是否有图片文件,有就自动保存到当前文档同级的assets目录,文件名用时间戳加随机串,然后在文档里插入相对路径。这样文档和图片都在同一个文件夹里,用 Git 管理、用网盘同步都没问题,换设备打开也能正常显示。

同时我在设置里提供两种图片路径风格可选:相对路径和纯文件名。用 Typora 的人可能知道,它默认是用./assets/xxx.png这种形式,我这边默认也走同样逻辑。导出时,这些相对路径会统一解析成可嵌入的本地文件或 base64,保证 Word、PDF 里图片不丢。

4. 架构选型:为什么是 Electron + TypeScript 而不是别的

4.1 跨平台桌面方案的取舍

编辑器项目的第一步,是选桌面壳子。我当时认真评估了三条路线:

  • Electron + TypeScript:开发效率高,生态成熟,内存占用稍大
  • Tauri + Rust:打包体积小,内存低,但要额外学 Rust,WebView 兼容性在不同 Linux 发行版上像开盲盒
  • 纯 Web App:直接用浏览器的 File System Access API,免安装,但浏览器兼容性和文件系统权限都是问题

我最后选了 Electron + TypeScript。说实话,Tauri 的轻量真的很诱人,但开发周期长,而且我当时主业不是 Rust,切语言会拖慢进度。Electron 的 V8 渲染性能对这个场景完全够用,桌面 API 也齐全,配合 TypeScript 的静态类型检查,重构起来至少不会心里发虚。

这里有一个个人建议:如果你也想做同类项目,不要一上来就追求“终极技术形态”,先看自己手里有多少时间,以及顺手的技术栈是什么。工具是拿来用的,不是拿来秀肌肉的。

4.2 存储策略:Markdown 文件永远是唯一真实源

很多笔记软件喜欢用数据库或者私有格式存内容,渲染和展示再另存一份副本。我对这种事非常有戒心——万一软件打不开了,我的笔记怎么办?我的核心原则是:磁盘上那个.md文件永远是唯一真实源,界面上的所有内容都是它的投影。

这样一来,目录结构就是我的文档管理逻辑,文件名就是标题,文件夹就是分类。我可以用任何其他工具打开这些文件,内容绝对可读,而不是一堆加密的 sqlite 二进制。

这个决定直接影响了很多后续设计:不建私有索引数据库,全文搜索直接扫目录里的.md文件;大纲目录也从 AST 动态生成,不需要额外存储。好处是换机、迁移、备份都极其简单,坏处是全文搜索的性能要靠自己优化,这个后面讲。

4.3 自动保存与大文件保护

自动保存是编辑器体验的生死线,但“无脑自动保存”会带来两个问题:一是频繁写盘缩短固态硬盘寿命,二是写盘时的卡顿会影响输入体验。

我的实现是三层策略:第一层,击键后 1.5 秒防抖,时间到了才触发保存逻辑;第二层,保存前把当前内容做一次 hash,和上次保存的内容做比对,内容没变就直接跳过写盘;第三层,写盘方式不是简单writeFile,而是先写到临时文件,再改名替换原文件,这样即使中途断电,原文件也不会损坏。

大文件保护这块,我设置了一个阈值:文档超过 1MB 自动进入“大型文档模式”。这个模式下实时预览的粒度变粗,部分重型插件延迟加载,同时提示用户手动关闭代码高亮或者数学公式,用空间换速度。实测下来,一个 2MB 的日志型文档,开启大文件模式后编辑流畅度提升了将近五倍。

5. 性能优化的两次关键战役:长文档和中文输入法

5.1 十万字文档从卡顿到跟手

最早版本写完后,我拿一篇十万字左右的文档做压力测试,结果惨不忍睹:输入一个字,界面要等一两秒才刷新,滚动页面时 CPU 直接飙到 100%。问题的根源在于,我把整个文档的 AST 变化都反映到 DOM 上,一次编辑会触发整篇文档重新渲染。

解决思路是“分块渲染”。我把 AST 按块级节点拆成很多块,每次编辑只重渲染变更所在的块,其他块完全不动。配合虚拟滚动,浏览器视图窗口外部的块直接不渲染,滚动时动态回收和创建。这个方案落地后,十万字文档的编辑延迟从秒级降到了毫秒级。

虚拟滚动的实现坑很多。比如滚动条的高度计算要用阈值估算,不然渲染内容变了,滚动条长度会一直跳动;又比如滚动到末尾时,要准确判断“最后一块是否已经完整露出”,否则会出现滚动到底部还差一段的错觉。这些细节,没有一个现成库能直接帮我解决,全是靠一遍遍实际体验、一点点调出来的。

另外一个容易被忽略的优化是contenteditable里的选区恢复。在“所见即所得”模式下,高亮、公式、代码块这些原子节点不能把光标放进去,用户上下左右移动光标时,要提前拦截并跳过整个原子节点。这个逻辑写不好,光标就会“钻进”代码块内部,整个编辑体验瞬间崩盘。

5.2 中文输入法组合输入下的光标保卫战

中文用户最痛苦的技术问题之一:中文输入法上屏时,编辑器的实时渲染会疯狂打断组合输入。你打“zhongwen”的时候,预览区域会按照拼音片段疯狂猜测、重新渲染,页面闪个不停,更严重的是,光标可能直接被重渲染冲掉,字还没上屏就丢了。

这个问题的本质是:输入法在组合输入阶段,会持续生成compositionstartcompositionend之间的一系列事件。如果编辑器每收到一个事件就触发 AST 解析和 DOM 更新,那中路截胡几乎是必然的。

我的解法是:

  1. 监听compositionstart,进入“输入法组合态”,暂停所有实时渲染
  2. 组合期间只做文本累积,不碰 AST,不碰选区
  3. 监听compositionend,等输入法把最终字符上屏后,一次性完成解析、渲染、选区恢复

这套逻辑必须同时处理代码编辑器的底层操作,因为用户在源码模式下的中文输入同样会出现这个问题。我一开始只处理了所见即所得模式,后来测试发现源码模式也有同样症状,又补了一遍。这也算是一个典型的“自己写编辑器才碰得到的坑”。

6. 编辑器开发中那些“文档里查不到”的坑

6.1 表格编辑与光标定位的相爱相杀

表格的网格编辑模式上线后,我遇到了一个非常诡异的问题:当表格有合并单元格,或者某一列特别宽时,点击单元格定位光标,光标跑到完全不相干的位置去。

排查了半天,最后发现根子出在contenteditable二维表格结构上。浏览器的光标定位逻辑天然是为线性文本设计的,一旦 DOM 是二维表格,它会在内部做个坐标转换,而不同浏览器的转换公式不一致。更不要提表格单元格里的换行,我以为输入 Enter 是换行,浏览器直接给我新开了一行表格行,整个版面全乱了。

解决办法是:把所有表格操作都拦在键盘事件层级。Enter 键被重写成“在单元格内部插入换行”而不是“新增表格行”,Tab 键被重写成“跳到下一个单元格”,方向键根据当前光标位置判断是否跨界,跨界的瞬间手动计算下一个单元格的光标坐标。这些代码写起来很繁琐,但每一条都是真实使用场景里磨出来的。

6.2 Base64 图片:一秒钟把文件变成怪兽

接着前面图片处理的话题,我再详细聊聊 base64 这个问题。截图粘贴后把图片编码成 base64 字符串塞进.md文件,这个做法早期版本不小心放过一次进入生产代码。测试的时候发现,一个只有 20 行的文档,因为嵌了一张 2MB 截图,整个文件膨胀到接近 3MB,尤其是中文内容加 base64 混排,渲染器和 Git diff 全部变慢。

更隐蔽的问题是搜索。base64 字符串里面有大量无意义字符,如果用户搜索某个中文关键词,Markdown 全文搜索会把 base64 也遍历一遍,性能直接爆炸。所以我后来加了严格规则:解析图片链接时,一旦发现data:image开头的内容,直接跳过不索引,同时在导出时提醒用户“文档中包含内嵌图片,建议转成本地文件”。

这个坑提醒我:编辑器是给写作者用的,不能因为自己图省事,把底层数据结构搞得很脏。数据源干净,后续功能才有做好的可能。

6.3 跨平台字体渲染差异的血泪教训

项目开发到后期,我把编辑器打包给几位朋友测试。结果同一个文档,Windows 上滚动顺滑,macOS 上滚动微卡;macOS 上光标定位精准,Linux 上光标往下偏了半个字。这些都是字体渲染差异引发的连锁反应。

核心原因是不同平台的字体指标不一样。微软雅黑和苹方在相同字号下的行高、基线位置都有区别。如果 CSS 里写死line-height: 1.75,Windows 和 macOS 显示的实际高度也可能不同,导致滚动容器高度算不准,虚拟滚动就会出问题。

我的解决办法是:给每个平台单独定义一套排版变量,在应用启动时检测平台类型,再覆盖默认值。同时开启text-rendering: optimizeLegibility,让中文笔画渲染更平滑。这个坑给我最大的教训是,跨平台应用永远不要只在自己熟悉的平台上测试,不同系统的渲染引擎有各自的脾气。

7. 后续规划:给编辑器装上更聪明的脚

目前的版本已经足够日常使用,但仍然有很多想做的方向。一句话总结,就是“更聪明,更懂写作者”。

第一个方向是全文索引。目前搜索走的是文件遍历加正则匹配,小目录没问题,几千个文件时会有明显延迟。打算接一个轻量级倒排索引,中文分词是难点,想先用粗糙的二元分词顶着,后续再优化。

第二个方向是模板系统。写博客和写周报的排版习惯完全不同,我想让编辑器支持“文档模板”:新建笔记时选择模板,模板定义好的标题层级、示例段落、常用代码块结构都自动生成,用户只需填充内容。

第三个方向是导出生态。目前已经支持 PDF、Word、HTML 和剪贴板富文本,但 Word 的导出偶尔还是会有样式偏移,这块准备引入更强大的转换引擎,顺便支持自定义 CSS 控制导出效果。

第四个方向是定位到块级引用。类似 Notion 那种“引用一个段落”的能力,在 Markdown 语境下可以用块级链接实现。不过这个方案要谨慎,如果做不好容易把纯文本生态搞复杂,我自己还在权衡。

最后一个方向是关于项目本身的哲学。做这个编辑器的过程让我明白,很多“想要的功能”其实经不起推敲,真正高频的永远是那几件事:打开、写字、存盘、导出。把这几件事做到极致,比堆一百个炫技功能更重要。

我在实际使用中最常被问到的一句话是,值不值得花这么多时间自研编辑器,毕竟现成工具那么多。我的回答是:如果你也是一个对效率和美感有执念的重度用户,值得试一次。就算最后只写了个残废版本,你也会在过程中重新梳理自己的工作流,明白自己真正离不开的到底是哪几个功能。这种掌控感和清醒感,现成工具永远给不了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询