导出功能实现
功能范围
当前导出功能支持把 Markdown 笔记导出为以下格式:
- PDF:
.pdf - Word:
.docx - Markdown:
.md - HTML:
.html - 图片:
.png
用户可以在设置中配置启用的导出格式、默认导出目录、是否在导出后打开文件所在目录。默认导出格式为 PDF,默认导出目录策略为和原文件同一目录。
整体链路
导出入口在渲染进程触发,例如文件树右键菜单点击“导出”。渲染进程通过 preload 暴露的安全 API 调用主进程 IPC,主进程读取当前导出配置后执行真实文件系统导出。
主流程如下:
- 渲染进程触发导出文件事件。
- preload 调用
IPC_CHANNELS.EXPORT.FILE。 - 主进程
registerExportIpc接收文件路径。 exportConfigManager.getConfig()读取当前导出配置。exportFile(filePath, config)读取 Markdown 文件并生成目标格式。- 导出成功后返回
directoryPath和filePaths,渲染进程展示 toast。 - 如果配置了导出后打开目录,主进程通过
shell.openPath打开导出目录。
核心文件:
src/main/ipc/export.ipc.ts:导出相关 IPC 注册。src/main/export-config.ts:导出配置加载、保存和默认值合并。src/main/export-service.ts:导出文件生成逻辑。src/shared/types/index.ts:导出格式、配置和结果类型。
关键实现
统一 Markdown 渲染源
除 Markdown 原文导出外,PDF、Word、HTML、图片都先把 Markdown 渲染为同一份 HTML 文档,再基于这份 HTML 生成目标格式。
这样可以避免不同格式各自实现一套解析逻辑,导致表格、中文、加粗、列表等内容表现不一致。
当前使用 markdown-it 渲染 Markdown:
const markdownRenderer = new MarkdownIt({
html: false,
linkify: true,
typographer: true,
breaks: true,
});
html: false 用于禁止 Markdown 原文中的 HTML 直接注入,降低导出时执行或保留非预期 HTML 的风险。
HTML 文档生成
createHtmlDocument 会生成完整 HTML:
<!doctype html><meta charset="utf-8" />lang="zh-CN"- 内联导出样式
- Markdown 渲染后的正文
导出样式中处理了标题、段落、列表、引用、代码块、图片和表格。表格使用 border-collapse: collapse,并设置 th、td 的边框和内边距,让 HTML、PDF、Word 和图片导出都尽量保持一致。
PDF 导出
PDF 不再手写 PDF 字符串,而是通过 Electron 的 Chromium 渲染能力生成:
- 创建隐藏的
BrowserWindow。 - 用
loadURL(data:text/html;charset=utf-8,...)加载 HTML。 - 调用
window.webContents.printToPDF输出 PDF Buffer。 - 用
writeFile写入.pdf文件。
这种方式解决了手写 PDF 时常见的问题:
- 中文乱码。
- 表格布局不完整。
- Markdown 源码被当作普通文本导出。
- CSS 样式无法复用。
PDF 当前设置 printBackground: true,保证表格背景色等样式能被输出;页面边距使用 marginType: "none",减少额外空白。
Word 导出
Word 使用 html-to-docx:
- 复用 Markdown 渲染后的 HTML。
- 调用
htmlToDocx(html, undefined, options)。 - 输出 Buffer 并写入
.docx。
配置中设置了:
lang: "zh-CN"font: "Microsoft YaHei"table.row.cantSplit: true
这样比保存 .doc 格式的 HTML 更接近真实 Word 文档,也更利于在 Microsoft Word、WPS、LibreOffice 等工具里打开。
图片导出
图片导出基于 Electron 截图能力:
- 隐藏
BrowserWindow加载 HTML。 - 在页面内执行 JavaScript,计算
document.body.children的真实内容边界。 - 根据内容矩形调整窗口尺寸。
- 调用
window.capturePage(rect)截取内容区域。 - 用
NativeImage.toPNG()写入.png。
这里没有直接截整个窗口,而是裁剪真实内容区域,避免把页面多余空白也导出到图片中。
Markdown 导出
Markdown 格式保持原文输出,不做渲染转换。这样用户选择 md 时得到的是原始 Markdown 文件副本。
文件命名策略
导出文件默认使用源文件名作为基础名。例如 daily.md 导出为:
daily.pdfdaily.docxdaily.htmldaily.pngdaily.md
如果目标目录中已存在同名导出文件,则整批导出使用副本名:
- 第一次冲突:
daily-副本1.pdf - 第二次冲突:
daily-副本2.pdf - 以此类推
命名冲突判断是按本次启用的所有格式一起检查。只要任意一个目标格式文件已存在,就切换到下一个副本名,避免同一批导出里有的覆盖旧文件、有的使用新文件名。
配置持久化
导出配置由 ExportConfigManager 负责:
- 配置文件存储在
app.getPath("userData")/export-config.json。 - 加载时会兼容旧版本缺失字段。
- 保存时会同步内存状态并写入用户数据目录。
- 自定义导出目录默认使用系统下载目录,避免首次选择自定义时为空路径。
配置结构:
interface ExportConfig {
enabledFormats: ExportFormat[];
defaultDirectoryMode: "same-as-source" | "custom";
customDirectoryPath: string;
openDirectoryAfterExport: boolean;
}
使用的技术和 API
第三方库:
markdown-it:Markdown 到 HTML 渲染,支持表格、链接、强调、列表等常见语法。markdown-it先把 Markdown 解析渲染成 HTML
html-to-docx:HTML 到.docx转换。
Electron API:
ipcMain.handle:注册导出配置和导出文件 IPC。BrowserWindow:创建隐藏窗口渲染导出 HTML。webContents.printToPDF:生成 PDF。webContents.executeJavaScript:计算图片导出时的真实内容区域。BrowserWindow.capturePage:截取渲染内容生成图片。shell.openPath:导出后打开目录。app.getPath("downloads"):自定义目录默认值。app.getPath("userData"):导出配置持久化目录。
Node API:
fs/promises.readFile:读取源 Markdown。fs/promises.writeFile:写入导出文件。fs/promises.mkdir:创建导出目录。fs/promises.access:判断目标文件是否存在。path.basename、dirname、extname、join:路径和文件名处理。
难点和处理方式
中文乱码
早期手写 PDF 时只能简单写入 ASCII 友好的内容,中文会出现乱码或被替换。现在改为 Chromium 渲染 HTML 后 printToPDF,由浏览器字体和 PDF 管线处理中文编码。
表格导出不完整
手写 Markdown 解析无法完整处理 Markdown 表格,容易把表格源码直接导出。现在统一使用 markdown-it 渲染表格,再复用 HTML 生成 PDF、Word、图片。
空表头导致多余空白行
部分 Markdown 表格只有分隔行和数据行,没有实际表头内容,markdown-it 会生成空 <thead>。导出前会检测“所有表头单元格都为空”的情况,并移除该空表头,避免导出时多一行空白表头。
图片导出包含多余边距
直接截取整个窗口会把页面空白一起导出。现在通过 getBoundingClientRect 计算正文子元素的合并边界,再用 capturePage(rect) 截取真实内容区域。
多格式导出命名冲突
导出多种格式时,如果只按单个文件判断冲突,可能出现同一批导出中部分文件覆盖、部分文件改名。现在先为整批格式计算一个可用基础名,再统一写入,保证一批导出文件命名一致。
主进程和渲染进程边界
文件系统读写、目录打开、PDF 和图片渲染都放在主进程执行。渲染进程只通过 preload 暴露的窄 API 触发导出,避免直接暴露 Node 文件系统能力。
测试覆盖
当前导出服务测试覆盖:
- Markdown、HTML、PDF、Word、图片的文件输出路径。
- HTML 中中文、加粗、列表和表格的渲染结果。
- Markdown 原文格式保持不变。
- Word 输出
.docx文件。 - 图片导出按内容区域裁剪。
- 重复导出时生成
-副本1命名。 - 空表头 Markdown 表格导出时移除空
<thead>。
相关测试文件:
src/main/export-service.test.tssrc/renderer/src/features/export/components/export-controller.test.tsxsrc/renderer/src/features/export/components/export-success-toast.test.tsxsrc/renderer/src/features/settings/components/settings-modal.test.tsx