Skip to content

插件开发实战

在掌握了插件开发基础后,本文将深入探讨实际开发中的高级技巧和最佳实践。

视图开发

自定义侧边栏视图

创建可交互的侧边栏面板:

typescript
import { ItemView, WorkspaceLeaf } from 'obsidian';

const VIEW_TYPE = 'my-custom-view';

export class MyCustomView extends ItemView {
  constructor(leaf: WorkspaceLeaf) {
    super(leaf);
  }

  getViewType(): string {
    return VIEW_TYPE;
  }

  getDisplayText(): string {
    return 'My Custom View';
  }

  getIcon(): string {
    return 'star';
  }

  async onOpen() {
    const container = this.containerEl.children[1];
    container.empty();
    container.createEl('h4', { text: 'Custom View' });
    
    // 添加交互内容
    const button = container.createEl('button', { text: 'Click Me' });
    button.addEventListener('click', () => {
      new Notice('Button clicked!');
    });
  }

  async onClose() {
    // 清理资源
  }
}

注册视图

typescript
// 在插件中注册视图
export default class MyPlugin extends Plugin {
  async onload() {
    this.registerView(
      VIEW_TYPE,
      (leaf) => new MyCustomView(leaf)
    );

    // 添加侧边栏图标
    this.addRibbonIcon('star', 'Open Custom View', () => {
      this.activateView();
    });
  }

  async activateView() {
    const { workspace } = this.app;
    
    let leaf: WorkspaceLeaf | null = null;
    const leaves = workspace.getLeavesOfType(VIEW_TYPE);
    
    if (leaves.length > 0) {
      leaf = leaves[0];
    } else {
      leaf = workspace.getRightLeaf(false);
      await leaf?.setViewState({ type: VIEW_TYPE, active: true });
    }
    
    if (leaf) {
      workspace.revealLeaf(leaf);
    }
  }
}

视图状态管理

typescript
interface ViewState {
  searchTerm: string;
  filter: string;
  items: any[];
}

export class StatefulView extends ItemView {
  private state: ViewState = {
    searchTerm: '',
    filter: 'all',
    items: []
  };

  setState(state: Partial<ViewState>): void {
    this.state = { ...this.state, ...state };
    this.render();
  }

  getState(): ViewState {
    return this.state;
  }

  private render() {
    const container = this.containerEl.children[1];
    container.empty();
    
    // 根据状态渲染内容
    const filtered = this.state.items.filter(item => {
      if (this.state.filter !== 'all') {
        return item.type === this.state.filter;
      }
      return true;
    });
    
    filtered.forEach(item => {
      container.createDiv({ text: item.name });
    });
  }
}

设置系统

设置数据结构

typescript
interface MyPluginSettings {
  enableFeature: boolean;
  apiKey: string;
  refreshInterval: number;
  theme: 'light' | 'dark' | 'system';
  excludedFolders: string[];
}

const DEFAULT_SETTINGS: MyPluginSettings = {
  enableFeature: true,
  apiKey: '',
  refreshInterval: 60,
  theme: 'system',
  excludedFolders: []
}

export default class MyPlugin extends Plugin {
  settings: MyPluginSettings;

  async onload() {
    await this.loadSettings();
  }

  async loadSettings() {
    this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
  }

  async saveSettings() {
    await this.saveData(this.settings);
  }
}

设置面板

typescript
import { App, PluginSettingTab, Setting } from 'obsidian';

class MySettingTab 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('Enable feature')
      .setDesc('Enable or disable the main feature')
      .addToggle(toggle => toggle
        .setValue(this.plugin.settings.enableFeature)
        .onChange(async (value) => {
          this.plugin.settings.enableFeature = value;
          await this.plugin.saveSettings();
        }));

    // API Key 输入
    new Setting(containerEl)
      .setName('API Key')
      .setDesc('Enter your API key')
      .addText(text => text
        .setPlaceholder('Enter your API key')
        .setValue(this.plugin.settings.apiKey)
        .onChange(async (value) => {
          this.plugin.settings.apiKey = value;
          await this.plugin.saveSettings();
        }));

    // 下拉选择
    new Setting(containerEl)
      .setName('Theme')
      .setDesc('Select the theme')
      .addDropdown(dropdown => dropdown
        .addOption('light', 'Light')
        .addOption('dark', 'Dark')
        .addOption('system', 'System')
        .setValue(this.plugin.settings.theme)
        .onChange(async (value: 'light' | 'dark' | 'system') => {
          this.plugin.settings.theme = value;
          await this.plugin.saveSettings();
        }));

    // 滑块
    new Setting(containerEl)
      .setName('Refresh interval')
      .setDesc('Refresh interval in seconds')
      .addSlider(slider => slider
        .setLimits(10, 300, 10)
        .setValue(this.plugin.settings.refreshInterval)
        .setDynamicTooltip()
        .onChange(async (value) => {
          this.plugin.settings.refreshInterval = value;
          await this.plugin.saveSettings();
        }));

    // 多选(文件夹排除)
    new Setting(containerEl)
      .setName('Excluded folders')
      .setDesc('Folders to exclude from processing')
      .addTextArea(text => text
        .setPlaceholder('folder1\nfolder2\nfolder3')
        .setValue(this.plugin.settings.excludedFolders.join('\n'))
        .onChange(async (value) => {
          this.plugin.settings.excludedFolders = value.split('\n').filter(f => f.trim());
          await this.plugin.saveSettings();
        }));
  }
}

注册设置面板

typescript
export default class MyPlugin extends Plugin {
  async onload() {
    // ... 其他初始化代码
    
    this.addSettingTab(new MySettingTab(this.app, this));
  }
}

模态框开发

简单模态框

typescript
import { App, Modal, Notice } from 'obsidian';

class SimpleModal extends Modal {
  constructor(app: App) {
    super(app);
  }

  onOpen() {
    const { contentEl } = this;
    contentEl.createEl('h2', { text: 'Simple Modal' });
    contentEl.createEl('p', { text: 'This is a simple modal.' });
    
    contentEl.createEl('button', { text: 'Close' })
      .addEventListener('click', () => {
        this.close();
      });
  }

  onClose() {
    const { contentEl } = this;
    contentEl.empty();
  }
}

// 使用模态框
new SimpleModal(this.app).open();

带输入的模态框

typescript
interface InputResult {
  title: string;
  content: string;
}

class InputModal extends Modal {
  private result: InputResult = { title: '', content: '' };
  private onSubmit: (result: InputResult) => void;

  constructor(app: App, onSubmit: (result: InputResult) => void) {
    super(app);
    this.onSubmit = onSubmit;
  }

  onOpen() {
    const { contentEl } = this;
    
    contentEl.createEl('h2', { text: 'Create New Note' });
    
    new Setting(contentEl)
      .setName('Title')
      .addText(text => text
        .setPlaceholder('Enter title')
        .onChange(value => {
          this.result.title = value;
        }));
    
    new Setting(contentEl)
      .setName('Content')
      .addTextArea(text => text
        .setPlaceholder('Enter content')
        .onChange(value => {
          this.result.content = value;
        }));
    
    new Setting(contentEl)
      .addButton(btn => btn
        .setButtonText('Cancel')
        .onClick(() => {
          this.close();
        }))
      .addButton(btn => btn
        .setButtonText('Create')
        .setCta()
        .onClick(() => {
          this.onSubmit(this.result);
          this.close();
        }));
  }

  onClose() {
    const { contentEl } = this;
    contentEl.empty();
  }
}

// 使用
new InputModal(this.app, async (result) => {
  await this.app.vault.create(`${result.title}.md`, result.content);
}).open();

建议框(Suggest Modal)

typescript
import { App, FuzzySuggestModal, TFile } from 'obsidian';

class FileSuggestModal extends FuzzySuggestModal<TFile> {
  private files: TFile[];
  private onChoose: (file: TFile) => void;

  constructor(app: App, files: TFile[], onChoose: (file: TFile) => void) {
    super(app);
    this.files = files;
    this.onChoose = onChoose;
  }

  getItems(): TFile[] {
    return this.files;
  }

  getItemText(file: TFile): string {
    return file.basename;
  }

  onChooseItem(file: TFile, evt: MouseEvent | KeyboardEvent): void {
    this.onChoose(file);
  }
}

// 使用
const markdownFiles = this.app.vault.getMarkdownFiles();
new FileSuggestModal(this.app, markdownFiles, (file) => {
  console.log('Selected:', file.path);
}).open();

数据存储

插件数据存储

typescript
// 存储简单数据
await this.saveData({ key: 'value' });

// 读取数据
const data = await this.loadData();

// 存储复杂数据
interface StoredData {
  version: string;
  cache: Record<string, any>;
  lastSync: number;
}

const data: StoredData = await this.loadData() || {
  version: '1.0.0',
  cache: {},
  lastSync: 0
};

// 更新部分数据
data.cache['key'] = 'value';
data.lastSync = Date.now();
await this.saveData(data);

文件操作

typescript
// 创建文件
async function createNote(title: string, content: string) {
  const path = `${title}.md`;
  const exists = await this.app.vault.adapter.exists(path);
  
  if (exists) {
    new Notice('File already exists');
    return null;
  }
  
  return await this.app.vault.create(path, content);
}

// 读取文件
async function readNote(path: string): Promise<string> {
  const file = this.app.vault.getAbstractFileByPath(path);
  if (file instanceof TFile) {
    return await this.app.vault.read(file);
  }
  return '';
}

// 更新文件
async function updateNote(file: TFile, content: string) {
  await this.app.vault.modify(file, content);
}

// 删除文件
async function deleteNote(file: TFile) {
  await this.app.vault.delete(file);
}

// 监听文件变化
this.registerEvent(
  this.app.vault.on('modify', (file) => {
    if (file instanceof TFile) {
      console.log('File modified:', file.path);
    }
  })
);

缓存管理

typescript
class CacheManager {
  private cache: Map<string, { data: any; expiry: number }>;
  private defaultTTL = 60 * 60 * 1000; // 1 hour

  constructor() {
    this.cache = new Map();
  }

  set(key: string, data: any, ttl = this.defaultTTL) {
    this.cache.set(key, {
      data,
      expiry: Date.now() + ttl
    });
  }

  get<T>(key: string): T | null {
    const item = this.cache.get(key);
    if (!item) return null;
    
    if (Date.now() > item.expiry) {
      this.cache.delete(key);
      return null;
    }
    
    return item.data as T;
  }

  has(key: string): boolean {
    return this.get(key) !== null;
  }

  delete(key: string): boolean {
    return this.cache.delete(key);
  }

  clear(): void {
    this.cache.clear();
  }
}

API 集成

外部 API 调用

typescript
import { requestUrl, RequestUrlParam } from 'obsidian';

async function fetchAPI<T>(url: string, options?: Partial<RequestUrlParam>): Promise<T> {
  try {
    const response = await requestUrl({
      url,
      method: 'GET',
      headers: {
        'Content-Type': 'application/json',
        ...options?.headers
      },
      ...options
    });
    
    return response.json as T;
  } catch (error) {
    console.error('API Error:', error);
    throw new Error(`Failed to fetch: ${error.message}`);
  }
}

// 使用示例
interface User {
  id: string;
  name: string;
}

const user = await fetchAPI<User>('https://api.example.com/user/1');

带认证的 API

typescript
class APIClient {
  private baseUrl: string;
  private apiKey: string;

  constructor(baseUrl: string, apiKey: string) {
    this.baseUrl = baseUrl;
    this.apiKey = apiKey;
  }

  async request<T>(endpoint: string, options?: Partial<RequestUrlParam>): Promise<T> {
    const url = `${this.baseUrl}${endpoint}`;
    
    const response = await requestUrl({
      url,
      headers: {
        'Authorization': `Bearer ${this.apiKey}`,
        'Content-Type': 'application/json'
      },
      ...options
    });
    
    if (response.status !== 200) {
      throw new Error(`API Error: ${response.status}`);
    }
    
    return response.json as T;
  }

  async get<T>(endpoint: string): Promise<T> {
    return this.request<T>(endpoint, { method: 'GET' });
  }

  async post<T>(endpoint: string, data: any): Promise<T> {
    return this.request<T>(endpoint, { 
      method: 'POST',
      body: JSON.stringify(data)
    });
  }
}

速率限制

typescript
class RateLimiter {
  private queue: Array<() => Promise<any>> = [];
  private processing = false;
  private minInterval: number;

  constructor(minInterval: number) {
    this.minInterval = minInterval;
  }

  async add<T>(fn: () => Promise<T>): Promise<T> {
    return new Promise((resolve, reject) => {
      this.queue.push(async () => {
        try {
          const result = await fn();
          resolve(result);
        } catch (error) {
          reject(error);
        }
      });
      this.process();
    });
  }

  private async process() {
    if (this.processing || this.queue.length === 0) return;
    
    this.processing = true;
    
    while (this.queue.length > 0) {
      const fn = this.queue.shift();
      if (fn) {
        await fn();
        await new Promise(resolve => setTimeout(resolve, this.minInterval));
      }
    }
    
    this.processing = false;
  }
}

// 使用
const limiter = new RateLimiter(1000); // 1秒间隔
const result = await limiter.add(() => fetchAPI('/data'));

命令系统进阶

条件命令

typescript
this.addCommand({
  id: 'format-active-note',
  name: 'Format current note',
  checkCallback: (checking: boolean) => {
    const file = this.app.workspace.getActiveFile();
    
    // 如果没有活动文件,命令不可用
    if (!file) return false;
    
    // 如果是 Markdown 文件才可用
    if (file.extension !== 'md') return false;
    
    // checking 模式只返回是否可用
    if (checking) return true;
    
    // 实际执行
    this.formatNote(file);
    return true;
  }
});

带参数的命令

typescript
// 使用模态框获取参数
this.addCommand({
  id: 'search-and-replace',
  name: 'Search and replace',
  callback: () => {
    new SearchReplaceModal(this.app, (search, replace) => {
      const editor = this.app.workspace.activeEditor?.editor;
      if (editor) {
        const content = editor.getValue();
        const newContent = content.replace(new RegExp(search, 'g'), replace);
        editor.setValue(newContent);
      }
    }).open();
  }
});

性能优化

虚拟列表

对于大量数据的渲染:

typescript
class VirtualList {
  private container: HTMLElement;
  private items: any[];
  private itemHeight: number;
  private visibleStart = 0;
  private visibleEnd = 0;

  constructor(container: HTMLElement, items: any[], itemHeight: number) {
    this.container = container;
    this.items = items;
    this.itemHeight = itemHeight;
    
    this.container.style.height = `${items.length * itemHeight}px`;
    this.container.style.overflow = 'auto';
    
    this.updateVisible();
    this.container.addEventListener('scroll', () => this.updateVisible());
  }

  private updateVisible() {
    const scrollTop = this.container.scrollTop;
    const viewportHeight = this.container.clientHeight;
    
    const newStart = Math.floor(scrollTop / this.itemHeight);
    const newEnd = Math.ceil((scrollTop + viewportHeight) / this.itemHeight);
    
    if (newStart !== this.visibleStart || newEnd !== this.visibleEnd) {
      this.visibleStart = newStart;
      this.visibleEnd = newEnd;
      this.render();
    }
  }

  private render() {
    const fragment = document.createDocumentFragment();
    
    for (let i = this.visibleStart; i <= this.visibleEnd && i < this.items.length; i++) {
      const item = this.items[i];
      const el = document.createElement('div');
      el.style.height = `${this.itemHeight}px`;
      el.style.position = 'absolute';
      el.style.top = `${i * this.itemHeight}px`;
      el.textContent = item.name;
      fragment.appendChild(el);
    }
    
    this.container.innerHTML = '';
    this.container.appendChild(fragment);
  }
}

延迟加载

typescript
// 使用 Intersection Observer 延迟加载
function createLazyLoader(container: HTMLElement, loadFn: () => Promise<HTMLElement>) {
  const observer = new IntersectionObserver((entries) => {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        loadFn().then(el => {
          container.appendChild(el);
        });
        observer.disconnect();
      }
    });
  });
  
  observer.observe(container);
}

测试

单元测试设置

typescript
// jest.config.js
module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
  testMatch: ['**/__tests__/**/*.test.ts'],
  moduleNameMapper: {
    'obsidian': '<rootDir>/__mocks__/obsidian.ts'
  }
};

// __mocks__/obsidian.ts
export class Plugin {
  app: any;
  settings: any;
  
  async loadData() { return {}; }
  async saveData() {}
  addCommand() {}
  registerView() {}
}

export class App {
  vault = {
    getMarkdownFiles: () => [],
    read: async () => '',
    create: async () => {}
  };
  workspace = {
    getActiveFile: () => null
  };
}

测试示例

typescript
// __tests__/plugin.test.ts
import MyPlugin from '../src/main';

describe('MyPlugin', () => {
  let plugin: MyPlugin;
  
  beforeEach(() => {
    plugin = new MyPlugin();
  });
  
  test('should load settings', async () => {
    await plugin.loadSettings();
    expect(plugin.settings).toBeDefined();
  });
  
  test('should save settings', async () => {
    plugin.settings.apiKey = 'test-key';
    await plugin.saveSettings();
    expect(plugin.settings.apiKey).toBe('test-key');
  });
});

发布前检查清单

markdown
- [ ] 代码编译无错误
- [ ] TypeScript 类型完整
- [ ] 设置正确保存和加载
- [ ] 命令正确注册和执行
- [ ] 错误处理完善
- [ ] 资源正确清理
- [ ] manifest.json 信息完整
- [ ] README.md 文档清晰
- [ ] 版本号正确
- [ ] CHANGELOG 已更新
- [ ] 兼容性测试通过

相关资源

最后更新:2026年4月4日编辑此页反馈问题