资讯详情

开源Markdown阅读器实战:从技术选型到核心功能实现

📅 2026/9/11 10:13:59 | 华诺云谱 👁 阅读
开源Markdown阅读器实战:从技术选型到核心功能实现
1. 为什么会有这个项目——一个 Markdown 阅读器凭什么收费1.1 事情起因从一次“付费弹窗”说起事情是这样的。我平时写技术文档、记笔记、整理博客草稿基本都离不开 Markdown。电脑里相关的编辑器和阅读器装了一堆其中有一款界面漂亮、功能齐全的 Markdown 编辑器我一直用得很顺手直到某一天打开它弹窗告诉我新版本开始收费了想继续用完整功能得掏 89 块。说实话那个价格放在一堆动辄几百块的笔记软件里并不算离谱编辑器本身做得也确实不错。但问题是——我需要的只是一个“阅读器”一个用来快速打开.md文件、看渲染效果、偶尔导出 PDF 的轻量工具。让我为这种高频但简单的需求持续付费我心里总觉得不太对劲。于是我开始认真地想一个问题市场上免费的 Markdown 阅读器那么多为什么我一直没有找到一款真正合心意的1.2 免费替代品为什么总差一口气我花了两天时间把已知的免费方案几乎试了个遍可以简单分成几类来说第一类是浏览器插件比如某些能直接渲染 Markdown 文件的 Chrome 扩展。优点是真的轻双击文件就能看缺点是样式太死板、不能自定义主题遇到稍微复杂一点的表格就渲染得七零八落代码高亮也经常缺胳膊少腿。第二类是通用文本编辑器的“预览模式”比如 VS Code 的 Markdown Preview、Sublime 的插件等。功能确实强但打开速度慢还总是带着一堆编辑器特有的边边角角、侧边栏、状态栏看着不像一个“阅读工具”。第三类是各大笔记软件自带的导入预览比如印象笔记、Notion 之类的。问题是它们都要求你先“导入”或“同步”本地文件只是临时预览文件一多、目录一深用起来极其别扭。这些方案不是不能用而是都没有做到“打开即读、专注内容、风格好看”这三点。既然市面上找不到、付费又觉得不值那我干脆自己动手做一个开源的免费阅读器。至少做出来之后我自己能用上想要的东西也能让同样被这问题困扰的人少走点弯路。1.3 动手之前先把需求边界画清楚在写第一行代码之前我花了一晚上梳理需求。这一步非常重要因为它决定了项目会不会中途失控。我给自己定的核心定位是“阅读器”不是“编辑器”。这意味着不追求写入功能光标、自动补全、快捷键编辑这些一概不做不追求项目管理不建数据库、不做标签体系、不搞多端同步只专注一件事把一个 Markdown 文件或整个目录快速渲染成漂亮、可读、可分享的页面附带几个真正高频的需求目录导航、代码高亮、主题切换、打印/导出 PDF。想清楚这几点之后项目的边界就非常清晰了。后面所有功能开发都围绕这条主线展开凡是跟“阅读体验”无关的需求我一律暂时不加避免项目越做越臃肿。2. 整体设计与技术选型解析2.1 技术栈选型Electron 还是 Tauri还是干脆做 Web第一步是选择跑在什么壳子里。市面上主流的跨平台桌面方案无非三种纯 Web 应用、Electron、Tauri。纯 Web 应用开发成本最低甚至可以用浏览器直接打开本地文件但有两个问题很难绕过去一是普通用户对“打开本地文件”的操作不熟悉二是浏览器的跨域限制让本地文件读取非常麻烦尤其是读取同目录下的图片、附件时会碰上一堆权限问题。这个方案只适合做在线 Demo不适合做正经工具。Electron 是大多数桌面工具的首选优点是生态成熟、文档多、遇到问题搜得到答案Chromium 内核的渲染能力也完全够用。缺点也很明显——打包体积大一个空应用就要 70MB 以上性能和内存占用也谈不上好。Tauri 是这两年很火的方案用系统自带的 WebView 渲染打包体积小、内存占用低还能直接调 Rust 后端看起来很完美。但它的短板是“还比较年轻”尤其是 PDF 打印、内嵌浏览器行为在 Windows 和 Linux 的不同 WebViewWebView2 / WebKitGTK上表现不一致容易踩到一些冷门坑。我最终选了 Electron。原因不复杂这是一个以“渲染效果”为核心的项目Electron 的 Chromium 渲染行为在各平台最统一开发时遇到的“为什么我的页面在这里是这样、在那里是那样”的烦恼会少很多。而且项目目标是开源、免费、低维护成本选最稳的方案比选最酷的方案重要。2.2 渲染管线拆分解析、渲染、样式三层分离在规划代码结构时我坚持了“解析、渲染、样式”三层分离的思路。这个设计决定了项目后期的可维护性。第一层是解析层负责把 Markdown 文本转换成 HTML。这里我选的库是 markdown-it它的插件生态非常丰富支持 GFMGitHub 风格的 Markdown 扩展、表格、任务列表、删除线等常见语法解析速度也很快。另外它可以配置“启用哪些规则”这对我后面做安全控制很有帮助。第二层是渲染层负责把 HTML 注入到页面中。这里要特别注意一个点如果是把用户自己写的 Markdown 直接塞进页面一定要经过清理和过滤否则恶意 HTML 会带来 XSS 安全风险。后面我会专门讲这个问题。第三层是样式层负责让渲染出来的内容好看。这一层我用的是纯 CSS 变量做主题系统内置了深色、浅色、护眼、高对比度等几套皮肤用户还可以自己写 CSS 覆盖。选择这套方案是因为它简单、透明、没有黑魔法用户想改哪里看一眼变量名就明白了。这三层各管各的事互不干扰后面想换解析器或改主题都容易。2.3 功能范围收敛这些功能我一开始就决定不做上面说过项目最容易失败的地方不是功能太少而是功能太多。我在 README 里明确写了“非目标”列表不上传文件到任何云端所有内容只在本机处理不做文档编辑遇到想改内容的情况请用你自己的编辑器不做文件管理不关心你的.md文件放在哪个目录、怎么组织不做插件系统至少在 v1.0 之前不做。这种“砍需求”的决策在开源项目里往往比加功能更关键。因为一旦承诺了什么维护者就得长期背着这个包袱。一个只有 12KB 核心逻辑、在 5 秒内能打开任意.md文件的工具才是我想做的。3. 核心功能实现与实操细节3.1 五步完成 Markdown 解析与渲染整个项目最核心的功能就是把 Markdown 变成漂亮的 HTML 页面这一步的实现并不复杂核心代码逻辑如下// 初始化 markdown-it启用 GFM 和表格支持 const MarkdownIt require(markdown-it); const md new MarkdownIt({ html: true, // 允许 HTML 标签但要经过后面清洗 linkify: true, // 自动把 URL 转成链接 breaks: false, // 保持 GFM 风格严格换行语义 typographer: true // 自动转换排版字符和省略号 }) .use(require(markdown-it-task-lists)) // 任务列表支持 .use(require(markdown-it-anchor)) // 标题生成锚点 .use(require(markdown-it-toc-done-right)); // 生成目录 // 读取本地文件内容 const rawText fs.readFileSync(filePath, utf-8); // 渲染成 HTML 字符串 const renderedHTML md.render(rawText); // 注入到页面主体区域 document.getElementById(reader-content).innerHTML renderedHTML;这里有个细节想强调breaks参数我特意设成了false。如果你不熟悉 Markdown 语法可能会以为“我在 Markdown 里敲了一个回车渲染出来就应该换行”但严格来说 Markdown 的换行语义是“需要两个空格 换行或者空一行才能形成新段落”。大多数平台比如 GitHub默认就是这种严格模式。设成false能保证我在某个平台上写的文档拿到这个阅读器里渲染段落结构和原平台看到的保持一致不会出现“明明人家那边好好的这里全变一行了”的情况。如果你个人比较习惯“敲回车就换行”的写作方式这其实是很多非程序员用户的习惯也可以在设置里加一个“软换行渲染”开关把breaks改成true就行了。我在设置面板里留了这个选项实测对中文用户非常友好。3.2 目录生成、行号定位与代码高亮的实现很多人看 Markdown 文档最常用的功能就是目录跳转。这个功能实现起来也不复杂核心思路是先通过markdown-it-anchor给每个标题生成带id的锚点再用markdown-it-toc-done-right提取标题层级关系生成目录树最后给目录条目绑定点击事件点击时通过scrollIntoView平滑滚动到对应标题位置。为了让阅读体验更贴近“看文章”的感觉我还加了一个“当前阅读位置高亮”。实现原理是监听页面滚动事件计算当前视口顶部靠近哪个标题的锚点位置然后在目录中给它加一个高亮 class。这里有一个细节要注意滚动事件会触发得非常频繁不能每个事件都去遍历所有标题否则会有卡顿感。我用了requestAnimationFrame做了节流只保证每帧最多计算一次。代码高亮用的是 highlight.js。这里要提醒一个容易踩的坑按需加载语言包。如果你直接默认引入全部语言打包体积会增加大概 300KB 左右而如果只引入你真正用到的语言可以大幅减小体积。我默认加载了 JavaScript、TypeScript、Python、Java、C/C、Go、Rust、Shell、SQL、JSON 等十几个常用语言同时做了自动检测遇到没引入的语言会退回纯文本避免误报高亮。// 按需引入需要高亮的语言 const hljs require(highlight.js); hljs.registerLanguage(javascript, require(highlight.js/lib/languages/javascript)); hljs.registerLanguage(typescript, require(highlight.js/lib/languages/typescript)); hljs.registerLanguage(python, require(highlight.js/lib/languages/python)); // ... 其他语言 // 在 HTML 注入完成后执行高亮 document.querySelectorAll(pre code).forEach((block) { hljs.highlightElement(block); });这样设计之后即使打开一个塞满代码的巨型文档页面依然能保持流畅滚动。3.3 主题系统与自定义样式的无侵入方案主题系统是这个项目比较出彩的一个部分。我没有用复杂的主题框架而是用了 CSS 变量这一套“看起来很简单但其实很强大”的方案。我先在:root里定义了所有设计变量比如背景色、前景色、标题色、边框色、代码块背景、链接色等等每个主题其实就是一组变量的取值。浅色主题、深色主题、护眼主题豆沙绿背景、高对比度主题都是这样实现出来的。用户在“设置”里切换主题时我只是在根元素上换一个>:root[data-themelight] { --bg-color: #ffffff; --text-color: #2c3e50; --border-color: #e8e8e8; --code-bg: #f8f8f8; } :root[data-themedark] { --bg-color: #1e1e1e; --text-color: #d4d4d4; --border-color: #3a3a3a; --code-bg: #2d2d2d; }我还给用户开放了“自定义 CSS”入口。在设置里你可以加载一份本地 CSS 文件这个文件会在渲染内容之后、关闭标签之前注入用户可以随意覆盖任何样式。这个方案被我称为“无侵入式”设计——我不需要做复杂的主题编辑器只需要留一个入口懂 CSS 的用户能自己玩出花来不懂 CSS 的用户也不受影响。注意开放自定义 CSS 意味着用户注入的样式完全可信不会造成安全问题。但如果以后想支持“用户加载任意 Markdown 里的 HTML”清洗工作就变得至关重要了这一点千万别省。3.4 导出 PDF 与打印的一些坑阅读器还得能导出 PDF不然很多时候没法拿去打印、分享或存档。Electron 有个原生 APIwebContents.printToPDF可以直接把当前页面渲染成 PDF。这个功能表面上很简单实际上有两个坑非常折磨人。第一个坑是字体渲染乱码。如果你直接把中文系统的默认字体嵌到 PDF 里有些平台导出后会出现方框或乱码。解决办法是在 CSS 里显式指定中文字体栈比如在 Windows 下用Microsoft YaHei, PingFang SC, Noto Sans CJK SC这些字体并在printToPDF时设置合适的pageSizeA4和边距。这里有实际调优过的打印样式代码async function exportPDF() { const pdf await webContents.printToPDF({ pageSize: A4, printBackground: true, margins: { top: 0.75, bottom: 0.75, left: 0.75, right: 0.75 } }); fs.writeFileSync(dialog.showSaveDialogSync({ filters: [{ name: PDF, extensions: [pdf] }] }), pdf); }第二个坑是代码块的换行。默认情况下超长代码行在打印时会被表格容器截断或溢出页面非常难看。我通过给pre设置white-space: pre-wrap让代码自动折行同时用word-break: break-all兜底解决了一行长代码把 PDF 撑破的问题。另外如果你有“Markdown 转 Word”的需求我的建议是不要试图在这个阅读器里塞一个文档转换引擎。直接导出 PDF 后再丢给 Word/WPS 转或者用 Coze 这类自动化工作流把 Markdown 文本喂进去生成大纲、再转成 Word 文档效率高得多。阅读器只解决“看得舒服”的问题不解决“改得顺手”的问题。4. 打包发布与开源维护经验4.1 electron-builder 跨平台打包的四个坑开发完成之后最让人搓火的环节就是打包发布。我用了 electron-builder它算是最主流的打包方案但坑也多。这里分享四个我实际踩过的第一个坑是图标问题。打包 Windows 安装包时需要.ico格式的图标而如果你用在线转换工具把 PNG 转成 ICO经常在桌面快捷方式上看着正常、装完后任务栏里却显示不出图标。我折腾了一下午后来直接用 electron-builder 官方文档推荐的方式把 512x512 的 PNG 放到build/icons目录让它自动生成各尺寸问题才真正解决。第二个坑是安装包被 Windows Defender 误报。这是因为代码没有数字签名Windows 对未签名的 Electron 应用基本都会提示“未知发布者”。对个人开源项目来说买代码签名证书太贵了我最终的选择是暂时接受这个提示在 README 里写清楚“源码可查、自行构建也可”同时提供 SHA256 校验值让下载的人能自己验证文件完整性。第三个坑是Linux 打包依赖问题。如果你用 AppImage 打包在部分老版本 Ubuntu 上可能起不来原因是缺libfuse2。解决思路是在项目文档里明确列出运行依赖而不是每个平台都自己塞运行时进去。第四个坑是asar 归档导致资源读取失败。Electron 默认会把代码打成一个app.asar文件如果你在代码里用了__dirname去拼接资源路径路径会失效。正确做法是改用process.resourcesPath或者明确把静态资源放到extraResources里。这个坑很多新手会踩排查起来又特别隐蔽因为开发模式下一切正常一打包就崩。4.2 开源仓库打理GitHub 还是 Gitee许可证怎么选项目做完了发布到 GitHub 是必然的但国内用户访问 GitHub 有时候实在不稳定。我做了双仓库同步GitHub 作为主仓库Gitee 作为国内镜像。Gitee 上有“从 GitHub 导入仓库”的功能每次发布新版本时手动触发一次同步就行成本很低。关于开源许可证这个项目选的是 MIT。为什么选这个因为我希望它被用得越广越好不管是个人使用、学习参考、还是商业项目想改改抄抄都随便用。MIT 是最宽松的一类许可证用户只需要保留版权声明没有其他义务。如果以后项目活下来且用户量涨起来了可以再考虑换成 Apache 2.0额外提供专利保护或者 GPL要求衍生作品也必须开源。但对这类“工具型小项目”来说MIT 是最省心、最不吓人的选择。Gitee 后台创建仓库时直接选“MIT License”它还会自动生成许可证文件非常方便。另外开源项目一定要注意写清楚两件事一是贡献指南告诉别人如果要提 PRPull Request需要遵循什么规范比如代码风格、提交信息格式、测试要求二是行为准则说明什么样的对话和反馈是被允许的。开源项目最怕的不是没人用而是有人喜欢最后被“满口傲慢的技术指导型用户”搅得筋疲力尽。5. 常见问题与排查技巧实录5.1 表格复制出来为什么总是乱糟糟的有一个反馈特别多的问题在阅读器里看表格排版明明很好但想复制到 Excel 或 WPS 里时拷贝出来的内容却是一坨“黏糊糊的纯文本”没有解析成行列结构。这个问题其实是 HTML 表格本身的特点——浏览器复制表格到剪贴板时会根据系统能力决定是否带表格结构信息。有些系统复制出来是制表符分隔有些是 HTML 格式还有些是全糊的纯文本。我加了一个“复制为 Markdown”右键菜单。当你在表格区域点击右键可以选择“复制为 Markdown 源文本”这样就直接把原始的| col1 | col2 |这类文本复制到剪贴板。你再粘贴到 Excel 里选择“数据 → 分列 → 按分隔符拆分”选择竖线和可能需要的 trim就能恢复成一格一格的表格数据。5.2 图片不显示相对路径和本地文件的恩怨另一个高频问题是![图片](./images/a.png)这样的相对路径图片在编辑器里能看见但拿到阅读器里打开就变成裂图。原因也很简单阅读器加载本地文件时页面所在的环境并不是文件系统根目录相对路径“相对于”的对象经常是应用安装目录或者当前 URL 路径而不是 Markdown 文件所在的目录。我解决这个问题的方法是在读取文件时先把基础路径记下来const basePath path.dirname(filePath); // 在渲染前把相对路径全部替换成绝对路径 const htmlWithAbsolutePaths renderedHTML.replace( /src(\.\/|\.\.\/)(.*?)/g, (match, prefix, src) srcfile://${path.join(basePath, prefix src)} );同时要考虑到 Windows 路径带盘符和反斜杠的问题所以我统一用path.join处理后转成正斜杠再拼到file://协议后面。这个修复一旦处理好本地图片、附件、甚至 Markdown 里引用的本地 HTML 都能正常加载了。5.3 大文件卡顿与滚动性能优化有些用户会把整本书、整个框架的文档、几万行的 Changelog 塞进一个 Markdown 文件里。打开这种文件时如果一次性把所有内容都渲染成 DOM 节点页面会非常卡滚动掉帧严重。我做过一个粗略统计一个 2MB 大小的 Markdown 文件大约包含 50 万行文本全部渲染后 DOM 节点数量可能在 8 万到 15 万之间。对浏览器来说这个体量虽然不至于崩溃但滚动、选择、长列表重排都会明显变慢。我采用的方案是分段渲染先按空行把 Markdown 切分成多个区块一次只渲染当前视口附近的 5 个区块滚动时动态替换。这有点像瀑布流懒加载的思路实现不复杂但带来的体感提升非常明显。// 分段渲染的简化逻辑 const blocks rawText.split(/\n\n/); let currentIndex 0; function renderVisible(viewportTop) { const start findBlockIndexByOffset(viewportTop) - 2; const end findBlockIndexByOffset(viewportTop) 2; // 只渲染 [start, end] 范围的区块 renderRange(start, end); currentIndex start; }当然分段渲染也有副作用比如“查找”功能不能一次性遍历全部内容需要切换成逐块搜索。目前我遇到的大文件场景还比较少所以这个优化是放到了 v0.2 版本再上线的。5.4 安全提醒Markdown 里的 HTML 是一把双刃剑最后必须认真提醒所有准备做类似工具的朋友Markdown 允许写 HTML 这个特性既是它强大的原因也是它最危险的地方。我在解析器里默认开启了html: true这意味着用户写的script标签也会被原样保留。如果你只是在本机打开自己写的文件问题不大但如果哪天你把这个阅读器做成在线服务、让别人上传文件来预览恶意用户就能通过一个script标签在你的服务器上执行任意代码。这就是经典的存储型 XSS 攻击。我的应对措施是引入 DOMPurify 对渲染后的 HTML 做一次清洗const DOMPurify require(dompurify); const safeHTML DOMPurify.sanitize(renderedHTML, { USE_PROFILES: { html: true }, ADD_TAGS: [input], // 任务列表的勾选框 ADD_ATTR: [checked, type] });清洗之后script、iframe、object这类危险标签会被全部剔除只保留安全的排版标签和属性。这个操作对性能的影响很小一次性清洗一个中等文件大约几毫秒但对安全性是质的提升。6. 项目现状与后续方向思考目前这个项目已经在 GitHub 上开源LICENSE 是 MIT代码量保持在一个“普通开发者一个周末就能看完”的水平。支持 Windows、macOS、Linux 三平台核心功能包括本地 Markdown 渲染、目录导航、当前阅读位置高亮、代码高亮、多主题、自定义 CSS、PDF 导出、复制为 Markdown、大文件分段渲染。后续我个人比较想做的方向有三个第一个是增加多标签页支持。现在一次只能打开一个文件如果想对照看两个文档就得开两个窗口体验不太理想。多标签页本质上是维护一个“已打开文件列表”逻辑不难但要考虑文件内容变化后的自动刷新策略。第二个是做一个简单的文件树侧栏。不是导管理项目只是在你打开一个文件夹时列出目录结构让你点击其他.md文件时不用每次都走系统文件选择器。它依然不包含任何项目管理逻辑只是“目录预览”。第三个是支持导出为自包含 HTML。把样式、图片全部以 base64 嵌入到一个 HTML 文件里这样你可以把文档发给任何人对方双击就能在浏览器里查看不需要安装任何软件。这个功能对分享技术文档来说非常实用。至于要不要做一个对应的编辑器我的态度一直没变不做。市面上成熟的 Markdown 编辑器已经很多了把阅读器做好、做深比再做一个“平庸的全家桶”有价值得多。如果你也被那些越做越重的 Markdown 工具折腾得不耐烦完全可以把这个项目拿去自己改一版做成你理想中的样子。我开源它的初衷就是希望它成为一块砖、一个起点。每个人心里其实都有自己理想中的阅读器与其抱怨免费的不好用、付费的不值得不如干脆自己动手——程序员解决问题的方式永远是写一个工具出来。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。