返回项目列表
MD2PDF 标志

MD2PDF

Markdown 编辑渐进式 Web 应用 (PWA),支持通过浏览器渲染引擎直接导出 PDF。

+1k 日访问量MarkdownPDFPWA

我设计该系统旨在作为嵌入在 Astro 静态环境中的单页应用 (SPA) 运行,提供持久的本地存储、滚动同步、渐进式 Web 应用 (PWA) 支持,以及直接通过浏览器渲染引擎实现的流畅 PDF 导出工作流。

业务上下文与系统实用性

构建此应用时,我的主要目标是解决技术写作与文档发布之间的常见阻力,且不依赖于云端基础设施或第三方数据库。我默认提供私密环境,这确保了敏感数据(如公司内部文档草稿或架构图)孤立保存在用户的本地机器上。

核心工作流 (Workflow) 按以下方式运行:

  1. 初始化: 加载时,我通过 Dexie.js 从 IndexedDB 检索文档列表和应用状态。如果没有文档,我会用结构化示例初始化数据存储。
  2. 数据输入与操作: 用户与 Monaco Editor 界面交互,该界面原生拦截键盘事件、格式结构,甚至是剪贴板粘贴的图像。
  3. 内存优化 (In-Memory): 我在事件传播前拦截粘贴的图像,通过内部 canvas 将最大宽度限制为 800 像素进行调整大小,重新编码以提高效率,并使用适当的 MIME 类型(如 WebP 或 JPEG)异步保存到浏览器的存储系统 (IndexedDB) 中。
  4. 实时渲染: 当文本模型发生变化时,我使用 Zustand(状态管理器)触发文档预览的更新。React-Markdown 处理抽象语法树 (AST) 以渲染数学代码 (KaTeX)、语法高亮 (highlight.js) 和矢量图形 (Mermaid.js)。
  5. 原生导出: 我没有依赖服务端重型 PDF 渲染引擎,而是显式调整文档 DOM 样式树以适应打印视图,注入隐藏 UI 的 CSS 设置,并发出 window.print() 指令,充分利用浏览器原生 PDF 引擎的高保真度。

整体架构分析

我按照高度不对称的混合架构组织了该项目,其中服务端或构建层仅负责提供“外壳”(Shell),将 100% 的业务处理下放给客户端。

architecture-beta
    group client(cloud)["客户端 / Web 浏览器"]

    service astro(server)["Astro Shell"] in client
    service react(server)["React SPA"] in client
    service storage(database)["IndexedDB / Dexie"] in client

    astro:L -- R:react
    react:B -- T:storage

结构模式

  1. Astro 作为 Shell Host: 我使用 Astro 层 (src/pages/index.astro 及 Layouts) 提供静态入口、PWA 全局配置 (vite-plugin-pwa) 和用于指标的 Vercel Analytics SDK 集成,从而避免在静态启动时水化重型框架。
  2. 内部 React 微前端: React 根容器 (src/components/App.tsx) 作为交互核心。
  3. 单向数据流: 遵循类似于应用于 Frontend 的 Clean Architecture 模式,我使用单一事实来源仓库 (Zustand) 管理状态,视图 (React) 仅反映状态变化,用户事件派发变更(动作),我将其整合并插入到持久化层 (Dexie.js) 中。

数据建模与事件逻辑

我通过 IndexedDB API 实现持久化,通过 Dexie.js 封装库进行编排,以获得可靠的异步 Promise。

erDiagram
    DOCUMENT {
        number id PK
        string title
        string content
        date updatedAt
    }
    IMAGE {
        number id PK
        blob blobData
        string mimeType
        date createdAt
    }
    DOCUMENT ||--o{ IMAGE : "通过 markdown 引用 local-image://{id}"

实体与关系流

  • Document 实体: 作为 Markdown 格式纯文本的存储库。
  • Image 实体: 作为二进制本地 Blob Store。与其直接在 Markdown 中嵌入 base64(这会严重减慢编辑器的 AST),不如在 Monaco 中拦截二进制数据的粘贴,通过 Canvas API 缩减其尺寸,将其注入 IndexedDB,并生成自定义内部 URI(如 local-image://12)。
  • 视觉生命周期拦截: 包装组件 (MarkdownImage 位于 src/components/Preview.tsx) 拦截 Markdown 解析器生成的图像请求,如果前缀对应于 local-image://,它会异步向 IndexedDB 请求 Blob,并使用 URL.createObjectURL() 创建临时 URL。一旦组件卸载,对象即被撤销,从而保护了浏览器的内存管理。

组件同步

sequenceDiagram
    participant User as 用户
    participant Editor as Monaco Editor
    participant Zustand as Store
    participant Dexie as IndexedDB
    participant Preview as 预览面板

    User->>Editor: 输入 Markdown
    Editor->>Zustand: updateCurrentDocument(content)
    Zustand->>Dexie: put(updatedDoc)
    Zustand-->>Preview: React State Change
    Preview->>Preview: 解析 Markdown AST
    Preview-->>User: 渲染界面

技术栈与工具角色

我对技术环境的选择优先考虑主线程的纯粹性能、客户端隔离和排版标准化。

技术类别具体角色
AstroShell 框架 / PWA构建基础网站的静态环境,并通过 Vite 编排本地 PWA 支持。
React 19界面与 UI 层管理编辑器与预览面板的响应性,包括用于虚拟化和 DOM 控制的 ref-hooks。
TypeScript主要语言数据库模型与全局状态的接口契约。
Zustand状态管理允许组件订阅特定片段的状态以减少重新渲染,并使用 LocalStorage 作为初始 fallback 水化配置。
Dexie.js数据持久化为带有关系 Schema 控制的 IndexedDB 提供基于 Promise 的接口。
Monaco Editor编辑引擎提取自 VS Code 的文本引擎,支持语法、缩略图和深层事件。
React-MarkdownAST 编译器转换原始文本,支持 remark-gfm(表格)和 remark-math(KaTeX 支持)插件。
Mermaid.js代码绘图在预览容器中解析特定语法块并渲染为 SVG 图形,与主题绑定。
Tailwind CSS v4样式引擎管理布局并应用 @tailwindcss/typography 的 .prose,在打印视图中通过 print:!w-full 规则进行条件操纵。
BiomeJSToolchain Linter在代码分析阶段替代 Prettier/ESLint,确保语法一致性。

架构影响与可打印性设计

该架构的核心成功在于其打印模式的设计 (Print Mode Override)。我没有依赖行业标准的依赖包 (react-to-print),而是短暂劫持了 React 的生命周期:

调用导出时,我在 Store 中注入同步的 "light" 状态,强制 React 和 Mermaid.js 在纯对比度下渲染整个 DOM 树,且没有暗色背景。setTimeout 计算 DOM 重绘的精确周期(约 1500ms),我调用 window.print() 对话框(临时停止 JS 引擎),在收到控制权返回时,如果之前启用了暗色主题,则立即恢复暗色主题。这结合通过 @media print 样式表重置边距,允许干净、精确且无双重边框的原生 PDF 导出。

拖动可移动图示。按 + 和 − 缩放,方向键移动,0 重置视图。

一起合作

软件工程师、高级全栈开发工程师,常驻玻利维亚Santa Cruz,远程办公。欢迎新的工作机会。