用 Rust 复刻了掘金的 Markdown 阅读体验,做了个纯阅读器

一句话:MD Reader 是一个用 Rust 写的本地 Markdown 阅读器------不做编辑,只把"读"这件事做到舒服。界面版式参考稀土掘金的 Markdown 预览。


一、为什么要写这个

先说我的真实使用场景,可能和你很像。

我平时写文档、记笔记都是 Markdown,但读 这些文档的时候一直很别扭:我的做法是把 .md 的内容整段复制到稀土掘金的写文章页面,靠右边那个预览窗口来读。听起来很绕对吧?但那个预览区的排版确实好看------中英文混排舒服、标题层级清晰、代码块有高亮、表格不挤。

我也试过 VS Code。它能读 Markdown,功能也强,但界面是编辑器逻辑:左侧资源管理器、顶部面包屑、行号、源码符号、右侧小地图......这些在"写"的时候是助力,在"读"的时候全是干扰。通读一篇长文档时,我需要的是一块干净的"纸",而不是一个 IDE。

所以需求其实很朴素:

  1. 一个双击就能打开的本地 .md 阅读器;
  2. 排版要像掘金那样好看;
  3. 只做阅读------不要编辑、不要工作区、不要插件;
  4. 用 Rust 写。

最后一项一半是兴趣,一半是认真考虑过:Rust 编译出的是单个 exe,没有运行时依赖,启动快,内存占用小,适合这种常驻后台、偶尔切过去看一眼的小工具。

于是有了 MD Reader。目前 v0.1.0 已经发布,Windows 版是一个 3.9 MB 的单文件 exe,解压即用。


二、它长什么样,能做什么

阅读体验

  • 掘金风格排版 :居中的白色"纸张"、灰底背景;标题带分隔线、引用带左侧色条、表格有表头底色、任务列表是可爱的圆角复选框。字体栈针对中英文混排做了处理(PingFang SC / Microsoft YaHei 优先)。
  • 滚动目录:自动抽取 H1--H4,滚动时高亮当前章节,可按标题过滤,点击平滑跳转。
  • 代码高亮:syntect 支持 200+ 语言,亮 / 暗两套配色,可选行号,右上角一键复制。
  • 阅读进度:顶部一条细进度条;每篇文档记住你读到哪,下次打开自动回到那个位置。
  • 专注模式 :Ctrl+Shift+F 把顶栏和侧栏全藏起来,屏幕上只剩那张"纸"。
  • 统计信息 :标题栏直接显示 792 字 · 约 2 分钟 · 7 节 · 2 段代码 · 1 张图。

文档库:把"读过的东西"管起来

这是我后来才想清楚的一个点。写完第一版后我习惯性地把"最近打开"扔在右上角的下拉菜单里------用起来才发现根本想不起来点 。后来看了看 Obsidian、VS Code、Kindle 的做法,它们的共同点是:读过的东西是一个常驻的书架,带进度和时间。

所以第二版把它挪到了侧边栏,做成"文档库":

  • 继续阅读:按最近阅读排序,每条右侧一个 16px 的小圆环显示进度(悬停才变清晰,不抢视线)、相对时间(刚刚 / 3 分钟前 / 2 天前)、预计读完时长
  • 固定:常用文档加星置顶
  • 移除 / 清空:只出库,不动磁盘文件
  • Ctrl+P 快速切换:模糊搜索已读文档 + 当前文件夹里的文档,↑↓ 选择、Enter 打开

输出:写作者最可能用得上

  • Ctrl+Shift+C 复制为富文本 :把渲染结果(含代码高亮配色)以 CF_HTML 写进剪贴板,同时写一份 Markdown 源码作为纯文本。直接粘到掘金编辑器里,标题、表格、代码高亮都在------这正好解决了我最初"把 md 复制到掘金"的绕路问题。
  • Ctrl+S 导出自带样式的独立 HTML
  • Ctrl+Shift+P 打印 / 导出 PDF

一些"应该有的"细节

  • 双击打开 :设置里一键关联 .md / .markdown(写 HKCU,不需要管理员权限)
  • 单实例:再启动一次会把文件交给已运行的窗口,不会开第二个
  • GBK 兼容:自动识别 UTF-8 / UTF-16 / BOM,UTF-8 解析失败时回退 GBK、Big5------中文 Windows 上的老文档不会整篇乱码
  • 安全:默认开启 HTML 消毒,过滤脚本与事件属性;本地图片只允许加载已打开目录下的文件

三、下载与使用

下载

Windows 用户直接下这个 zip:

bash 复制代码
https://gitee.com/zlin151/md_reader/releases/download/v0.1.0/md_reader-v0.1.0-windows-x64.zip

解压后双击 md_reader.exe 就能用,不需要安装,也不写注册表(除非你主动点"关联 .md 文件")。

依赖:Windows 10/11 自带 WebView2。如果是较老的 Windows,先装 WebView2 Runtime。

其他平台目前需要自己编译(cargo build --release),CI 里有 macOS 和 Linux 的构建配置,但还没在真机上完整验证过,欢迎试。

打开文档的四种方式

  1. 双击 .md(先关联)→ 直接打开
  2. 把文件拖进窗口
  3. md_reader 文档.md 命令行
  4. Ctrl+O 或 Ctrl+P 快速切换

快捷键

快捷键 功能
Ctrl + O 打开文件
Ctrl + P 快速切换文档
Ctrl + F 页内查找
Ctrl + R 重新加载(编辑器里改完自动刷新也行)
Ctrl + T 切换亮色 / 暗色
Ctrl + B 显示 / 隐藏侧边栏
Ctrl + Shift + F 专注阅读
Ctrl + Shift + C 复制为富文本
Ctrl + S 导出 HTML
Ctrl + Shift + P 打印 / 导出 PDF
Ctrl + PageUp / PageDown 上一篇 / 下一篇(同目录)
Alt + ← 返回上一个跳转位置

四、技术选型

领域 选择 理由
窗口 tao 跨平台,与 wry 同源
WebView wry 只想要 WebView + IPC,Tauri 整套打包与插件体系太重
Markdown pulldown-cmark CommonMark 完整,事件流便于中途改写
代码高亮 syntect Sublime 语法与主题生态,配色质量好
HTML 消毒 ammonia Rust 生态最成熟的白名单清洗
文件监听 notify 跨平台统一
编码 encoding_rs Firefox 同款,GBK / Big5 支持完善
剪贴板 arboard 支持 CF_HTML 富文本
文件对话框 rfd 系统原生,无 GTK 依赖

一个刻意的决定:前端不引入任何构建工具 。整个 UI 就是 index.html / style.css / app.js 三个手写文件,用 include_str! 编译进二进制,运行时通过自定义协议提供给 WebView。没有 npm、没有打包器、没有框架。好处是构建快、产物小、改完即生效;代价是要手写 DOM 操作,不过这个 UI 的复杂度完全撑得住。

最终产物:Rust 侧约 3200 行,前端约 2700 行,单个 exe 3.9 MB。


五、架构

arduino 复制代码
┌──────────────────────────────────────────────┐
│ 前端(assets/,编译进二进制)                 │
│   index.html · style.css · app.js            │
└──────▲───────────────────────────▲───────────┘
       │ ipc.postMessage           │ evaluate_script
┌──────┴───────────────────────────┴───────────┐
│ 宿主(Rust)                                  │
│   main.rs     窗口 / WebView / 协议 / 事件循环 │
│   app.rs      状态机:打开、渲染、设置、导出...  │
│   markdown.rs 渲染引擎                        │
│   config.rs   配置与文档库                    │
│   辅助:encoding / sanitize / scan / instance │
│        / assoc / clipboard / logging / cli    │
└──────────────────────────────────────────────┘

打开一篇文档的完整链路:

arduino 复制代码
双击 / 拖拽 / Ctrl+O
   → UserEvent::OpenPath
   → 路径规范化 → 编码探测(BOM → UTF-8 → GBK → Big5)
   → markdown::render(解析 + 代码高亮 + 目录 + 消毒)
   → 记入文档库 → 监听所在目录
   → evaluate_script("MDReader.renderDoc(...)")
   → 前端注入 HTML、构建目录、恢复上次阅读位置

反向:前端用 window.ipc.postMessage(JSON) 发命令,Rust 端转成 UserEvent::Command 交给 app.handle()。所有逻辑都在事件循环线程上跑,webview 不需要跨线程共享------这一点省掉了大量加锁的麻烦。


六、几个值得说的实现细节

1. 前端资源编译进 exe

assets/ 三个文件用 include_str! 嵌入,运行时由自定义协议 wry://localhost/... 提供。所以发布产物只有一个 exe,不用附带任何文件。

这里踩了个坑,值得记一下 :Windows 上 WebView2 不支持自定义 URL scheme,wry 会悄悄把 wry://localhost/index.html 改写成 http://wry.localhost/index.html。我一开始写的导航处理器判断"以 http 开头就是外链",结果把自己的首页当成外链拦掉了------表现是页面白屏,只有 index.html 被请求,CSS 和 JS 都没加载。

排查方式是让程序每隔一段时间用 evaluate_script_with_callback 回传前端状态(后来干脆固化成 MD_READER_PROBE=1 的自检开关)。修法是:

rust 复制代码
fn is_internal(url: &str) -> bool {
    match url.split_once("://") {
        Some(("wry", _)) => true,
        Some((_, rest)) => rest.starts_with("wry.localhost"),  // Windows 上的改写形态
        None => false,
    }
}

顺带一个推论:文档里生成的本地图片链接不能写死 wry:// 前缀(在 Windows 上命中不了拦截器),统一用相对地址 /img/?p=... 最稳。

2. 渲染引擎:在事件流上做改写

没有直接用 pulldown_cmark::html::push_html 一把梭,而是先把事件收集成 Vec<Event>,再在三个点做改写:

  • 标题 :先取出内部文本 → 生成稳定 slug(保留中文,如 标题一)→ 注入 id 与锚点 → 收集进目录
  • 代码块 :拦截 CodeBlock 起止事件,把内部文本交给 syntect,包成带语言标签和复制按钮的结构
  • 链接 / 图片 :指向本地 .md 的链接改写成内部地址(点击在阅读器里直接打开);本地图片改写成 /img/?p=<绝对路径>

最后按配置决定是否过一遍 ammonia。

3. 高亮结果缓存

主题切换、文件热重载都会触发重新渲染,如果每次都重跑 syntect,几 MB 的文档会明显卡。所以按 (主题, 行号, 代码哈希, 语言) 缓存高亮结果,超过 256 条就清空。切换主题时基本是瞬时的。

4. 编码探测

rust 复制代码
BOM(UTF-8 / UTF-16LE / UTF-16BE)
  → 严格 UTF-8 能解?
  → 否则 GBK
  → 否则 Big5
  → 最后才 lossy

顺序很重要:GBK 的中文在 UTF-8 下几乎必然解析失败,反之 UTF-8 中文用 GBK 解会变成一堆乱码字。先试严格 UTF-8 能同时兼顾正确性和兼容性。

5. 单实例

%APPDATA%/md_reader/instance.lock 当心跳文件,主实例每 2 秒更新一次时间戳。第二个实例发现心跳存活(6 秒内有更新)就把文件路径追加到 pending_open.txt 然后退出;主实例监听这个文件并打开。锁陈旧则抢占。没有用 socket 或命名管道,因为只需要传一个路径,文件够用了。

6. 图标是代码画出来的

不想引二进制资源文件,就在 src/icon.rs 里用几何运算画了一个圆角卡片 + 三行文本的图标(3×3 超采样抗锯齿),生成 16/32/48/256 四尺寸的 ICO。build.rs 用 include! 复用同一份代码,再调 rc.exe 嵌进 exe 并写入版本信息资源;找不到资源编译器就跳过,不影响正常编译。


七、项目结构

css 复制代码
src/
  main.rs        窗口 + WebView + 自定义协议(资源与图片白名单)+ IPC
  app.rs         状态机:打开、渲染、设置、导出、搜索、前后篇
  markdown.rs    渲染引擎:锚点、目录、高亮(缓存)、行号
  config.rs      配置与文档库持久化
  encoding.rs    编码探测     sanitize.rs  HTML 消毒
  scan.rs        文件树与全文搜索
  instance.rs    单实例        assoc.rs    文件关联
  clipboard.rs   富文本复制    logging.rs  日志
  cli.rs         命令行        icon.rs     图标绘制
assets/          index.html / style.css / app.js

数据与配置都放在 %APPDATA%/md_reader/:config.json(含文档库与阅读进度)、logs/app.log、单实例锁文件。


八、还没做的

坦诚列一下当前的限制:

  • 一次只读一篇,没有多标签和并排视图
  • 不支持数学公式与 Mermaid 图
  • Linux 依赖写进了 CI,但没在真机上验证过
  • 自动更新只是打开发行版页面,不做下载安装

排在后面的计划:双列 / 分页阅读 、摘录与批注(选中即存、可导出,让"读过的东西"真正沉淀下来)、按标题跳转(上一节 / 下一节)、护眼纸色主题、自动滚动。


九、最后

如果你也有"写好的 Markdown 要找个舒服地方通读一遍"的需求,可以试试:

代码是 MIT 许可,随便看、随便改。有用的话给个 Star,遇到 bug 或者想要什么功能,直接提 Issue 就好------尤其是"排版哪里不好看"这种反馈,我最需要。

相关推荐
rannn_1113 小时前
JVM 面试题:类加载过程详解(附高频考点)
java·jvm·后端
程序猿乐锅6 小时前
从0-1一文详解RabbitMQ
java·分布式·后端·中间件·rabbitmq·ruby
考虑考虑9 小时前
JDK26中的List.ofLazy()
java·后端·java ee
小蒜学长10 小时前
基于SpringBoot的公寓报修管理系统的设计与实现(代码+数据库+LW)
java·spring boot·后端·公寓报修管理系统·多角色协同
云浪10 小时前
Go源码分析:搞懂 Go 是如何实现堆的
后端·go·源码阅读
调试人生的显微镜13 小时前
iOS开发入门:Interface Builder、基础控件及UITextField详解
后端·ios
泡海椒13 小时前
巡检照片与工单附件:jquick-pdf 图片嵌入的业务实战
后端
蜗牛互联网13 小时前
Python消费Responses SSE事件:增量文本、超时与取消
java·开发语言·人工智能·后端·python