# VitePress Exporter 执行流程 ## 插件概述 VitePress Exporter 是一个 Obsidian 插件,用于将当前笔记导出到 VitePress 文档系统,包括资源文件管理和 Canvas 渲染功能。 ## 完整执行流程图 ```mermaid flowchart TD Start([用户触发导出]) --> CheckActiveFile{检查当前文件} CheckActiveFile -->|没有活动文件| ShowError1[显示错误提示] CheckActiveFile -->|不是 md 文件| ShowError2[显示错误提示] CheckActiveFile -->|是 md 文件| ReadFile[读取文件内容
使用 binary 模式] ReadFile --> ProcessAssets[处理资源文件
processAssets] ProcessAssets --> GetCache[获取文件元数据缓存
metadataCache.getCache] GetCache --> CheckCache{缓存是否存在} CheckCache -->|否| ReturnContent[返回原内容] CheckCache -->|是| InitializeSets[初始化集合
assetsToCopy, canvasFiles] InitializeSets --> ScanEmbeds[扫描嵌入文件
cache.embeds] ScanEmbeds --> CheckEmbed{有嵌入文件?} CheckEmbed -->|否| ScanLinks CheckEmbed -->|是| ResolveEmbed[解析嵌入路径
getFirstLinkpathDest] ResolveEmbed --> CheckCanvas{是 canvas 文件?} CheckCanvas -->|是| AddToCanvasSet[添加到 canvasFiles] CheckCanvas -->|否| AddToAssetsSet[添加到 assetsToCopy] AddToCanvasSet --> ScanEmbeds AddToAssetsSet --> ScanEmbeds ScanEmbeds --> ScanLinks[扫描链接文件
cache.links] ScanLinks --> CheckLink{有链接文件?} CheckLink -->|否| ProcessCanvasFiles CheckLink -->|是| ResolveLink[解析链接路径
getFirstLinkpathDest] ResolveLink --> CheckMD{是 md 文件?} CheckMD -->|是| SkipLink[跳过] CheckMD -->|否| AddToAssetsSet SkipLink --> ScanLinks AddToAssetsSet --> ScanLinks ScanLinks --> ProcessCanvasFiles[处理 Canvas 文件] ProcessCanvasFiles --> CheckCanvasEmpty{有 Canvas 文件?} CheckCanvasEmpty -->|否| ProcessAssetFiles CheckCanvasEmpty -->|是| GetCanvasFile[获取 Canvas 文件对象] GetCanvasFile --> CaptureCanvas[captureCanvas] CaptureCanvas --> OpenFile[打开 Canvas 文件
activeLeaf.openFile] OpenFile --> GetCanvas[获取 Canvas 对象
activeLeaf.view.canvas] GetCanvas --> CheckCanvasObj{Canvas 对象存在?} CheckCanvasObj -->|否| NextCanvas CheckCanvasObj -->|是| ExportInternalAPI[exportCanvasWithInternalAPI] ExportInternalAPI --> GetBBox[获取视口边界框
getViewportBBox] GetBBox --> SetZoom[设置 zoom = 0
100% 缩放] SetZoom --> RequestFrame[请求重绘
requestFrame] RequestFrame --> WaitRender[等待渲染
100ms + rAF] WaitRender --> TakeScreenshot[调用 takeScreenshot
使用绝对路径] TakeScreenshot --> WaitWrite[等待文件写入
100ms] WaitWrite --> ReadTemp[读取临时文件
readBinary] ReadTemp --> CheckBuffer{读取成功?} CheckBuffer -->|否| LogError1[记录错误] CheckBuffer -->|是| RemoveTemp[删除临时文件] RemoveTemp --> WriteFinal[写入最终位置
writeBinary] WriteFinal --> UpdateCanvasLinks[更新 Canvas 链接
updateCanvasLinksInContent] UpdateCanvasLinks --> NextCanvas{还有 Canvas?} LogError1 --> NextCanvas NextCanvas -->|是| GetCanvasFile NextCanvas -->|否| ProcessAssetFiles subgraph 资源复制流程 ProcessAssetFiles[处理资源文件] --> CheckAssets{有资源文件?} CheckAssets -->|否| WriteDocs CheckAssets -->|是| GetAssetFile[获取资源文件对象] GetAssetFile --> ReadAsset[读取资源文件
readBinary binary 模式] ReadAsset --> MkdirAssets[创建 assets 目录
mkdir] MkdirAssets --> WriteAsset[写入到 docs/assets
writeBinary binary 模式] WriteAsset --> UpdateLinks[更新资源链接
updateLinksInContent] UpdateLinks --> NextAsset{还有资源?} NextAsset -->|是| GetAssetFile NextAsset -->|否| WriteDocs end WriteDocs[写入到 docs 目录
writeBinary binary 模式] --> Success[显示成功提示] Success --> End([结束]) ShowError1 --> End ShowError2 --> End ReturnContent --> WriteDocs style Start fill:#e1f5e1 style End fill:#ffe1e1 style Success fill:#e1f5e1 style ShowError1 fill:#ffe1e1 style ShowError2 fill:#ffe1e1 style ProcessCanvasFiles fill:#fff4e1 style ProcessAssetFiles fill:#e1f0ff style ExportInternalAPI fill:#ffe1ff ``` ## 核心功能模块 ### 1. 导出流程 **触发方式**: - 命令面板:`Export current note to VitePress` - 文件右键菜单:`Export to VitePress` - 编辑器右键菜单:`Export to VitePress` **文件验证**: - 检查活动文件是否存在 - 验证文件扩展名是否为 `md` **内容读取**: - 使用 `vault.readBinary()` 读取文件 - 保留原始格式(包括行尾空格) ### 2. 资源处理 **扫描机制**: - 使用 `metadataCache.getCache()` 获取文件元数据 - 扫描 `cache.embeds` 获取嵌入文件 - 扫描 `cache.links` 获取链接文件 **文件分类**: - Canvas 文件:扩展名为 `.canvas` - 普通资源:非 Markdown 文件 - Markdown 文件:不复制 **路径解析**: - 使用 `metadataCache.getFirstLinkpathDest()` 解析路径 - 支持相对路径和绝对路径 ### 3. Canvas 渲染 **打开文件**: - 使用 `workspace.getLeaf()` 获取 leaf - 调用 `activeLeaf.openFile()` 打开 Canvas - 从 `activeLeaf.view.canvas` 获取 Canvas 对象 **渲染过程**: - 获取视口边界框 `getViewportBBox()` - 设置 `zoom = 0`(100% 缩放) - 请求重绘 `requestFrame()` - 等待渲染完成(100ms + requestAnimationFrame) - 调用 `takeScreenshot(绝对路径)` 截图 **文件处理**: - 保存到临时文件(绝对路径) - 读取临时文件(相对路径) - 删除临时文件 - 写入到最终位置 ### 4. 链接更新 **Markdown 格式**: ```markdown 之前: ![](path/to/image.png) 之后: ![](assets/image.png) ``` **Wikilink 格式**: ```markdown 之前: ![[path/to/image.png]] 之后: ![[assets/image.png]] ``` **Canvas 特殊处理**: ```markdown 之前: ![[diagram.canvas]] 之后: ``` **路径匹配**: - 支持多种格式(绝对、相对、URL 编码) - 使用正则表达式匹配和替换 - 转换为相对路径 ### 5. 文件写入 **Binary 模式**: - 使用 `TextEncoder.encode()` 转换内容 - 调用 `vault.adapter.writeBinary()` 写入 - 保留原始格式 ## 关键方法说明 ### exportCurrentNote() 导出当前活动笔记 ```typescript async exportCurrentNote() { const activeFile = this.app.workspace.getActiveFile(); if (!activeFile) { new Notice('No active file found'); return; } if (activeFile.extension !== 'md') { new Notice('Current file is not a markdown file'); return; } await this.exportNote(activeFile); } ``` ### exportNote(file) 执行导出逻辑 ```typescript async exportNote(file: TFile) { // 1. 读取文件(binary 模式) const contentBuffer = await vault.readBinary(file); const content = new TextDecoder().decode(contentBuffer); // 2. 处理资源 const updatedContent = await this.processAssets(content, file); // 3. 写入文件(binary 模式) const docsPath = `${this.settings.docsPath}/${file.name}`; const updatedBuffer = new TextEncoder().encode(updatedContent); await vault.adapter.writeBinary(docsPath, updatedBuffer.buffer); } ``` ### processAssets(content, file) 处理资源文件 ```typescript async processAssets(content: string, sourceFile: TFile): Promise { const cache = metadataCache.getCache(sourceFile.path); if (!cache) return content; const assetsToCopy = new Set(); const canvasFiles = new Set(); // 收集嵌入文件 if (cache.embeds) { for (const embed of cache.embeds) { const resolvedLink = metadataCache.getFirstLinkpathDest(embed.link, sourceFile.path); if (resolvedLink) { if (resolvedLink.extension === 'canvas') { canvasFiles.add(resolvedLink.path); } else { assetsToCopy.add(resolvedLink.path); } } } } // 收集链接文件 if (cache.links) { for (const link of cache.links) { const resolvedLink = metadataCache.getFirstLinkpathDest(link.link, sourceFile.path); if (resolvedLink && resolvedLink.extension !== 'md') { assetsToCopy.add(resolvedLink.path); } } } // 处理 Canvas 文件 for (const canvasPath of canvasFiles) { const renderedImage = await this.captureCanvas(canvasFile); if (renderedImage) { // 写入并更新链接 } } // 复制资源文件 for (const assetPath of assetsToCopy) { const assetContent = await vault.readBinary(sourceFile); await vault.adapter.writeBinary(destPath, assetContent); content = this.updateLinksInContent(content, ...); } return content; } ``` ### captureCanvas(canvasFile) 捕获 Canvas 为图片 ```typescript async captureCanvas(canvasFile: TFile): Promise { const activeLeaf = this.app.workspace.getLeaf(); await activeLeaf.openFile(canvasFile); const canvas = activeLeaf.view.canvas; if (!canvas) return null; return await this.exportCanvasWithInternalAPI(canvas, canvasFile); } ``` ## 数据流图 ``` 用户触发 ↓ exportCurrentNote() ↓ exportNote(file) ↓ readBinary(file) → content ↓ processAssets(content, file) ├─→ metadataCache.getCache() → cache ├─→ 收集文件 → canvasFiles, assetsToCopy ├─→ 处理 Canvas │ ├─→ captureCanvas() │ ├─→ takeScreenshot() │ └─→ updateCanvasLinksInContent() └─→ 复制资源 ├─→ readBinary() ├─→ writeBinary() └─→ updateLinksInContent() ↓ updatedContent ↓ TextEncoder.encode() → buffer ↓ writeBinary(docsPath, buffer) ↓ 完成 ``` ## 错误处理 ### 文件验证失败 - 没有活动文件 → 显示 Notice - 文件不是 Markdown → 显示 Notice ### Canvas 渲染失败 - 捕获异常并记录日志 - 继续处理其他文件 - 不中断整个导出流程 ### 资源复制失败 - 捕获异常并记录日志 - 继续处理下一个资源 - 不影响其他文件 ## 性能特点 ### 优化策略 1. **使用 MetadataCache** - 快速获取文件关系 - 避免手动解析 2. **Binary 模式** - 减少字符串转换 - 保留原始格式 3. **异步操作** - 所有文件操作异步 - 不阻塞 UI 4. **错误隔离** - 单个文件失败不影响其他 - 继续处理后续文件 ### 时间复杂度 - 文件读取:O(n) - 资源扫描:O(e + l) 其中 e=嵌入数,l=链接数 - Canvas 渲染:O(c) 其中 c=Canvas 数量 - 资源复制:O(a) 其中 a=资源数量 总体:O(n + e + l + c + a) ## 配置选项 用户可以在插件设置中配置: - **docsPath**: 导出文档目录(默认: `docs`) - **assetsPath**: 资源文件目录(默认: `docs/assets`) ## 未来改进方向 1. **批量导出** - 导出整个文件夹 - 导出多个选中的文件 2. **进度显示** - 显示导出进度条 - 显示当前处理的文件 3. **更多配置** - 自定义图片格式 - 自定义链接转换规则 - 排除特定文件类型 4. **性能优化** - 并行处理资源 - 缓存已渲染的 Canvas - 增量导出(只处理修改的文件)