一句话:MD Reader 是一个用 Rust 写的本地 Markdown 阅读器------不做编辑,只把"读"这件事做到舒服。界面版式参考稀土掘金的 Markdown 预览。
- 项目地址:gitee.com/zlin151/md_...
- 下载地址:gitee.com/zlin151/md_...
- Windows 直链:gitee.com/zlin151/md_...
一、为什么要写这个
先说我的真实使用场景,可能和你很像。
我平时写文档、记笔记都是 Markdown,但读 这些文档的时候一直很别扭:我的做法是把 .md 的内容整段复制到稀土掘金的写文章页面,靠右边那个预览窗口来读。听起来很绕对吧?但那个预览区的排版确实好看------中英文混排舒服、标题层级清晰、代码块有高亮、表格不挤。
我也试过 VS Code。它能读 Markdown,功能也强,但界面是编辑器逻辑:左侧资源管理器、顶部面包屑、行号、源码符号、右侧小地图......这些在"写"的时候是助力,在"读"的时候全是干扰。通读一篇长文档时,我需要的是一块干净的"纸",而不是一个 IDE。
所以需求其实很朴素:
- 一个双击就能打开的本地
.md阅读器; - 排版要像掘金那样好看;
- 只做阅读------不要编辑、不要工作区、不要插件;
- 用 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导出自带样式的独立 HTMLCtrl+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 的构建配置,但还没在真机上完整验证过,欢迎试。
打开文档的四种方式
- 双击
.md(先关联)→ 直接打开 - 把文件拖进窗口
md_reader 文档.md命令行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 就好------尤其是"排版哪里不好看"这种反馈,我最需要。