
先交代背景我原来也是个重度 Markdown 编辑器用户从 Typora 用到 Obsidian还折腾过 VS Code 一堆插件。后来我彻底放弃所有本地 Markdown 编辑器改成用一个自己写的、大约 200 行的 HTML 文件来写作和排版。这个决定听起来很极端但实际用下来非常舒服我甚至把它用成了主力的写作与排版工具。这篇文章就把我为什么这么做、这个 HTML 文件是怎么设计的、用在什么场景、踩过哪些坑全部摊开讲一遍。我知道很多人第一反应是200 行 HTML 能干什么是不是太简陋了没代码基础能不能用这些问题我都遇到过。先给结论如果把 Markdown 编辑器当成一个写作工具那 200 行 HTML 够用了而且在某些方向比大而全的编辑器更顺手。1. 本地 Markdown 编辑器到底毁掉了什么先说清楚我为什么要折腾这个事不然直接甩一个 HTML 文件出来你很难理解背后的选择逻辑。市面上主流的本地 Markdown 编辑器本质上都在做同一件事把写 Markdown 文本和看渲染效果这两个动作绑定在一款桌面软件里。这个方向本身没问题问题出在它带来的一系列连锁反应。1.1 插件体系和主题体系是一个无底洞我用过的几款编辑器几乎都有插件市场和主题市场。听起来很美好但你一旦进入这个生态就会发现自己永远在折腾工具而不是在写作。以 Obsidian 为例我当年为了一个每日笔记自动归档的功能装了 6 个插件其中有两个互相冲突每次更新版本都要重新调。后来为了换一套舒服的阅读主题我又花了整整两个晚上调整 CSS 片段。写笔记的时间还没调样式的时间长这已经背离了工具服务写作的初衷。更重要的是这些插件和主题都是有生命周期的。作者弃更、版本不兼容、和系统更新冲突任何一个环节出问题你的写作环境就会崩。我经历过一次最离谱的某次编辑器大版本更新后我日常用的几个核心插件全部失效等于整个工作流被打回原形。1.2 数据所有权本身就是个伪命题本地编辑器听起来比在线文档更私有数据都在你自己电脑上。但你仔细想想你的 Markdown 文件真的能被任意工具打开吗我试过在不同编辑器之间迁移笔记库。就一个最基础的内部链接功能A 编辑器的链接格式是[[笔记名]]B 编辑器支持的格式却是[笔记名](笔记名.md)还有的喜欢用obsidian://协议来跳转。同一个 Markdown 文件在不同编辑器里呈现出来的完全不是同一个东西。更隐蔽的问题是元数据。很多编辑器会在文件头部写入YAML front-matter包含标签、别名、创建时间等信息。这些信息在编辑器里显示得很规整但一旦脱离了这个编辑器就是一堆需要手动清理的垃圾字符。你觉得自己在写纯文本实际上已经被编辑器的格式偷偷绑架了。1.3 编辑器的性能越来越重这个可能很多人体会没那么深因为大家默认编辑器就该越来越占资源。但经历过就知道有多离谱一个几万字的 Markdown 文件在某个大牌编辑器里打开都要转圈几秒。如果里面嵌了几张图片编辑时每敲一个字整个页面都会卡一下。我写长文的时候经常要同时开好几个文档参照着看。一旦文档数量超过五个编辑器的内存占用就是个灾难。风扇狂转手指敲完字要等半秒屏幕上才显示出来这种体验真的会让人失去写作欲望。说到底Markdown 的全部优势在于极简、纯文本、可迁移。但现代编辑器为了抢占用户不断往里加功能最后反而把 Markdown 最宝贵的轻量属性给拖垮了。2. 我为什么选择用一个单文件 HTML 当写作工具在决定自己做工具之前我其实也考虑过几个替代方案但都有让我不满意的地方。2.1 我最初的约束条件我给自己的写作工具定下了几个硬性约束数据必须是我能完全掌控的纯文本文件任何软件都能打开启动时间不能超过 1 秒界面不能卡必须是跨平台的换电脑、换系统都能用不依赖任何需要注册、登录、同步账号的服务渲染效果要稳定一致不能因为软件版本升级而变化最好能不装额外软件就能运行。这几条约束直接排除了所有桌面编辑器因为没有任何一款能同时满足。于是我换了个思路既然现有编辑器给不了我想要的那我用浏览器自带的渲染能力能不能自己搭一个2.2 单文件 HTML 的独特优势答案是可以而且比我想象中干净得多。一个 HTML 文件天然满足上面所有约束它是一个纯文本文件你可以用任何编辑器打开查看内部内容浏览器打开就是秒开不会占什么内存Windows、macOS、Linux、甚至手机平板的浏览器都能打开不需要安装运行时数据可以存在 HTML 文件里也可以单独导出成标准 Markdown 文件渲染逻辑写死在文件里打开什么效果就是什么效果不会变。最关键的一条是它几乎为零的维护成本。你不用管什么依赖、版本、插件冲突一个文件复制走整个工具就带走了。这种文件即工具的感觉桌面软件给不了你。2.3 为什么是200 行而不是更多200 行这个数字不是硬凑的它是一个很自然的平衡点。实现最核心的需求——一个 Markdown 输入区一个渲染预览区加一个导出功能——其实只需要 50 行左右。再多加一些实用功能比如自动保存、一键复制 HTML、打开本地文件、打印成 PDF才到 150 行左右。剩下几十行是 CSS 样式让界面看起来不至于太寒酸。我之前也试过往里面堆功能比如加文档树、加标签管理、加全文搜索结果发现这些功能一旦加进去文件很快就膨胀到上千行而且本质上就是在低水平地重复造 Obsidian 的轮子。200 行是个很好的心理锚点它提醒我这个工具只是为了解决写 Markdown 并得到好看排版这一个核心问题而不是为了拥有一个完整的知识管理系统。3. 这个 200 行 HTML 的核心实现路径注意理解标题里的200 行指的是填写项目正文和完成核心的 Markdown 解析与页面交互逻辑。Markdown 语法解析本身我并没有手写一个解析器这是这篇文章最重要的一个为什么不要在 2025 年自己写 Markdown 解析器。3.1 核心依赖选择一个可靠的 Markdown 解析库Markdown 解析规则的复杂度远超普通人想象。光是一个换行到底生不生成br在不同实现里就有好几种行为差异。如果你自己写正则去解析很容易陷入各种边界条件的泥潭里。我选择的是marked这个库它是一个老牌、稳定、单文件可用的 Markdown 解析器。以前需要去官网下载现在比较方便的方式是用 CDN 引入。之所以选它而不是别的库主要是因为体积小压缩后只有几十 KB不需要构建工具浏览器环境直接用script标签加载解析行为非常接近 GitHub 的 Markdown 风格这对我跨平台发帖很关键周边生态成熟很多渲染细节问题都能搜到解决方案。当然CDN 引入有一个问题如果离线打开 HTML解析库就不加载了。但这个场景我实际测试下来影响不大因为写作环境通常都有网络。而且即便没有网络也可以用备用方案就是先把marked.js的源码粘贴到 HTML 文件里。不过这样文件行数会变多反而违背了200 行的初衷所以我最终还是选择了维护一个标准的 HTML 外部引用结构。3.2 总体文件结构一个单文件里的三个功能区这个 HTML 文件的整体设计思路是把写、看、发三个动作全部集中在同一个界面里。文件结构非常简单就三个部分!doctype html html langzh-cn head meta charsetutf-8 meta nameviewport contentwidthdevice-width, initial-scale1 titleMD Workbench/title style /* CSS 区负责界面布局和打印样式 */ /style /head body !-- 界面区textarea 输入框、预览区、工具栏按钮 -- div idtoolbar button onclickexportHTML()复制 HTML/button button onclickexportMD()导出 MD/button button onclickwindow.print()导出 PDF/button button onclickopenFile()打开文件/button /div div idcontainer textarea idinput placeholder在这里输入 Markdown.../textarea div idpreview/div /div !-- 脚本区引入 marked.js 业务逻辑 -- script srchttps://cdn.jsdelivr.net/npm/marked/marked.min.js/script script // 业务逻辑区 /script /body /html这里面最核心的设计是左右分栏左边是所见即所得的输入区右边是实时渲染的预览区。打字的时候右边会立刻同步出排版后的效果。这个体验跟 Typora 那种即写即渲染不一样但习惯之后反而更舒服——因为左边永远是纯文本你能清楚看到 Markdown 标记本身右边则是面向读者的最终效果。3.3 渲染逻辑实时刷新与安全过滤实时渲染是整个文件里最关键的逻辑核心就是一个函数function render() { const text document.getElementById(input).value; const html marked.parse(text); document.getElementById(preview).innerHTML html; }但直接这样做有一个安全漏洞如果 Markdown 里写了script标签或者内联事件处理器marked 默认情况会原样输出预览区就会执行这段脚本。这在写技术文章时可能还好但如果要复制粘贴别人发给你的 Markdown 内容就存在被植入恶意脚本的风险。所以我实际在使用时会加上一个简单的 HTML 转义过滤器把原始文本里的script等危险标签先清理掉。这个动作在 marked 官方文档里叫DOMPurify或者自定义sanitizer。因为我不想额外引入一个 DOMPurify 库所以用了最朴素的方式在渲染前先用正则把输入里的script等关键标签替换掉。function render() { let text document.getElementById(input).value; text text.replace(/script[\s\S]*?\/script/gi, ); const html marked.parse(text); document.getElementById(preview).innerHTML html; }这就是一个安全底线。哪怕牺牲掉在 Markdown 里写 HTML 标签的能力也要保证预览区永远执行不了陌生代码。3.4 自动保存LocalStorage 的巧妙应用桌面编辑器崩溃的痛大家都懂。为了防止浏览器意外关闭导致内容丢失我用浏览器自带的localStorage实现了自动保存。function autosave() { localStorage.setItem(md-workbench, document.getElementById(input).value); } function restore() { const saved localStorage.getItem(md-workbench); if (saved) { document.getElementById(input).value saved; render(); } } // 每 5 秒自动保存一次 setInterval(autosave, 5000);这个功能很轻加起来不到 10 行但它解决了一个大痛点以前用桌面编辑器如果忘记手动保存软件崩溃就等于白写。现在浏览器里的 localStorage 帮我兜底关掉页面再打开内容还在。不过要注意localStorage 是跟浏览器和域名绑定的。在本地file://协议下localStorage 的行为因浏览器而异但实测 Chrome 和 Edge 都支持。保险起见我还是保留了一个导出 MD按钮写重要内容时手动存一份到磁盘上。3.5 导出Markdown、HTML、PDF 三个出口写作最终要拿到别处用所以我设计了三个导出出口。导出 Markdown 文件用的是浏览器下载文件的标准方式function exportMD() { const text document.getElementById(input).value; const blob new Blob([text], { type: text/markdown }); const url URL.createObjectURL(blob); const a document.createElement(a); a.href url; a.download note.md; a.click(); URL.revokeObjectURL(url); }这个导出的文件是纯文本你可以用任意编辑器打开符合我最初数据完全可控的要求。复制 HTML这个功能是我最常用的一个。因为很多平台的编辑器比如公众号后台和一些博客系统都支持直接粘贴 HTML 内容。我只需要在预览区右键检查把渲染后的 HTML 整体复制再粘贴到富文本编辑器里排版就完全保留了。后来琢磨了一下这是个可以优化的地方于是加了一行代码让复制 HTML按钮直接把预览区的innerHTML写入剪贴板。导出 PDF则完全依赖浏览器的打印能力// 配置好打印样式后直接用 window.print()在 CSS 里加一段media print规则告诉浏览器打印时只显示预览区内容、隐藏输入区和工具栏。然后浏览器会弹出系统打印对话框选择另存为 PDF就能得到一份排版干净的 PDF 文档。这个流程我用过很多次体验非常顺畅。3.6 图片与附件一个必须明确的边界Markdown 写作绕不开的是图片。这个 200 行的 HTML 文件我设计的原则是只做文本排版不做图片管理。我的处理方式是如果你在 Markdown 文件里引用本地图片路径比如HTML 预览区无法直接显示。但这在我看来不是问题反而是一个特性。因为在大部分桌面编辑器里图片嵌入功能做得越花哨文件的可迁移性就越差。有些编辑器会偷偷把图片复制到自己的附件目录你导出纯 Markdown 时反而找不到图片。而在我的方案里我倾向于用在线图床的 URL 来写图片这样预览区能显示导出后任意设备打开也没问题。如果你必须在离线场景引用本地图片我实测有一个变通方法用 base64 编码把图片内嵌到 Markdown 里这样预览区能显示但会让文本变得巨大。所以我的建议是这个方案的重心是纯文本写作和排版输出图片尽量走图床或稍后手动在发布平台补上。3.7 打开现有 Markdown 文件目前完整的方案已经在正文里描述得很清晰了我补充一个打开文件按钮的实现思路这样就不用每次把文本粘贴进 textareafunction openFile() { const input document.createElement(input); input.type file; input.accept .md,.markdown,.txt; input.onchange (e) { const file e.target.files[0]; const reader new FileReader(); reader.onload () { document.getElementById(input).value reader.result; render(); }; reader.readAsText(file); }; input.click(); }这个功能结合了本地文件是唯一数据源的理念HTML 只是一个工作台文件本身始终留在你的磁盘上你需要工作时用它来打开写完保存它又回到磁盘。这个循环跑通之后完全不需要再信任任何单独的数据仓库。4. 实测过程中踩过的几个坑和避坑细节这套工作流我对身边朋友分享过好几个实操中大家问得最多的不是功能实现而是为什么我的效果不一样。我整理一下最常见的坑。4.1 marked 版本差异导致的渲染行为变化我记得早期某个版本里marked.parse的返回行为在不同版本间有细微差异。有一次我更新了 CDN 链接没有仔细核对结果预览区突然不渲染多级引用列表了排查了很久才发现是 marked 版本升级之后默认关闭了某些旧的扩展语法。现在我用到的解决办法是在 HTML 文件头部显式指定一个固定的 marked 版本号比如script srchttps://cdn.jsdelivr.net/npm/marked/marked.min.js/script重点是用精确版本号锁定比如marked4.2.12。不要用latest这种浮动版本否则哪天依赖更新你的写作助手行为就变了这违背了工具稳定性的初衷。4.2 打印导出 PDF 时的分页问题导出 PDF 时最容易出现的问题是长代码块或表格在打印时被拦腰截断。默认的打印样式不会理你的内容结构。我加了一段样式来解决这个问题media print { #input, #toolbar { display: none; } #preview { width: 100%; } pre, blockquote, table { break-inside: avoid; } }break-inside: avoid是解决分页截断的关键。它告诉浏览器尽量让代码块和表格保持在同一页内不要拆开。这个属性兼容性很好主流浏览器都支持。4.3 中文排版细节字体和行距Markdown 排版出来的中文和西文混排效果很多人忽略。如果使用纯默认字体在 Windows 上经常是宋体观感确实不好。我在 CSS 里把字体栈改成了更适合中文阅读的组合#preview { font-family: -apple-system, BlinkMacSystemFont, Segoe UI, PingFang SC, Hiragino Sans GB, Microsoft YaHei, sans-serif; font-size: 16px; line-height: 1.8; color: #333; max-width: 720px; }这里有一个专门的考虑先写系统默认西文字体Apple 系再写中文回退字体苹方、雅黑。因为中文字体里混入西文字母时用西文字体渲染字形更精致。line-height: 1.8是中文阅读比较舒服的行距太紧凑会显得拥挤太松散又显得文本稀疏。4.4 全角与半角符号的输入干扰还有一个容易忽略的坑在中文输入法下你可能下意识打出全角引号或者全角冒号。这本身不影响 Markdown 的渲染但如果你写的是 Markdown 链接或者代码块则容易出问题。比如写[链接](https://example.com)时如果是全角括号或者全角冒号Markdown 解析器会认为这不是一个合法链接渲染不出来你检查半天也看不出哪里错了。这种问题靠代码很难提前预防。我的经验是写完后统一扫一遍链接有没有全角字符混入。好在现在的代码错误不会造成文件损坏顶多就是渲染不出来养成习惯就好。4.5 本地打开文件的浏览器限制如果双击 HTML 文件用本地file://协议打开有几点要注意。最突出的问题是某些浏览器会限制打开本地文件这个操作尤其是一些安全策略较严格的浏览器。Edge 和 Chrome 实测是支持的但 Safari 比较麻烦。如果你在打开文件时发现没有反应解决方案很简单把 HTML 文件拖到浏览器窗口里打开或者用文件 → 打开文件菜单来加载通常能解决。4.6 XSS 安全问题的边界前面提到过要对script做过滤。实际使用中我后来发现除了script标签还要注意onerror这类事件属性。比如 Markdown 里写了img srcx onerroralert(1)有的事件处理器会绕过简单的标签过滤。所以我现在采用一个更彻底的做法禁用 raw HTML 或者严格白名单过滤。marked 本身提供sanitize配置在较新版本里更推荐的做法是配合 DOMPurify 使用。但为了控制行数我还是保留正则过滤的方式然后明确告诉自己不要在不信任的来源内容里粘贴到编辑器里执行预览。这个边界是任何 Markdown 编辑器都要面对的问题并不是我这个方案独有但必须时刻意识到。5. 这个方案的适用边界和长期演进思路讲了这么多优点最后也必须诚实说一下这个方案不适合哪些场景。不然你兴冲冲做了一天结果发现自己根本不需要那就浪费感情了。5.1 它到底适合谁不适合谁先说不适合的重度知识管理者你有一万个笔记需要建立层级目录、双向链接、关系图谱。这类用户需要 Obsidian / Logseq 提供的那套信息组织体系200 行 HTML 给不了多人协作团队你需要的是类似飞书文档、腾讯文档这样的多人实时协作能力而不是一个离线单文件工具追求眼花缭乱功能的人想要嵌入思维导图、白板、时间线、数据库视图的这个方案只会让你失望。再说适合的写作高频、排版要求稳定的人比如写技术博客、公众号文章、公司周报需要 Markdown 转 HTML 后粘贴到富文本编辑器里这个工作流非常顺手对工具稳定性极度敏感的人你受够了功能升级和不兼容只想找个十年后打开还是一模一样的解决方案喜欢纯粹 Markdown 语法的人你不想要任何私有标记语法、不想要编辑器生成的隐藏元数据只想干净地写.md文件。按我的实际使用来看它最适合的是写作 排版这个中间环节而不是笔记管理前端或发布后端。5.2 我后来加了什么功能以及为什么没继续加用了一个多月后我给这个 HTML 文件加过两个功能。第一个是字数统计。顶栏显示当前 Markdown 字数不含空格和预计阅读时间这对写公众号和知乎文章很有用方便估算篇幅。实现起来非常轻渲染时同步统计一下字符数就好。第二个是自动生成目录。因为我的文章长文较多我需要一个跳转目录。我基于标题标签h1-h4生成锚点目录放到预览区顶部。这个功能在写作长篇教程时很好用效果类似于多级标题导航。但也到此为止。我原本还想加标签管理、双链笔记、全文搜索后来冷静下来想一想这些功能一旦加进去就会跟桌面编辑器陷入同样的陷阱——功能和复杂度螺旋增长最终失去了简单这个唯一优势。所以我把它们挡在设计原则之外这个工具服务的是写作当刻不是长远的档案管理。5.3 从反工具化到再工具化的一点思考有人会说你自己动手写编辑器这不也是一种折腾吗我想澄清一下区别桌面编辑器的折腾是被动的你要不断适应软件作者的决策和生态的变动而写这 200 行 HTML 的折腾是主动的每加一个功能我都明确知道它解决什么问题不加它也不会有什么损失。用一句大白话总结就是以前的我在给工具打工现在的工具在给我打工。对于普通用户如果你觉得文章里这套方案对你有帮助但又暂时不想自己写 HTML我建议你可以先复制这个结构改改 CSS 配色把字体和按钮换成自己喜欢的再慢慢增加功能。它不会替你做知识管理但它能让你找回一段打开文件、写完、关闭的纯粹写作体验。实际上我到现在还把这份 HTML 放在网盘和 GitHub 仓库里作为日常技术写作的固定起点。它不完美但对我而言它比那些半年更新一次、加一堆新功能的大块头编辑器更贴近 Markdown 最初的设计精神纯文本可迁移永远属于你自己。