返回 Skill 列表
extension
分类: 开发与工程无需 API Key

obsidian-sdk-patterns

应用生产就绪的Obsidian插件模式以支持TypeScript。在实现复杂功能、重构插件或为Obsidian开发建立编码标准时使用。可以通过诸如“obsidian patterns”、“obsidian best practices”、“obsidian code patterns”、“idiomatic obsidian plugin”等短语触发。

person作者: jakexiaohubgithub

Obsidian SDK Patterns

Overview

Production-ready patterns for Obsidian plugin development in TypeScript.

Prerequisites

  • Completed obsidian-install-auth setup
  • Familiarity with TypeScript and async/await
  • Understanding of Obsidian's plugin lifecycle

Instructions

Step 1: Type-Safe Settings Pattern

// src/settings.ts
import { App, PluginSettingTab, Setting } from 'obsidian';
import type MyPlugin from './main';

export interface MyPluginSettings {
  apiEndpoint: string;
  enableFeatureX: boolean;
  maxItems: number;
  excludedFolders: string[];
}

export const DEFAULT_SETTINGS: MyPluginSettings = {
  apiEndpoint: 'https://api.example.com',
  enableFeatureX: true,
  maxItems: 100,
  excludedFolders: ['templates', 'archive'],
};

export class MyPluginSettingTab extends PluginSettingTab {
  plugin: MyPlugin;

  constructor(app: App, plugin: MyPlugin) {
    super(app, plugin);
    this.plugin = plugin;
  }

  display(): void {
    const { containerEl } = this;
    containerEl.empty();

    new Setting(containerEl)
      .setName('API Endpoint')
      .setDesc('The API endpoint for fetching data')
      .addText(text => text
        .setPlaceholder('https://api.example.com')
        .setValue(this.plugin.settings.apiEndpoint)
        .onChange(async (value) => {
          this.plugin.settings.apiEndpoint = value;
          await this.plugin.saveSettings();
        }));

    new Setting(containerEl)
      .setName('Enable Feature X')
      .setDesc('Toggle experimental feature')
      .addToggle(toggle => toggle
        .setValue(this.plugin.settings.enableFeatureX)
        .onChange(async (value) => {
          this.plugin.settings.enableFeatureX = value;
          await this.plugin.saveSettings();
        }));

    new Setting(containerEl)
      .setName('Max Items')
      .setDesc('Maximum number of items to display')
      .addSlider(slider => slider
        .setLimits(10, 500, 10)
        .setValue(this.plugin.settings.maxItems)
        .setDynamicTooltip()
        .onChange(async (value) => {
          this.plugin.settings.maxItems = value;
          await this.plugin.saveSettings();
        }));
  }
}

Step 2: Service Layer Pattern

// src/services/vault-service.ts
import { App, TFile, TFolder, Vault } from 'obsidian';

export class VaultService {
  constructor(private app: App) {}

  async getMarkdownFiles(folder?: string): Promise<TFile[]> {
    const files = this.app.vault.getMarkdownFiles();
    if (!folder) return files;

    return files.filter(f => f.path.startsWith(folder));
  }

  async readFile(file: TFile): Promise<string> {
    return this.app.vault.read(file);
  }

  async writeFile(file: TFile, content: string): Promise<void> {
    await this.app.vault.modify(file, content);
  }

  async createFile(path: string, content: string): Promise<TFile> {
    return this.app.vault.create(path, content);
  }

  async ensureFolder(path: string): Promise<TFolder> {
    const folder = this.app.vault.getAbstractFileByPath(path);
    if (folder instanceof TFolder) return folder;

    await this.app.vault.createFolder(path);
    return this.app.vault.getAbstractFileByPath(path) as TFolder;
  }

  getFileByPath(path: string): TFile | null {
    const file = this.app.vault.getAbstractFileByPath(path);
    return file instanceof TFile ? file : null;
  }
}

Step 3: Event Management Pattern

// src/events.ts
import { Plugin, EventRef, Events } from 'obsidian';

export class EventManager {
  private eventRefs: EventRef[] = [];

  constructor(private plugin: Plugin) {}

  register(events: Events, name: string, callback: (...args: any[]) => any): void {
    const ref = events.on(name as any, callback);
    this.eventRefs.push(ref);
    this.plugin.registerEvent(ref);
  }

  registerWorkspaceEvent(name: string, callback: (...args: any[]) => any): void {
    this.register(this.plugin.app.workspace, name, callback);
  }

  registerVaultEvent(name: string, callback: (...args: any[]) => any): void {
    this.register(this.plugin.app.vault, name, callback);
  }

  cleanup(): void {
    // Events are automatically cleaned up by Obsidian
    // But keep track for manual cleanup if needed
    this.eventRefs = [];
  }
}

// Usage in main.ts:
const eventManager = new EventManager(this);
eventManager.registerWorkspaceEvent('file-open', (file) => {
  if (file) console.log('Opened:', file.path);
});
eventManager.registerVaultEvent('modify', (file) => {
  console.log('Modified:', file.path);
});

Step 4: Command Builder Pattern

// src/commands.ts
import { Command, Editor, MarkdownView, Plugin } from 'obsidian';

interface CommandConfig {
  id: string;
  name: string;
  icon?: string;
  hotkeys?: { modifiers: string[]; key: string }[];
}

export class CommandBuilder {
  private commands: Command[] = [];

  constructor(private plugin: Plugin, private prefix: string) {}

  addSimple(config: CommandConfig, callback: () => void): this {
    this.commands.push({
      id: `${this.prefix}-${config.id}`,
      name: config.name,
      icon: config.icon,
      callback,
    });
    return this;
  }

  addEditor(
    config: CommandConfig,
    callback: (editor: Editor, view: MarkdownView) => void
  ): this {
    this.commands.push({
      id: `${this.prefix}-${config.id}`,
      name: config.name,
      icon: config.icon,
      editorCallback: callback,
    });
    return this;
  }

  addCheck(
    config: CommandConfig,
    check: () => boolean,
    callback: () => void
  ): this {
    this.commands.push({
      id: `${this.prefix}-${config.id}`,
      name: config.name,
      icon: config.icon,
      checkCallback: (checking) => {
        if (checking) return check();
        callback();
        return true;
      },
    });
    return this;
  }

  register(): void {
    this.commands.forEach(cmd => this.plugin.addCommand(cmd));
  }
}

// Usage:
new CommandBuilder(this, 'my-plugin')
  .addSimple({ id: 'greet', name: 'Show Greeting' }, () => {
    new Notice('Hello!');
  })
  .addEditor({ id: 'insert', name: 'Insert Text' }, (editor) => {
    editor.replaceSelection('Inserted text');
  })
  .register();

Step 5: Async Queue Pattern

// src/utils/async-queue.ts
export class AsyncQueue {
  private queue: (() => Promise<void>)[] = [];
  private processing = false;

  async add(task: () => Promise<void>): Promise<void> {
    return new Promise((resolve, reject) => {
      this.queue.push(async () => {
        try {
          await task();
          resolve();
        } catch (e) {
          reject(e);
        }
      });
      this.process();
    });
  }

  private async process(): Promise<void> {
    if (this.processing) return;
    this.processing = true;

    while (this.queue.length > 0) {
      const task = this.queue.shift();
      if (task) await task();
    }

    this.processing = false;
  }
}

// Usage for rate-limited operations:
const writeQueue = new AsyncQueue();

async function safeWrite(file: TFile, content: string) {
  await writeQueue.add(async () => {
    await this.app.vault.modify(file, content);
  });
}

Output

  • Type-safe settings management
  • Service layer for vault operations
  • Event registration with automatic cleanup
  • Fluent command builder
  • Async queue for rate limiting

Error Handling

| Pattern | Use Case | Benefit | |---------|----------|---------| | Settings validation | User input | Prevents invalid config | | Service layer | Vault access | Centralizes file ops | | Event manager | Lifecycle events | Prevents memory leaks | | Command builder | Plugin commands | Cleaner registration | | Async queue | Bulk operations | Prevents race conditions |

Examples

Debounce Pattern

// src/utils/debounce.ts
export function debounce<T extends (...args: any[]) => any>(
  func: T,
  wait: number
): (...args: Parameters<T>) => void {
  let timeout: NodeJS.Timeout | null = null;

  return (...args: Parameters<T>) => {
    if (timeout) clearTimeout(timeout);
    timeout = setTimeout(() => func(...args), wait);
  };
}

// Usage for search:
const debouncedSearch = debounce(async (query: string) => {
  const results = await performSearch(query);
  updateUI(results);
}, 300);

Singleton Service Pattern

// src/main.ts
export default class MyPlugin extends Plugin {
  private static instance: MyPlugin;
  private vaultService: VaultService;

  static getInstance(): MyPlugin {
    return MyPlugin.instance;
  }

  async onload() {
    MyPlugin.instance = this;
    this.vaultService = new VaultService(this.app);
  }

  getVaultService(): VaultService {
    return this.vaultService;
  }
}

Resources

Next Steps

Apply patterns in obsidian-core-workflow-a for vault operations.