- Add Canvas rendering implementation in `notes/render-canvas.md` - Create VitePress exporter plugin in `src/main.ts` with commands for exporting notes - Implement settings management for the plugin in `src/settings.ts` - Add CSS file for potential styling needs - Create version bump script in `version-bump.mjs` for managing versioning - Initialize version tracking in `versions.json`
11 KiB
11 KiB
VitePress Exporter 架构设计
概述
VitePress Exporter 是一个 Obsidian 插件,用于将笔记和资源导出到 VitePress 格式。核心功能包括:
- 导出 Markdown 文件到
docs/目录 - 复制图片等资源到
docs/assets/目录 - 将 Obsidian Canvas 渲染为图片
技术栈
- 语言: TypeScript
- 构建工具: esbuild
- 包管理器: pnpm
- Obsidian API: Plugin, Workspace, TFile, Vault, MetadataCache
核心架构
1. 导出流程
用户触发导出
↓
exportCurrentNote()
↓
exportNote(file)
↓
processAssets(content, file)
├─→ 扫描嵌入和链接(使用 MetadataCache)
├─→ 处理 Canvas 文件 (captureCanvas)
│ ├─→ 打开 Canvas 文件
│ ├─→ 获取 Canvas 对象
│ └─→ exportCanvasWithInternalAPI
│ ├─→ 设置 zoom = 0 (100%)
│ ├─→ 调用 takeScreenshot
│ └─→ 保存图片
└─→ 复制资源文件
├─→ 使用 binary 模式读取
└─→ 写入到 docs/assets/
↓
updateLinksInContent() / updateCanvasLinksInContent()
↓
写入 docs/ 目录(使用 binary 模式)
2. 文件结构
vitepress-exporter/
├── src/
│ ├── main.ts # 主插件源代码
│ └── settings.ts # 设置相关代码
├── manifest.json # 插件清单
├── package.json # 项目配置
├── esbuild.config.mjs # 构建配置
├── tsconfig.json # TypeScript 配置
└── notes/ # 开发文档
├── architecture.md # 本文档
├── canvas-rendering.md # Canvas 渲染详细说明
└── flowchart.md # 流程图
3. 关键类和方法
VitePressExporterPlugin
主插件类,继承自 Plugin:
class VitePressExporterPlugin extends Plugin {
settings: VitePressExporterSettings;
// 生命周期
async onload() // 插件加载,注册命令和菜单
onunload() // 插件卸载
// 设置管理
async loadSettings() // 加载设置
async saveSettings() // 保存设置
// 导出功能
async exportCurrentNote() // 导出当前笔记
async exportNote(file) // 导出单个笔记
// 资源处理
async processAssets(content, file) // 处理资源文件和 Canvas
// Canvas 渲染
async captureCanvas(canvasFile) // 捕获 Canvas 为图片
async exportCanvasWithInternalAPI(canvas, canvasFile) // 使用内部 API
// 链接更新
updateLinksInContent(content, sourceFile, assetPath, newFileName) // 更新资源链接
updateCanvasLinksInContent(content, canvasFile, canvasPath, imageFileName) // 更新 Canvas 链接
}
Canvas 渲染架构
核心决策
使用 Obsidian Canvas 内部 API,而不是自己实现渲染逻辑:
- ✅
canvas.takeScreenshot()- 直接截图 - ✅
canvas.getViewportBBox()- 获取边界框 - ✅
canvas.zoom- 控制缩放(对数刻度) - ✅
canvas.requestFrame()- 触发重绘
为什么不使用第三方库:
- ❌
html-to-image- 需要滑动窗口,代码复杂 - ❌
dom-to-image-more- 维护不活跃 - ✅ Obsidian 内部 API - 兼容性好,代码简洁
Zoom 对数刻度
关键发现:canvas.zoom 使用对数刻度(base 2)
zoom = 0 → scale = 2^0 = 1.0 (100%)
zoom = 1 → scale = 2^1 = 2.0 (200%)
zoom = -1 → scale = 2^-1 = 0.5 (50%)
重要:设置 zoom = 0 获得准确的 100% 缩放,不要设置 zoom = 1.0!
文件路径处理
takeScreenshot 需要绝对文件系统路径:
// 1. 获取 vault 的完整路径
const vaultBasePath = vault.adapter.basePath;
// 2. 构建临时文件的绝对路径
const tempFullPath = `${vaultBasePath}/temp_canvas_${Date.now()}.png`;
// 3. 调用 takeScreenshot(需要绝对路径)
await canvas.takeScreenshot(tempFullPath, false);
// 4. 使用相对路径读取(vault API)
const imageBuffer = await vault.adapter.readBinary(tempFileName);
// 5. 删除临时文件
await vault.adapter.remove(tempFileName);
// 6. 写入到正确位置(相对路径)
await vault.adapter.writeBinary(finalPath, imageBuffer);
资源处理
资源类型
-
嵌入资源 (
![[image.png]])- 图片、PDF 等非 Markdown 文件
- 自动复制到
docs/assets/ - 使用 binary 模式保留原始内容
-
Canvas 文件 (
![[diagram.canvas]])- 使用内部 API 渲染为 PNG 图片
- 替换为
<Viewer src="assets/diagram.png" />
-
普通链接 (
[[other-file]])- Markdown 文件不复制
- 其他资源文件会被复制
资源扫描机制
使用 Obsidian 的 MetadataCache API:
const cache = metadataCache.getCache(sourceFile.path);
// 嵌入文件
if (cache.embeds) {
for (const embed of cache.embeds) {
const resolvedLink = metadataCache.getFirstLinkpathDest(embed.link, sourceFile.path);
// 检查是否是 Canvas 或其他资源
}
}
// 普通链接
if (cache.links) {
for (const link of cache.links) {
const resolvedLink = metadataCache.getFirstLinkpathDest(link.link, sourceFile.path);
// 只复制非 Markdown 文件
}
}
路径更新
资源链接会自动更新为相对于 docs/ 的路径:
Markdown 格式:
之前: 
之后: 
Wikilink 格式:
之前: ![[images/screenshot.png]]
之后: ![[assets/screenshot.png]]
Canvas 特殊处理:
之前: ![[diagram.canvas]]
之后: <Viewer src="assets/diagram.png" />
路径解析策略
支持多种路径格式的智能匹配:
const possiblePaths = [
assetPath, // 完整路径
assetPath.substring(sourceDir.length + 1), // 相对路径
encodeURI(assetPath), // URL 编码
encodeURI(assetPath.substring(sourceDir.length + 1)),
sourceFile.name, // 文件名
encodeURI(sourceFile.name)
];
配置
插件设置
interface VitePressExporterSettings {
docsPath: string; // 默认: 'docs'
assetsPath: string; // 默认: 'docs/assets'
}
Obsidian 配置
插件通过 Obsidian 的设置界面配置:
- 设置 → 社区插件 → VitePress Exporter → 选项
- 配置
Docs path和Assets path - 保存设置
文件读写
Binary 模式
所有文件操作都使用 binary 模式,以保留原始格式:
// 读取文件
const contentBuffer = await vault.readBinary(file);
const content = new TextDecoder().decode(contentBuffer);
// 写入文件
const updatedBuffer = new TextEncoder().encode(updatedContent);
await vault.adapter.writeBinary(docsPath, updatedBuffer.buffer);
优势:
- 保留行尾空格
- 保留特殊字符
- 不修改原始格式
错误处理
Canvas 渲染失败
当 Canvas 渲染失败时,会在控制台输出错误信息,但不会中断整个导出流程:
try {
const renderedImage = await this.captureCanvas(canvasFile);
if (renderedImage) {
// 成功处理
}
} catch (error) {
console.error(`Failed to render canvas ${canvasPath}:`, error);
}
// 继续处理其他文件
资源复制失败
单个资源文件复制失败不会影响其他文件:
try {
// 复制资源
} catch (error) {
console.error(`Failed to copy asset ${assetPath}:`, error);
}
// 继续处理下一个资源
性能考虑
优化策略
-
使用 MetadataCache
- 快速获取文件的嵌入和链接信息
- 避免手动解析 Markdown
-
批量处理
- 一次性收集所有需要处理的文件
- 批量复制和更新
-
异步操作
- 所有文件操作都是异步的
- 不阻塞 UI 线程
-
Binary 模式
- 减少字符串转换开销
- 直接操作二进制数据
预期性能
- 小笔记(< 10 个资源):< 1 秒
- 中等笔记(10-50 个资源):< 3 秒
- 大笔记(> 50 个资源或多个 Canvas):< 10 秒
触发方式
插件提供三种导出触发方式:
-
命令面板
Export current note to VitePress
-
文件菜单
- 右键点击文件 →
Export to VitePress
- 右键点击文件 →
-
编辑器菜单
- 右键点击编辑器 →
Export to VitePress
- 右键点击编辑器 →
未来扩展
可能的功能
-
批量导出
- 导出整个文件夹
- 导出所有笔记
-
高级配置
- 自定义图片格式(PNG/JPEG/WebP)
- 图片质量控制
- 自定义链接替换规则
-
VitePress 集成
- 自动生成 frontmatter
- 自动创建索引页面
- 集成 VitePress 构建流程
-
更多资源类型
- 支持视频嵌入
- 支持音频文件
- 支持其他特殊格式
维护指南
修改导出逻辑
主要导出逻辑在 exportNote() 方法中:
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 updatedBuffer = new TextEncoder().encode(updatedContent);
await vault.adapter.writeBinary(docsPath, updatedBuffer.buffer);
}
添加新的资源类型
在 processAssets() 方法中添加:
if (cache.embeds) {
for (const embed of cache.embeds) {
const resolvedLink = metadataCache.getFirstLinkpathDest(embed.link, sourceFile.path);
if (resolvedLink) {
if (resolvedLink.extension === 'canvas') {
// Canvas 处理
} else if (resolvedLink.extension === 'newtype') {
// 处理新类型
} else {
// 普通资源
}
}
}
}
调试技巧
-
启用详细日志
console.log('Processing assets for file:', sourceFile.path); console.log('Cache:', cache); console.log('Assets to copy:', Array.from(assetsToCopy)); -
使用 Obsidian 开发者工具
Ctrl+Shift+I打开控制台- 查看日志输出
-
测试单个功能
- 创建最小测试用例
- 逐步验证每个步骤
相关文档
- Canvas 渲染详细说明 - Canvas 渲染的实现细节
- 执行流程图 - 完整的导出流程图
- Obsidian API 文档 - 官方 API