关于 obsidian
面向 Obsidian 插件开发者的开发规范技能,汇总 ESLint 规则与提交要求,涵盖生命周期、API、界面、兼容性和无障碍实践,并提供新插件脚手架生成器。
主要功能
- 01生成最小插件脚手架
- 02涵盖提交与命名规则
- 03指导生命周期与 API 使用
- 04提供 UI、样式及无障碍规范
适用场景
- 01创建 Obsidian 插件项目
- 02检查插件代码及提交流程
- 03处理设置、文件和事件实践
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ gapmiss/ obsidian-plugin-skill --skill obsidian安装后在 Claude Code 中输入/obsidian确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
reference/accessibility.mdcode-quality.mdcommunity-scanner.mdcss-styling.mdeslint-setup.mdfile-operations.mdmemory-management.mdsubmission.mdtype-safety.mdui-ux.md详细介绍
Obsidian 插件开发指南
请遵循以下综合指南,这些指南基于官方 Obsidian ESLint 插件规则、提交要求和最佳实践。
入门
快速入门工具
对于新的插件项目,可以使用交互式样板生成器:
- 脚本:技能仓库中的
tools/create-plugin.js - 命令:使用 Agent 的调用方式(
/create-plugin、$create-plugin或@create-plugin)调用create-plugin - 生成精简且符合最佳实践的样板代码,不包含示例代码
- 检测现有项目,并且只添加缺少的文件
当用户询问如何创建新插件、想要启动新项目或需要帮助搭建基本结构时,建议使用样板生成器。
规则参考(eslint-plugin-obsidianmd v0.4.2)
提交与命名
| # | 规则 | ✅ 应做 | ❌ 不应做 |
|---|---|---|---|
| 1 | 插件 ID | 省略“obsidian”;不要以“plugin”结尾 | 包含“obsidian”或以“plugin”结尾 |
| 2 | 插件名称 | 省略“Obsidian”;不要以“Plugin”结尾 | 包含“Obsidian”或以“Plugin”结尾 |
| 3 | 插件名称 | 不要以“Obsi”开头或以“dian”结尾 | 以“Obsi”开头或以“dian”结尾 |
| 4 | 描述 | 省略“Obsidian”、“This plugin”等字样 | 使用“Obsidian”或“This plugin” |
| 5 | 描述 | 以 .?!) 标点结尾 | 描述末尾不加标点 |
内存与生命周期
| # | 规则 | ✅ 应做 | ❌ 不应做 |
|---|---|---|---|
| 6 | 事件清理 | 使用 registerEvent() 自动清理 | 注册事件但不清理 |
| 6a | DOM 事件 | 在插件或所属组件上使用 registerDomEvent() | 将 addEventListener 与手动 removeEventListener 清理配对使用 |
| 7 | 视图引用 | 直接返回视图/组件 | 将视图引用存储在插件属性中,或将插件作为组件传递给 MarkdownRenderer |
| 8 | 叶子分离 | 让 Obsidian 处理叶子清理 | 在 onunload 中调用 detachLeavesOfType() |
类型安全
| # | 规则 | ✅ 应做 | ❌ 不应做 |
|---|---|---|---|
| 9 | TFile/TFolder | 使用 instanceof 进行类型检查 | 强制转换为 TFile/TFolder;使用 any;使用 var |
| 10 | DOM instanceof | 对 DOM 节点/UIEvents 使用 .instanceOf(T) | 使用 instanceof 进行跨窗口 DOM 检查 |
UI/UX
| # | 规则 | ✅ 应做 | ❌ 不应做 |
|---|---|---|---|
| 11 | UI 文本 | 使用句首字母大写格式——“Advanced settings”;专有名称使用 acronyms/brands 选项 | 使用标题式大小写——“Advanced Settings” |
| 12 | JSON 本地化文件 | 在 JSON 本地化文件中使用句首字母大写格式(recommendedWithLocalesEn) | 在本地化 JSON 中使用标题式大小写 |
| 13 | TS/JS 本地化文件 | 在 TS/JS 本地化模块中使用句首字母大写格式 | 在本地化模块中使用标题式大小写 |
注意(v0.4.0):
ui/sentence-case现已启用(warn),并会检查内联 UI 字符串;它在 v0.3.0 中处于禁用状态。使用recommendedWithLocalesEn配置还可以检查英文本地化文件(规则 12–13)。 | 14 | 命令名称 | 在命令名称/ID 中省略“command” | 在名称/ID 中包含“command” | | 15 | 命令 ID | 在命令 ID/名称中省略插件 ID/名称 | 在命令 ID 中重复插件 ID | | 16 | 热键 | 不设置默认热键 | 设置默认热键 | | 17 | 设置标题 | 使用.setHeading()| 手动创建 HTML 标题;在标题中使用“General”、“settings”或插件名称 |
声明式设置(1.13.0+)
recommended 中的全部 4 条 settings-tab 规则均以 warn 级别提供。规则 17a/17c/17d 会从 manifest.json 读取 minAppVersion;17b 不受版本限制。
| # | 规则 | ✅ 应做 | ❌ 不应做 |
|---|---|---|---|
| 17a | settings-tab/require-display | 当 minAppVersion < 1.13.0 时保留 display() | 仅提供声明式设置,导致旧版 Obsidian 上无法渲染任何内容 |
| 17b | settings-tab/prefer-setting-definitions | 为每个 PluginSettingTab 实现 getSettingDefinitions() | 仅依赖 display()——设置不会出现在 1.13+ 的全局搜索中 |
| 17c | settings-tab/prefer-update-over-display | 调用 this.update() 重新渲染声明式设置 | 调用 this.display()——当定义非空时会绕过它 |
| 17d | settings-tab/no-deprecated-display | 当 minAppVersion >= 1.13.0 且定义存在时删除 display() | 留下无用的 display()(可自动修复) |
| — | 设置数据 | 将所有持久化数据保存在 plugin.settings 中 | 通过 saveData() 存储同级键——自动持久化会覆盖它们 |
检测注意事项: 这些规则仅匹配直接
extends PluginSettingTab的情况。extends obsidian.PluginSettingTab不在检测范围内,不会被标记——但相关指导仍然适用。
API 最佳实践
| # | 规则 | ✅ 应做 | ❌ 不应做 |
|---|---|---|---|
| 18 | 活动文件编辑 | 使用 Editor API | 使用 Vault.modify() 编辑活动文件 |
| 19 | 后台文件修改 | 使用 Vault.process() | 使用 Vault.modify() 进行后台修改 |
| 20 | 文件删除 | 使用 FileManager.trashFile() | 直接使用 Vault.trash() 或 Vault.delete() |
| 21 | 文件查找 | 使用 Vault.getAbstractFileByPath() | 使用 Vault.getFiles().find() 遍历所有文件 |
| 22 | 用户路径 | 使用 normalizePath() | 将 .obsidian 路径硬编码;使用未经处理的用户路径 |
| 23 | 操作系统检测 | 使用 Platform API | 使用 navigator.platform/userAgent |
| 24 | 网络请求 | 使用 requestUrl() | 使用 fetch() |
| 24a | 响应类型处理 | 解析 response.text,并在 API 边界处验证/转换一次 | 将 response.json(类型为 any)传入插件代码 |
| 25 | 日志记录 | 尽量减少控制台日志;生产环境中不要在 onload/onunload 中记录日志 | 在 onload/onunload 中使用 console.log |
| 26 | 输入建议 | 使用内置的 AbstractInputSuggest | 复制 Liam 的 TextInputSuggest 实现 |
| 27 | API 兼容性 | 检查 API 是否可用的 minAppVersion(例如,getSettingDefinitions() 需要 1.13.0) | 使用声明的 minAppVersion 中不可用的 API |
| 28 | 语言检测 | 使用 Obsidian 的 getLanguage() | 使用 localStorage.getItem('language') 或 i18next-browser-languagedetector |
弹出窗口兼容性
| # | 规则 | ✅ 应做 | ❌ 不应做 |
|---|---|---|---|
| 29 | 文档/窗口 | 使用 activeDocument 和 activeWindow | 使用全局 document 和 window |
| 29a | Getter 捕获 | 如果之后需要使用同一文档,请将 activeDocument 捕获到变量中 | 在设置时和清理时分别调用 activeDocument——它会随焦点变化,可能返回不同的文档 |
| 30 | 定时器 | 使用 window.setTimeout()、window.setInterval()、window.requestAnimationFrame() 等(规则 29 的例外) | 使用裸露的 setTimeout()/setInterval(),或使用 activeWindow.setTimeout()——prefer-window-timers 会标记这两种写法 |
| 31 | 主工作区 UI | 从设置中使用 this.app.workspace.containerEl.ownerDocument | 从设置窗口使用 activeDocument 更新主工作区 |
注意(v0.4.0):
prefer-active-doc默认仍处于禁用状态——它是唯一以off状态发布的 Obsidian 规则。要支持弹出窗口,请手动启用它。
注意(v1.13.0): 设置现已在新窗口中打开。设置回调中的
activeDocument指向设置窗口,而非主库。要定位主工作区 UI,请使用this.app.workspace.containerEl.ownerDocument。
注意:
activeDocument/activeWindow是动态 Getter,会跟踪当前聚焦的窗口。在设置时通过activeDocument.addEventListener()添加的监听器,在清理时无法可靠地通过activeDocument.removeEventListener()移除。建议使用registerDomEvent()(规则 6a),它会在注册时捕获目标。
事件处理
| # | 规则 | ✅ 应做 | ❌ 不应做 |
|---|---|---|---|
| 31 | 编辑器拖放/粘贴 | 检查 evt.defaultPrevented,并调用 evt.preventDefault() | 不检查 defaultPrevented 就处理编辑器拖放/粘贴 |
样式
| # | 规则 | ✅ 应做 | ❌ 不应做 |
|---|---|---|---|
| 32 | CSS 变量 | 所有样式都使用 Obsidian CSS 变量 | 将颜色、尺寸或间距硬编码 |
| 33 | CSS 作用域 | 将 CSS 限定在插件容器内 | 使用范围过广的 CSS 选择器 |
| 34 | 样式元素 | 使用 styles.css 文件(no-forbidden-elements) | 创建 <link> 或 <style> 元素;通过 JavaScript 赋予样式 |
| 34a | !important | 提高选择器优先级或使用 CSS 变量 | 使用 !important——这会覆盖用户主题/代码片段 |
| 34b | :has 选择器 | 条件变化时通过 TypeScript 切换类 | 使用 :has——这会导致选择器大范围失效并引发性能问题 |
安全性与兼容性
| # | 规则 | ✅ 应做 | ❌ 不应做 |
|---|---|---|---|
| 35 | DOM 创建 | 通过 prefer-create-el 使用 Obsidian DOM 辅助方法(createEl()、createDiv()、createSpan()、createSvg()、createFragment());代码检查器会自动将 activeDocument.createElement() 修正为 activeWindow.createEl()(v0.4.1) | 使用 document.createElement()、document.createDocumentFragment() 等 |
| 36 | Node.js 模块 | 使用 Platform.isDesktop 检查保护 Node.js 导入(no-nodejs-modules) | 不进行平台检查就导入 Node.js 模块 |
| 37 | iOS 兼容性 | 避免使用正则表达式后行断言(与 iOS < 16.4 不兼容) | 使用正则表达式后行断言 |
无障碍(强制要求)
| # | 规则 | ✅ 应做 | ❌ 不应做 |
|---|---|---|---|
| 38 | 键盘访问 | 确保所有交互元素都可通过键盘访问;可使用 Tab 遍历所有元素 | 创建无法访问的交互元素 |
| 39 | ARIA 标签 | 为图标按钮提供 ARIA 标签;使用 data-tooltip-position 设置工具提示 | 使用没有 ARIA 标签的图标按钮 |
| 40 | 焦点指示器 | 使用带有 Obsidian CSS 变量的 :focus-visible;触控目标 ≥ 44×44px | 移除焦点指示器;使触控目标 < 44×44px |
代码质量
| 规则 | ✅ 应该做 | ❌ 不应该做 |
|---|---|---|
| 示例代码 | 删除所有示例/模板代码 | 保留 MyPlugin、SampleModal 这类类名 |
| Object.assign | 使用 Object.assign({}, defaults, overrides)(object-assign) | 使用 Object.assign(defaultsVar, other)——这会修改 defaults |
| LICENSE | 版权持有者不能是 "Dynalist Inc.";年份必须是当前年份(validate-license) | 将 "Dynalist Inc." 留作版权持有者,或使用过时的年份 |
| 异步操作 | 使用 async/await | 使用 Promise 链 |
| 已弃用的包 | 将被标记的 npm 包替换为 Node.js 内置模块(例如,builtin-modules → import { builtinModules } from "node:module") | 使用扫描器标记为可替换的包 |
详细指南
有关特定主题的完整信息,请参阅参考文件:
内存管理与生命周期
- 使用
registerEvent()、addCommand()、registerDomEvent()、registerInterval() registerDomEvent()与手动使用addEventListener的区别(以及activeDocument漂移错误)- 避免在插件中引用视图
- 不要将插件用作组件
- 正确清理 leaf
类型安全
- 使用
instanceof,而不是类型转换 - 避免使用
any类型 - 使用
const和let,而不是var
UI/UX 标准
- 强制使用句首字母大写格式(TypeScript、JSON locale、TS/JS locale 模块)
- 使用
recommendedWithLocalesEn配置检查 locale 文件 - 命令命名约定(不包含 "command"、插件名称或插件 ID)
- 设置和配置最佳实践
- 声明式设置 API(1.13+):迁移路径、控件类型、注意事项
文件与库操作
- 视图访问模式
- Editor API 与 Vault API
- 原子文件操作
- 文件管理
- 路径处理
CSS 样式最佳实践
- 避免使用内联样式
- 使用 Obsidian CSS 变量
- 避免使用
!important(改用选择器特异性或 CSS 变量) - 避免使用
:has选择器(改为通过 TypeScript 切换类) - 限定插件样式的作用范围
- 主题支持
- 间距与布局
无障碍(A11y)
- 键盘导航(必须)
- ARIA 标签和角色(必须)
- 工具提示与无障碍
- 焦点管理(必须)
- 焦点可见样式(必须)
- 屏幕阅读器支持(必须)
- 移动设备和触控无障碍(必须)
- 无障碍检查清单
代码质量与最佳实践
- 删除示例代码
- 安全最佳实践
- 平台兼容性
- API 使用最佳实践
- async/await 模式
- DOM 辅助工具
- 已弃用/可替换的包(例如,
builtin-modules→node:module)
插件提交要求
- 仓库结构
- 提交流程
- 语义化版本控制
- 测试检查清单
- 补充资源与重要说明
社区插件扫描器
- 扫描器运行的内容(ESLint 规则集及 ESLint 之外的检查)
- 评分卡系统(健康状况、审核、信息披露、改进建议)
- 标注版本——随着扫描器迭代,只需更新这一个文件
ESLint 设置指南
- 符合社区扫描器要求的完整 ESLint 配置
- 为什么必须使用
typescript-eslintrecommendedTypeChecked - 常见违规及修复方法(未处理的 Promise、require 导入等)
- 弹出窗口兼容性规则
插件提交验证工作流
提交插件前,请按以下顺序操作:
- 运行 ESLint — 使用
eslint-plugin-obsidianmd执行npx eslint .;修复所有错误和警告(警告会影响你的评分卡) - 验证清单文件 — 确认
id、name、description、version和minAppVersion符合命名和格式规则(规则 1–5) - 检查 LICENSE — 版权持有者不能是 "Dynalist Inc.",年份必须是当年
- 在移动设备上测试 — 确认没有正则表达式后行断言、没有
fetch(),且触控目标 ≥ 44×44px(仅当插件声明为仅支持桌面时可跳过) - 键盘无障碍审查 — 使用 Tab 键遍历所有交互元素;确认存在焦点指示器和 ARIA 标签
- 创建 GitHub Release — 标签必须与
manifest.json中的版本一致;附上main.js、manifest.json和styles.css(可选) - 通过 community.obsidian.md 提交 — 登录,关联 GitHub 账号,前往 Plugins → New plugin,输入仓库 URL,查看开发者政策,然后提交
如果修复后 ESLint 报告了新错误,请从第 1 步重新运行。
评分卡系统
已发布的插件会获得一个显示在 community.obsidian.md 上的评分卡。评分卡会影响用户信任度和可发现性——评分较低会阻碍用户安装。
要点:
- 总分目标为 90% 以上
- 修复所有 ESLint 警告,而不仅仅是错误——警告会公开显示
- 使用
typescript-eslint/recommendedTypeChecked进行类型感知检查 - 为发布版本添加 GitHub 工件证明
有关扫描器检查、健康度指标、审查检查、常见警告和改进建议的完整详情,请参阅 社区插件扫描器。
审查/编写代码时
使用此清单进行代码审查和实现:
- 内存管理:组件和视图是否得到了妥善管理?
- 类型安全:是否使用
instanceof而不是类型转换? - UI 文本:所有文本是否都使用句式大小写?
- 命令命名:是否没有多余的词?
- 文件操作:是否使用了推荐的 API?
- 移动设备兼容性:是否没有与 iOS 不兼容的功能?
- 示例代码:是否已移除所有样板代码?
- 清单文件:版本是否正确,结构是否有效?
- 无障碍性:是否支持键盘导航,是否有 ARIA 标签和焦点指示器?
- 测试:不使用鼠标也能使用插件吗?
- 触控目标:所有交互元素是否至少为 44×44px?
- 焦点样式:是否使用了
:focus-visible和合适的 CSS 变量? - 设置:在 1.13+ 上是否使用声明式
getSettingDefinitions()?所有已保存的数据是否都集中在plugin.settings中?
常见模式
正确注册命令
// ✅ CORRECT
this.addCommand({
id: 'insert-timestamp',
name: 'Insert timestamp',
editorCallback: (editor: Editor, view: MarkdownView) => {
editor.replaceSelection(new Date().toISOString());
}
});
安全缩窄类型
// ✅ CORRECT
const file = this.app.vault.getAbstractFileByPath(path);
if (file instanceof TFile) {
// TypeScript now knows it's a TFile
await this.app.vault.read(file);
}
支持键盘操作的按钮
// ✅ CORRECT
const button = containerEl.createEl('button', {
attr: {
'aria-label': 'Open settings',
'data-tooltip-position': 'top'
}
});
button.setText('⚙️');
button.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
performAction();
}
});
声明式设置(1.13+)
// ✅ CORRECT — getSettingDefinitions() replaces display() on 1.13+
getSettingDefinitions() {
return [
{
name: 'Mode',
control: {
type: 'dropdown',
key: 'mode',
defaultValue: 'fast',
options: { fast: 'Fast', thorough: 'Thorough' },
},
},
];
}
主题化 CSS
/* ✅ CORRECT */
.my-plugin-modal {
background: var(--modal-background);
color: var(--text-normal);
padding: var(--size-4-4);
border-radius: var(--radius-m);
font-size: var(--font-ui-medium);
}
.my-plugin-button:focus-visible {
outline: 2px solid var(--interactive-accent);
outline-offset: 2px;
}
协助开发 Obsidian 插件时,请主动应用这些规则,并根据这些指南提出改进建议。有关具体主题的完整信息,请参阅详细参考文件。