插件调试技巧
调试是插件开发过程中不可或缺的环节。本文介绍 Obsidian 插件开发的各种调试技巧,帮助你快速定位和解决问题。
开发者工具
打开开发者工具
Obsidian 基于 Electron,可以使用 Chrome 开发者工具进行调试:
| 平台 | 快捷键 |
|---|---|
| Windows/Linux | Ctrl + Shift + I |
| macOS | Cmd + Option + I |
也可以通过菜单打开: 视图 → 切换开发者工具
开发者工具面板
Console 面板
查看日志输出和错误信息:
javascript
// 在插件代码中使用
console.log('普通日志');
console.info('信息日志');
console.warn('警告日志');
console.error('错误日志');
// 输出对象
console.log('插件设置:', this.settings);
// 表格形式输出
console.table([{ name: 'Alice', age: 25 }, { name: 'Bob', age: 30 }]);
// 分组输出
console.group('API 调用');
console.log('请求参数:', params);
console.log('响应数据:', response);
console.groupEnd();
// 计时
console.time('操作耗时');
// ... 一些操作
console.timeEnd('操作耗时');Sources 面板
查看和调试源代码:
- 在左侧文件树中找到你的插件
- 路径通常是
vault/.obsidian/plugins/你的插件名/ - 点击文件打开源码
Network 面板
监控网络请求:
javascript
// 发送 API 请求时
const response = await fetch('https://api.example.com/data');
const data = await response.json();
console.log('API 响应:', data);断点调试
设置断点
- 打开 Sources 面板
- 找到你的插件代码文件
- 点击行号左侧设置断点
- 触发相关功能,代码会在断点处暂停
断点类型
| 类型 | 说明 | 使用场景 |
|---|---|---|
| 普通断点 | 代码执行到此暂停 | 一般调试 |
| 条件断点 | 满足条件时暂停 | 循环或频繁调用 |
| 日志点 | 输出信息不暂停 | 查看变量值 |
| 异常断点 | 抛出异常时暂停 | 捕获错误 |
设置条件断点
- 右键点击行号
- 选择「Add conditional breakpoint」
- 输入条件表达式,如
id === 'target-id'
调试控制
| 操作 | 快捷键 | 说明 |
|---|---|---|
| 继续 | F8 | 继续执行到下一个断点 |
| 单步跳过 | F10 | 执行当前行,不进入函数 |
| 单步进入 | F11 | 进入函数内部 |
| 单步跳出 | Shift+F11 | 跳出当前函数 |
实时修改代码
热重载
使用 Obsidian 的热重载功能:
- 在插件中添加:
typescript
import { Plugin } from 'obsidian';
export default class MyPlugin extends Plugin {
onload() {
// 开发模式下添加热重载
if (this.app.isMobile) return;
this.registerEvent(
this.app.vault.on('modify', (file) => {
if (file.path.includes('main.js')) {
// 提示用户重新加载
new Notice('代码已更新,请重新加载插件');
}
})
);
}
}- 或者使用 Hot Reload 插件自动重载
控制台直接执行
在 Console 中直接执行代码:
javascript
// 获取当前活动文件
const activeFile = app.workspace.getActiveFile();
console.log('当前文件:', activeFile);
// 获取插件实例
const plugin = app.plugins.plugins['your-plugin-id'];
console.log('插件设置:', plugin.settings);
// 手动调用插件方法
plugin.yourMethod();日志记录
创建日志工具
typescript
// utils/logger.ts
export class Logger {
private prefix: string;
private enabled: boolean;
constructor(prefix: string, enabled = true) {
this.prefix = prefix;
this.enabled = enabled;
}
log(message: string, ...args: any[]) {
if (this.enabled) {
console.log(`[${this.prefix}] ${message}`, ...args);
}
}
warn(message: string, ...args: any[]) {
if (this.enabled) {
console.warn(`[${this.prefix}] ${message}`, ...args);
}
}
error(message: string, ...args: any[]) {
console.error(`[${this.prefix}] ${message}`, ...args);
}
time(label: string) {
if (this.enabled) {
console.time(`[${this.prefix}] ${label}`);
}
}
timeEnd(label: string) {
if (this.enabled) {
console.timeEnd(`[${this.prefix}] ${label}`);
}
}
}
// 使用
const logger = new Logger('MyPlugin', true);
logger.log('插件已加载');
logger.time('数据处理');
// ... 处理逻辑
logger.timeEnd('数据处理');带日志级别的调试
typescript
enum LogLevel {
DEBUG,
INFO,
WARN,
ERROR
}
class DebugLogger {
private level: LogLevel;
constructor(level: LogLevel = LogLevel.INFO) {
this.level = level;
}
debug(message: string, ...args: any[]) {
if (this.level <= LogLevel.DEBUG) {
console.debug(`[DEBUG] ${message}`, ...args);
}
}
info(message: string, ...args: any[]) {
if (this.level <= LogLevel.INFO) {
console.info(`[INFO] ${message}`, ...args);
}
}
warn(message: string, ...args: any[]) {
if (this.level <= LogLevel.WARN) {
console.warn(`[WARN] ${message}`, ...args);
}
}
error(message: string, ...args: any[]) {
if (this.level <= LogLevel.ERROR) {
console.error(`[ERROR] ${message}`, ...args);
}
}
}常见问题排查
插件无法加载
症状:插件列表中找不到插件或显示错误
排查步骤:
- 检查
manifest.json格式:
json
{
"id": "my-plugin",
"name": "My Plugin",
"version": "1.0.0",
"minAppVersion": "0.15.0",
"description": "插件描述",
"author": "作者名",
"authorUrl": "https://example.com",
"isDesktopOnly": false
}- 检查
main.js是否存在 - 查看 Console 中的错误信息
- 检查 TypeScript 编译是否有错误
代码不生效
症状:修改代码后没有变化
排查步骤:
- 确认代码已编译:bash
npm run build - 重新加载插件:关闭再启用插件
- 检查是否修改了正确的文件
- 清除缓存后重启 Obsidian
事件监听器不触发
症状:注册的事件没有响应
排查步骤:
typescript
// 检查事件是否正确注册
this.registerEvent(
this.app.vault.on('create', (file) => {
console.log('文件创建:', file.path); // 添加日志
})
);
// 确保使用正确的 this 绑定
this.registerEvent(
this.app.workspace.on('file-open', this.handleFileOpen.bind(this))
);内存泄漏
症状:插件运行时间长了变慢
排查步骤:
- 使用开发者工具的 Memory 面板
- 检查是否有未清理的事件监听器:
typescript
// 错误示例
setInterval(() => {
// 如果在 onload 中注册,onunload 时需要清理
}, 1000);
// 正确做法
this.registerInterval(
window.setInterval(() => {
// ...
}, 1000)
);- 检查是否有未释放的资源:
typescript
export default class MyPlugin extends Plugin {
private debounceTimer: number;
onload() {
this.debounceTimer = window.setTimeout(() => {}, 1000);
}
onunload() {
// 清理定时器
if (this.debounceTimer) {
window.clearTimeout(this.debounceTimer);
}
}
}性能调试
使用 Performance 面板
- 打开开发者工具
- 切换到 Performance 面板
- 点击录制按钮
- 执行需要测试的操作
- 停止录制,分析结果
性能标记
typescript
// 使用 performance API
performance.mark('operation-start');
// 执行操作
for (let i = 0; i < 10000; i++) {
// ...
}
performance.mark('operation-end');
performance.measure('operation', 'operation-start', 'operation-end');
// 查看结果
const measures = performance.getEntriesByName('operation');
console.log('耗时:', measures[0].duration, 'ms');防抖与节流
typescript
// 防抖 - 延迟执行
function debounce<T extends (...args: any[]) => any>(
func: T,
wait: number
): (...args: Parameters<T>) => void {
let timeout: number;
return function(this: any, ...args: Parameters<T>) {
clearTimeout(timeout);
timeout = window.setTimeout(() => func.apply(this, args), wait);
};
}
// 节流 - 固定频率执行
function throttle<T extends (...args: any[]) => any>(
func: T,
limit: number
): (...args: Parameters<T>) => void {
let inThrottle: boolean;
return function(this: any, ...args: Parameters<T>) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
// 使用示例
this.registerEvent(
this.app.workspace.on('file-open', debounce((file) => {
console.log('文件打开:', file.path);
}, 300))
);调试技巧总结
开发模式设置
typescript
// 在插件中区分开发/生产模式
const isDev = process.env.NODE_ENV === 'development';
export default class MyPlugin extends Plugin {
async onload() {
if (isDev) {
// 开发模式下的调试代码
this.addCommand({
id: 'debug-info',
name: '显示调试信息',
callback: () => this.showDebugInfo()
});
}
}
showDebugInfo() {
const info = {
settings: this.settings,
activeFile: this.app.workspace.getActiveFile(),
vault: this.app.vault.getRoot().path
};
console.log('调试信息:', info);
}
}错误边界
typescript
// 包装可能出错的操作
async safeExecute<T>(operation: () => Promise<T>): Promise<T | null> {
try {
return await operation();
} catch (error) {
console.error('操作失败:', error);
new Notice('操作失败,请查看控制台');
return null;
}
}
// 使用
const result = await this.safeExecute(async () => {
const data = await this.fetchData();
return this.processData(data);
});模拟数据
typescript
// 在开发时使用模拟数据
class MockDataService {
private useMock = true;
async fetchData(): Promise<Data[]> {
if (this.useMock) {
return this.getMockData();
}
return this.getRealData();
}
private getMockData(): Data[] {
return [
{ id: 1, name: 'Test 1' },
{ id: 2, name: 'Test 2' },
];
}
private async getRealData(): Promise<Data[]> {
// 实际获取数据
}
}