【免费下载链接】mo
mo is a Markdown viewer that opens .md files in a browser.
mo是一款在浏览器中打开 Markdown 文件的 Markdown 查看器(Markdown viewer)。它最吸引人的地方,是整套系统——包括一个功能完整的React 单页应用(SPA)——最终只交付一个 Go 可执行文件:下载、运行,无需安装任何运行时。本文将带你拆解 mo 的架构:Vite 如何构建前端、go:embed如何把静态资源"藏进"二进制、Go HTTP 服务器又如何同时扮演 API 后端与前端静态资源托管者。
🧭 先认识 mo:一个 .md 文件的"浏览器"
你只需要一个命令,mo 就会在后台启动一个本地 HTTP 服务(默认端口6275),并自动打开浏览器渲染你的 Markdown:
- GitHub 风格 Markdown(表格、任务列表、删除线)
- Shiki 代码高亮、Mermaid 图表、KaTeX 数学公式
- 文件分组(Groups)、侧边栏平铺/树形视图、全文搜索
- 保存文件后浏览器自动刷新(Live-Reload)
用法示例可在 README.md 中看到,而项目自身的架构速览被记录在 CLAUDE.md 中。
🏗️ 架构全景:一个进程,三种角色
上图正是 mo 用自身渲染的一张架构 Mermaid 图:浏览器通过 HTTP 访问 GoServer,GoServer 一方面读取文件系统中的 .md 文件,另一方面内嵌(Embed)静态文件。
在 mo 的进程内部,三种角色合为一体:
| 角色 | 承担者 | 关键位置 |
|---|---|---|
| CLI 入口 | cmd/root.go(基于 Cobra) | 解析参数、管理后台进程 |
| API 后端 | internal/server/server.go | /_/api/...路由 + SSE 事件流 |
| 前端托管 | 内嵌的 React SPA | handleSPA() |
一个巧妙的设计:所有 API 都挂在/_/前缀下(如/_/api/status、/_/events),其余所有路径都交给 SPA 处理——这样用户自定义的分组名(如/design)永远不会和后端 API 冲突。
📦 构建阶段①:Vite 把 React 前端编译成静态文件
mo 的前端是一个标准的Vite + React 19 + TypeScript + Tailwind CSS v4单页应用,源码位于 internal/frontend/,依赖清单见 package.json(react-markdown、shiki、mermaid、katex等渲染能力都在这里)。
构建的关键配置在 vite.config.ts:
build: { outDir: "../static/dist", emptyOutDir: true,注意这个outDir——Vite 的产物并不输出到frontend/dist,而是直接写到旁边的internal/static/dist/。这不是巧合,而是为下一阶段的"嵌入"做准备。
React SPA 渲染的效果之一:树形侧边栏按目录层级展示 Markdown 文件,对应源码 TreeView.tsx 与 buildTree.ts。
🔩 构建阶段②:两行代码,把前端"装进"二进制
真正的魔法发生在 internal/static/static.go 中,整个文件只有两行有效代码:
//go:generate sh -c "cd ../frontend && pnpm install && pnpm run build" //go:embed all:dist var Frontend embed.FS这两行串起了前后端两个世界:
//go:generate:执行go generate时,先切到前端目录跑pnpm install && pnpm run build,即触发上面那套 Vite 构建;//go:embed all:dist:Go 编译器在go build时,把整个dist/目录打包进二进制的只读虚拟文件系统Frontend中。
Makefile 把这条流水线编排得非常简洁:
generate: go generate ./internal/static/ build: generate go build -ldflags=$(BUILD_LDFLAGS) -trimpath -o mo .也就是说,make build一条命令 = 构建 React 前端 + 编译 Go 后端 + 版本信息注入(ldflags写入 version/version.go)。最终产出的mo可执行文件里,已经包含了整个网站。
🚀 运行阶段:Go 服务器如何服务 React SPA
前端文件藏进二进制后,运行时只需"取出来"。handleSPA() 的逻辑只有两步:
- 先从内嵌文件系统
static.Frontend中切出dist子目录,交给标准的http.FileServer; - 请求命中真实静态文件(JS/CSS/HTML)就原样返回;否则一律回退到
index.html——这就是经典的 SPA 路由兜底,让用户刷新/design?file=xxx这类深链时也不会 404。
与此同时,同一台服务器还提供:
- 文件 API:
POST /_/api/groups/{group}/files添加文件、DELETE关闭文件等; - SSE 实时推送:文件保存后通过
/_/events推送file-changed事件,前端 useSSE.ts 收到后按文件 ID 重新拉取内容,实现"保存即刷新"。
单个 mo 服务可同时打开多个 .md 文件:左侧是 React 渲染的侧边栏,右侧正文由 Go 后端按需提供内容。
🎯 架构亮点:单实例设计让"多开"变"追加"
mo 默认只在6275端口跑一个服务器。当你第二次执行mo CHANGELOG.md时,probeServer() 会先探测端口:发现已有 mo 在运行,就通过 HTTP API 把文件"追加"到现有会话,而不是再开一个服务。
首次启动时,CLI 则走 startBackground():写出会话恢复文件后,拉起一个--foreground的子进程并使其脱离当前 Shell 常驻后台;--status、--shutdown、--restart都只是对运行中服务器发 HTTP 请求。
这套设计带来的直接好处:一个二进制、一个进程、一份状态,而会话状态还会自动备份到 XDG 目录(internal/backup/),重启服务器后浏览器里的文件列表原样恢复。
📝 总结:一张 mo 架构的"流水线"
React 源码 ──Vite──▶ dist/ 静态文件 ──go:embed──▶ 单个 mo 二进制 │ .md 文件 ──▶ Go HTTP 服务器 ◀─────────────┘ │ /_/api/* API + / SPA 兜底 ▼ 浏览器中的 React SPAmo 的架构回答了很多"CLI 工具 + Web 界面"项目面临的经典问题:
- 前端去哪了?→
go:embed直接打包进二进制,零额外部署; - API 和页面路由怎么共存?→
/_/前缀隔离 + SPA 兜底; - 如何优雅管理会话?→ 后台单实例 + HTTP 控制 + 状态备份。
如果想动手验证,克隆仓库(git clone https://gitcode.com/gh_mirrors/mo7/mo)后执行make dev即可在本地体验完整构建链路(详见 CLAUDE.md)。对于想学习"Go + 前端混合应用"打包方案的同学,mo 是一个麻雀虽小、五脏俱全的完整样本。
【免费下载链接】mo
mo is a Markdown viewer that opens .md files in a browser.
相关推荐
终极指南:如何用Bud框架将全栈Go应用打包为单一可执行文件
终极指南:如何用Bud框架将全栈Go应用打包为单一可执行文件 想要将你的Go全栈应用打包成一个独立的可执行文件吗?Bud框架正是你需要的解决方案!Bud是一个专
后端PyOxidizer终极指南:如何一键打包Python应用为单个可执行文件
PyOxidizer终极指南:如何一键打包Python应用为单个可执行文件 PyOxidizer是一款革命性的Python应用打包和分发工具,它能够将整个Pyt
开发工具CLI终极指南:如何将Node.js应用打包成单个可执行文件
终极指南:如何将Node.js应用打包成单个可执行文件 🚀 想要轻松分发Node.js应用而无需用户安装Node.js环境吗? Nexe工具正是您需要的解决方
构建工具开发工具CLI
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考