插件开发完整教程
🛠️ 从零开始开发一个完整的 Obsidian 插件,涵盖开发环境、核心 API、调试技巧和发布流程。
准备工作
环境要求
| 工具 | 版本要求 | 说明 |
|---|---|---|
| Node.js | >= 16.x | JavaScript 运行时 |
| 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(); // 刷新设置面板
}));
}
}第五步:调试技巧
使用开发者工具
- 打开 Obsidian
- 按
Ctrl/Cmd + Shift + I打开开发者工具 - 在 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
## 功能测试
- [ ] 命令正常执行
- [ ] 设置正确保存
- [ ] 视图正常显示
- [ ] 菜单项正常工作
- [ ] 快捷键正常响应
## 兼容性测试
- [ ] 桌面端正常
- [ ] 移动端正常(如适用)
- [ ] 暗色主题适配
- [ ] 亮色主题适配
## 边界测试
- [ ] 空输入处理
- [ ] 特殊字符处理
- [ ] 大量数据处理
- [ ] 网络异常处理第七步:发布
准备发布
- 更新版本号
- 更新 CHANGELOG
- 运行构建命令
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提交到社区插件库
- Fork obsidian-releases
- 编辑
community-plugins.json - 添加你的插件信息:
json
{
"id": "my-plugin",
"repo": "your-username/my-plugin",
"branch": "main"
}- 创建 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 事件