Skip to content

插件开发完整教程

🛠️ 从零开始开发一个完整的 Obsidian 插件,涵盖开发环境、核心 API、调试技巧和发布流程。

准备工作

环境要求

工具版本要求说明
Node.js>= 16.xJavaScript 运行时
npm / pnpm最新版包管理器
VS Code推荐代码编辑器
Git最新版版本控制
Obsidian最新版测试环境

安装依赖

bash
# 检查 Node.js 版本
node -v

# 检查 npm 版本
npm -v

第一步:创建项目

方法一:使用模板

bash
# 克隆官方模板
git clone https://github.com/obsidianmd/obsidian-sample-plugin my-plugin
cd my-plugin

# 安装依赖
npm install

方法二:手动创建

bash
# 创建项目目录
mkdir my-plugin
cd my-plugin

# 初始化项目
npm init -y

# 安装开发依赖
npm install -D typescript @types/node esbuild
npm install obsidian

项目结构

my-plugin/
├── main.ts              # 插件入口
├── styles.css           # 插件样式(可选)
├── manifest.json        # 插件清单
├── versions.json        # 版本兼容性
├── package.json         # NPM 配置
├── tsconfig.json        # TypeScript 配置
└── .eslintrc            # ESLint 配置(可选)

第二步:配置文件

manifest.json

json
{
  "id": "my-plugin",
  "name": "My Plugin",
  "version": "1.0.0",
  "minAppVersion": "0.15.0",
  "description": "一个示例插件",
  "author": "你的名字",
  "authorUrl": "https://your-website.com",
  "isDesktopOnly": false
}

package.json

json
{
  "name": "my-plugin",
  "version": "1.0.0",
  "description": "一个示例插件",
  "main": "main.js",
  "scripts": {
    "dev": "node esbuild.config.mjs",
    "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
    "version": "node version-bump.mjs && git add manifest.json versions.json"
  },
  "keywords": ["obsidian", "plugin"],
  "devDependencies": {
    "@types/node": "^20.0.0",
    "esbuild": "^0.20.0",
    "obsidian": "latest",
    "typescript": "^5.0.0"
  }
}

tsconfig.json

json
{
  "compilerOptions": {
    "baseUrl": ".",
    "inlineSourceMap": true,
    "inlineSources": true,
    "module": "ESNext",
    "target": "ES6",
    "allowJs": true,
    "noImplicitAny": true,
    "moduleResolution": "node",
    "importHelpers": true,
    "isolatedModules": true,
    "strictNullChecks": true,
    "lib": ["DOM", "ES5", "ES6", "ES7"]
  },
  "include": ["**/*.ts"]
}

esbuild.config.mjs

javascript
import esbuild from "esbuild";
import process from "process";
import builtins from "builtin-modules";

const banner = `/*
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
if you want to view the source, please visit the github repository of this plugin
*/
`;

const prod = process.argv[2] === "production";

const context = await esbuild.context({
  banner: {
    js: banner,
  },
  entryPoints: ["main.ts"],
  bundle: true,
  external: [
    "obsidian",
    "electron",
    "@codemirror/autocomplete",
    "@codemirror/collab",
    "@codemirror/commands",
    "@codemirror/language",
    "@codemirror/lint",
    "@codemirror/search",
    "@codemirror/state",
    "@codemirror/view",
    "@lezer/common",
    "@lezer/highlight",
    "@lezer/lr",
    ...builtins,
  ],
  format: "cjs",
  target: "es2018",
  logLevel: "info",
  sourcemap: prod ? false : "inline",
  treeShaking: true,
  outfile: "main.js",
});

if (prod) {
  await context.rebuild();
  process.exit(0);
} else {
  await context.watch();
}

第三步:编写插件

最小可运行插件

typescript
// main.ts
import { App, Plugin, PluginSettingTab, Setting } from 'obsidian';

// 定义设置接口
interface MyPluginSettings {
  mySetting: string;
  enableFeature: boolean;
}

// 默认设置
const DEFAULT_SETTINGS: MyPluginSettings = {
  mySetting: 'default value',
  enableFeature: true
};

// 插件主类
export default class MyPlugin extends Plugin {
  settings: MyPluginSettings;

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

    // 添加命令
    this.addCommand({
      id: 'open-sample-modal',
      name: '打开示例弹窗',
      callback: () => {
        new SampleModal(this.app).open();
      }
    });

    // 添加 Ribbon 图标
    this.addRibbonIcon('star', '示例插件', (evt: MouseEvent) => {
      new Notice('Hello, Obsidian!');
    });

    // 添加状态栏项目
    const statusBarItemEl = this.addStatusBarItem();
    statusBarItemEl.setText('状态栏文本');

    // 注册设置面板
    this.addSettingTab(new SampleSettingTab(this.app, this));
  }

  onunload() {
    console.log('卸载插件');
  }

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

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

// 弹窗组件
class SampleModal extends Modal {
  constructor(app: App) {
    super(app);
  }

  onOpen() {
    const { contentEl } = this;
    contentEl.setText('这是一个示例弹窗');
  }

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

// 设置面板
class SampleSettingTab 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('设置名称')
      .setDesc('设置描述')
      .addText(text => text
        .setPlaceholder('输入文本')
        .setValue(this.plugin.settings.mySetting)
        .onChange(async (value) => {
          this.plugin.settings.mySetting = value;
          await this.plugin.saveSettings();
        }));

    new Setting(containerEl)
      .setName('启用功能')
      .setDesc('启用或禁用某个功能')
      .addToggle(toggle => toggle
        .setValue(this.plugin.settings.enableFeature)
        .onChange(async (value) => {
          this.plugin.settings.enableFeature = value;
          await this.plugin.saveSettings();
        }));
  }
}

第四步:核心功能实现

添加命令

typescript
// 简单命令
this.addCommand({
  id: 'hello-world',
  name: 'Hello World',
  callback: () => {
    new Notice('Hello, World!');
  }
});

// 编辑器命令
this.addCommand({
  id: 'insert-timestamp',
  name: '插入时间戳',
  editorCallback: (editor: Editor, view: MarkdownView) => {
    const timestamp = new Date().toISOString();
    editor.replaceSelection(timestamp);
  }
});

// 检查条件的命令
this.addCommand({
  id: 'format-note',
  name: '格式化笔记',
  checkCallback: (checking: boolean) => {
    const file = this.app.workspace.getActiveFile();
    if (file && file.extension === 'md') {
      if (!checking) {
        this.formatFile(file);
      }
      return true;
    }
    return false;
  }
});

添加 Ribbon 图标

typescript
// 添加左侧边栏图标
this.addRibbonIcon('star', '收藏', (evt: MouseEvent) => {
  // 处理点击
  this.toggleFavorite();
});

// 使用 Lucide 图标
// 常用图标:star, heart, bookmark, settings, search, edit, trash

添加菜单项

typescript
// 文件菜单
this.registerEvent(
  this.app.workspace.on('file-menu', (menu, file) => {
    menu.addItem((item) => {
      item
        .setTitle('我的插件操作')
        .setIcon('star')
        .onClick(() => {
          this.handleFile(file);
        });
    });
  })
);

// 编辑器菜单
this.registerEvent(
  this.app.workspace.on('editor-menu', (menu, editor, view) => {
    menu.addItem((item) => {
      item
        .setTitle('插入模板')
        .setIcon('template')
        .onClick(() => {
          this.insertTemplate(editor);
        });
    });
  })
);

创建视图

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

const VIEW_TYPE = 'my-plugin-view';

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

  getViewType(): string {
    return VIEW_TYPE;
  }

  getDisplayText(): string {
    return '我的视图';
  }

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

  async onOpen() {
    const container = this.containerEl.children[1];
    container.empty();
    container.createEl('h4', { text: '我的视图' });
    
    // 添加内容
    const content = container.createDiv();
    content.createEl('p', { text: '这是视图内容' });
  }

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

// 在插件中注册
this.registerView(VIEW_TYPE, (leaf) => new MyView(leaf));

// 添加打开视图的命令
this.addCommand({
  id: 'open-my-view',
  name: '打开我的视图',
  callback: () => {
    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
class MySettingTab extends PluginSettingTab {
  plugin: MyPlugin;

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

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

    // 标题
    containerEl.createEl('h2', { text: '插件设置' });

    // 文本输入
    new Setting(containerEl)
      .setName('API 密钥')
      .setDesc('输入你的 API 密钥')
      .addText(text => text
        .setPlaceholder('输入密钥')
        .setValue(this.plugin.settings.apiKey)
        .onChange(async (value) => {
          this.plugin.settings.apiKey = value;
          await this.plugin.saveSettings();
        }));

    // 下拉选择
    new Setting(containerEl)
      .setName('主题')
      .setDesc('选择默认主题')
      .addDropdown(dropdown => dropdown
        .addOption('light', '亮色')
        .addOption('dark', '暗色')
        .addOption('auto', '自动')
        .setValue(this.plugin.settings.theme)
        .onChange(async (value) => {
          this.plugin.settings.theme = value;
          await this.plugin.saveSettings();
        }));

    // 开关
    new Setting(containerEl)
      .setName('启用功能')
      .setDesc('启用或禁用某功能')
      .addToggle(toggle => toggle
        .setValue(this.plugin.settings.enabled)
        .onChange(async (value) => {
          this.plugin.settings.enabled = value;
          await this.plugin.saveSettings();
        }));

    // 滑块
    new Setting(containerEl)
      .setName('字体大小')
      .setDesc('调整字体大小')
      .addSlider(slider => slider
        .setLimits(10, 24, 1)
        .setValue(this.plugin.settings.fontSize)
        .setDynamicTooltip()
        .onChange(async (value) => {
          this.plugin.settings.fontSize = value;
          await this.plugin.saveSettings();
        }));

    // 按钮
    new Setting(containerEl)
      .setName('重置设置')
      .setDesc('将所有设置恢复为默认值')
      .addButton(button => button
        .setButtonText('重置')
        .setWarning()
        .onClick(async () => {
          this.plugin.settings = { ...DEFAULT_SETTINGS };
          await this.plugin.saveSettings();
          this.display(); // 刷新设置面板
        }));
  }
}

第五步:调试技巧

使用开发者工具

  1. 打开 Obsidian
  2. Ctrl/Cmd + Shift + I 打开开发者工具
  3. 在 Console 中查看日志

调试代码

typescript
// 使用 console.log
console.log('调试信息:', this.settings);

// 使用 console.table
console.table(this.app.vault.getMarkdownFiles());

// 使用 console.time
console.time('操作耗时');
// ... 执行操作
console.timeEnd('操作耗时');

热重载

typescript
// 在开发模式下启用热重载
if (process.env.NODE_ENV === 'development') {
  this.registerEvent(
    this.app.vault.on('modify', (file) => {
      if (file.path === 'main.js') {
        this.app.plugins.disablePlugin('my-plugin');
        this.app.plugins.enablePlugin('my-plugin');
      }
    })
  );
}

使用 Hot Reload 插件

安装 Hot Reload 插件,自动重载修改后的代码。


第六步:测试

手动测试清单

markdown
## 功能测试
- [ ] 命令正常执行
- [ ] 设置正确保存
- [ ] 视图正常显示
- [ ] 菜单项正常工作
- [ ] 快捷键正常响应

## 兼容性测试
- [ ] 桌面端正常
- [ ] 移动端正常(如适用)
- [ ] 暗色主题适配
- [ ] 亮色主题适配

## 边界测试
- [ ] 空输入处理
- [ ] 特殊字符处理
- [ ] 大量数据处理
- [ ] 网络异常处理

第七步:发布

准备发布

  1. 更新版本号
  2. 更新 CHANGELOG
  3. 运行构建命令
bash
npm run build

发布到 GitHub

bash
# 创建发布标签
git tag 1.0.0
git push origin 1.0.0

# 在 GitHub 创建 Release
# 上传 main.js, manifest.json, styles.css

提交到社区插件库

  1. Fork obsidian-releases
  2. 编辑 community-plugins.json
  3. 添加你的插件信息:
json
{
  "id": "my-plugin",
  "repo": "your-username/my-plugin",
  "branch": "main"
}
  1. 创建 Pull Request

📚 API 速查

常用 API

typescript
// App 对象
this.app.workspace           // 工作区
this.app.vault              // 文件库
this.app.metadataCache      // 元数据缓存
this.app.fileManager        // 文件管理器

// 工作区
this.app.workspace.getActiveFile()           // 获取当前文件
this.app.workspace.getLeaf()                 // 获取叶子节点
this.app.workspace.openLinkText()            // 打开链接

// 文件库
this.app.vault.getMarkdownFiles()            // 获取所有 Markdown 文件
this.app.vault.read(file)                    // 读取文件
this.app.vault.modify(file, content)         // 修改文件
this.app.vault.create(path, content)         // 创建文件
this.app.vault.delete(file)                  // 删除文件
this.app.vault.rename(file, newPath)         // 重命名文件

// 元数据
this.app.metadataCache.getFileCache(file)    // 获取文件缓存
this.app.metadataCache.getLinks()            // 获取所有链接
this.app.metadataCache.getBacklinksForFile() // 获取反向链接

生命周期

typescript
onload()    // 插件加载时
onunload()  // 插件卸载时

注册方法

typescript
this.registerCommand()           // 注册命令
this.registerView()              // 注册视图
this.registerEvent()             // 注册事件
this.registerMarkdownPostProcessor()  // 注册 Markdown 处理器
this.registerEditorExtension()   // 注册编辑器扩展
this.registerDomEvent()          // 注册 DOM 事件

🔗 相关阅读

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