Svelte 插件开发
Svelte 是开发 Obsidian 插件的热门框架选择,其编译时优化的特性和简洁的语法非常适合构建插件界面。本文介绍如何使用 Svelte 开发 Obsidian 插件。
为什么选择 Svelte?
| 特性 | Svelte | React | 原生 JS |
|---|---|---|---|
| 包体积 | 小(编译优化) | 较大 | 最小 |
| 学习曲线 | 低 | 中 | 低 |
| 响应式 | 内置 | 需 Hooks | 手动 |
| 模板语法 | 简洁 | JSX | 无 |
| 适合插件 | ✅ | ✅ | ✅ |
推荐 Svelte 的原因
- Svelte 编译后生成原生 JS,无需运行时框架,包体积小
- 响应式系统简单直观,不需要
useState/useEffect - 模板语法接近 HTML,上手快
- Obsidian 社区已有成熟的 Svelte 模板和工具链
环境搭建
使用官方模板
推荐使用 obsidian-svelte 模板创建项目:
bash
# 克隆模板
git clone https://github.com/ojsl/obsidian-svelte-template my-plugin
cd my-plugin
# 安装依赖
npm install
# 在 obsidian vault 中链接
npm run dev手动搭建
如果你已有插件项目,可以手动添加 Svelte 支持:
bash
# 安装 Svelte 相关依赖
npm install -D svelte svelte-preprocess esbuild-svelte
# 安装 TypeScript 支持(可选)
npm install -D @tsconfig/svelte typescript配置 esbuild
修改 esbuild.config.mjs,添加 Svelte 编译支持:
javascript
import esbuild from "esbuild";
import process from "process";
import builtins from "builtin-modules";
import esbuildSvelte from "esbuild-svelte";
import sveltePreprocess from "svelte-preprocess";
const prod = process.argv[2] === "production";
const context = await esbuild.context({
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",
plugins: [
esbuildSvelte({
compilerOptions: { css: true },
preprocess: sveltePreprocess(),
}),
],
});
if (prod) {
await context.rebuild();
process.exit(0);
} else {
await context.watch();
}配置 TypeScript
在 tsconfig.json 中添加 Svelte 支持:
json
{
"extends": "@tsconfig/svelte/tsconfig.json",
"compilerOptions": {
"types": ["svelte", "node"],
"target": "ES6",
"module": "ESNext",
"moduleResolution": "node",
"allowImportingTsExtensions": true,
"esModuleInterop": true
},
"include": ["**/*.ts", "**/*.svelte"],
"exclude": ["node_modules"]
}第一个 Svelte 组件
创建视图插件
在 Obsidian 中添加自定义视图:
typescript
// src/views/MyView.ts
import { ItemView, WorkspaceLeaf } from "obsidian";
import MyComponent from "./MyComponent.svelte";
import type MyPlugin from "../main";
export const VIEW_TYPE_MY = "my-plugin-view";
export class MyView extends ItemView {
private component: MyComponent | null = null;
constructor(leaf: WorkspaceLeaf, private plugin: MyPlugin) {
super(leaf);
}
getViewType(): string {
return VIEW_TYPE_MY;
}
getDisplayText(): string {
return "My Plugin";
}
getIcon(): string {
return "star";
}
async onOpen() {
const container = this.containerEl.children[1];
container.empty();
container.createEl("h2", { text: "My Plugin View" });
this.component = new MyComponent({
target: container as HTMLElement,
props: {
plugin: this.plugin,
app: this.app,
},
});
}
async onClose() {
this.component?.$destroy();
this.component = null;
}
}创建 Svelte 组件
svelte
<!-- src/views/MyComponent.svelte -->
<script lang="ts">
import type { App, Plugin } from "obsidian";
import { onMount } from "svelte";
export let plugin: Plugin;
export let app: App;
let noteCount = 0;
let searchText = "";
onMount(async () => {
// 加载初始数据
noteCount = app.vault.getMarkdownFiles().length;
});
function handleSearch() {
// 使用 Obsidian API 进行搜索
const query = searchText.trim();
if (query) {
app.internalPlugins.plugins["global-search"].instance.openSearch(query);
}
}
</script>
<div class="my-plugin-container">
<h3>📊 仓库统计</h3>
<p>笔记总数: {noteCount}</p>
<div class="search-box">
<input
type="text"
bind:value={searchText}
placeholder="搜索笔记..."
on:keydown={(e) => e.key === "Enter" && handleSearch()}
/>
<button on:click={handleSearch}>搜索</button>
</div>
</div>
<style>
.my-plugin-container {
padding: 16px;
}
.search-box {
display: flex;
gap: 8px;
margin-top: 12px;
}
.search-box input {
flex: 1;
padding: 6px 10px;
border: 1px solid var(--background-modifier-border);
border-radius: 4px;
background: var(--background-primary);
color: var(--text-normal);
}
.search-box button {
padding: 6px 16px;
background: var(--interactive-accent);
color: var(--text-on-accent);
border: none;
border-radius: 4px;
cursor: pointer;
}
.search-box button:hover {
background: var(--interactive-accent-hover);
}
</style>核心开发模式
使用 Obsidian API
Svelte 组件中可以直接使用 Obsidian API:
svelte
<script lang="ts">
import type { App } from "obsidian";
import { Notice } from "obsidian";
export let app: App;
async function createNote() {
const name = "New Note " + Date.now();
const file = await app.vault.create(`${name}.md`, "# " + name);
await app.workspace.openLinkText(name, "");
new Notice(`已创建: ${name}`);
}
</script>
<button on:click={createNote}>新建笔记</button>响应式数据
Svelte 的响应式系统让数据绑定更简洁:
svelte
<script lang="ts">
let count = 0;
let doubled = $: count * 2;
// 响应式语句
$: if (count > 10) {
console.log("Count exceeded 10!");
}
function increment() {
count += 1;
}
</script>
<button on:click={increment}>
点击了 {count} 次 (双倍: {doubled})
</button>使用 Obsidian 设置
在 Svelte 组件中使用插件设置:
svelte
<script lang="ts">
import type MyPlugin from "../main";
export let plugin: MyPlugin;
// 从设置中获取值
let settingValue = $: plugin.settings.mySetting;
async function updateSetting(value: string) {
plugin.settings.mySetting = value;
await plugin.saveSettings();
}
</script>
<div class="setting-item">
<div class="setting-item-info">
<div class="setting-item-name">我的设置</div>
<div class="setting-item-description">配置说明</div>
</div>
<div class="setting-item-control">
<input
type="text"
value={settingValue}
on:change={(e) => updateSetting(e.currentTarget.value)}
/>
</div>
</div>设置面板组件
使用 Svelte 构建设置面板:
svelte
<!-- src/settings/SettingsTab.svelte -->
<script lang="ts">
import type MyPlugin from "../main";
import { Notice } from "obsidian";
export let plugin: MyPlugin;
let apiKey = plugin.settings.apiKey || "";
let maxResults = plugin.settings.maxResults || 10;
let enableFeature = plugin.settings.enableFeature ?? true;
async function save() {
plugin.settings.apiKey = apiKey;
plugin.settings.maxResults = maxResults;
plugin.settings.enableFeature = enableFeature;
await plugin.saveSettings();
new Notice("设置已保存");
}
async function reset() {
apiKey = "";
maxResults = 10;
enableFeature = true;
await save();
}
</script>
<div class="settings-container">
<h2>插件设置</h2>
<div class="setting-item">
<div class="setting-item-info">
<div class="setting-item-name">API Key</div>
<div class="setting-item-description">输入你的 API 密钥</div>
</div>
<div class="setting-item-control">
<input type="password" bind:value={apiKey} />
</div>
</div>
<div class="setting-item">
<div class="setting-item-info">
<div class="setting-item-name">最大结果数</div>
<div class="setting-item-description">搜索返回的最大结果数量</div>
</div>
<div class="setting-item-control">
<input type="number" bind:value={maxResults} min={1} max={100} />
</div>
</div>
<div class="setting-item">
<div class="setting-item-info">
<div class="setting-item-name">启用功能</div>
<div class="setting-item-description">开启或关闭此功能</div>
</div>
<div class="setting-item-control">
<input type="checkbox" bind:checked={enableFeature} />
</div>
</div>
<div class="setting-item-control">
<button on:click={save}>保存设置</button>
<button on:click={reset}>恢复默认</button>
</div>
</div>模态框组件
svelte
<!-- src/modals/ConfirmModal.svelte -->
<script lang="ts">
import type { App } from "obsidian";
export let app: App;
export let title = "确认操作";
export let message = "确定要执行此操作吗?";
export let onConfirm: () => void;
export let onCancel: () => void;
</script>
<div class="modal-container">
<div class="modal-title">{title}</div>
<div class="modal-content">{message}</div>
<div class="modal-button-container">
<button class="mod-cancel" on:click={onCancel}>取消</button>
<button class="mod-cta" on:click={onConfirm}>确认</button>
</div>
</div>typescript
// src/modals/ConfirmModal.ts
import { App, Modal } from "obsidian";
import ConfirmComponent from "./ConfirmModal.svelte";
export class ConfirmModal extends Modal {
private component: ConfirmComponent | null = null;
constructor(
app: App,
private title: string,
private message: string,
private onConfirm: () => void
) {
super(app);
}
onOpen() {
this.component = new ConfirmComponent({
target: this.contentEl,
props: {
app: this.app,
title: this.title,
message: this.message,
onConfirm: () => {
this.onConfirm();
this.close();
},
onCancel: () => this.close(),
},
});
}
onClose() {
this.component?.$destroy();
}
}样式与主题适配
使用 Obsidian CSS 变量
Obsidian 提供了大量 CSS 变量,确保你的组件与主题适配:
svelte
<style>
.container {
background: var(--background-primary);
color: var(--text-normal);
border: 1px solid var(--background-modifier-border);
border-radius: var(--radius-s);
padding: var(--size-4-4);
}
.button {
background: var(--interactive-accent);
color: var(--text-on-accent);
border-radius: var(--radius-s);
padding: var(--size-4-1) var(--size-4-3);
}
.button:hover {
background: var(--interactive-accent-hover);
}
.input {
background: var(--background-modifier-form-field);
color: var(--text-normal);
border: 1px solid var(--background-modifier-border);
}
</style>常用 CSS 变量参考
| 变量 | 用途 |
|---|---|
--background-primary | 主背景色 |
--background-secondary | 次背景色 |
--text-normal | 正常文本色 |
--text-muted | 弱化文本色 |
--interactive-accent | 交互高亮色 |
--background-modifier-border | 边框色 |
--radius-s | 小圆角 |
--size-4-4 | 间距单位 |
暗色/亮色模式适配
svelte
<style>
.card {
background: var(--background-secondary);
border: 1px solid var(--background-modifier-border);
}
/* 使用 CSS 变量自动适配暗色/亮色 */
.highlight {
background: var(--text-highlight-bg);
}
</style>进阶技巧
使用 Svelte Store 管理状态
svelte
<!-- src/stores.ts -->
<script lang="ts">
import { writable, derived } from "svelte/store";
export const notes = writable<string[]>([]);
export const searchQuery = writable("");
export const filteredNotes = derived(
[notes, searchQuery],
([$notes, $searchQuery]) => {
if (!$searchQuery) return $notes;
return $notes.filter((n) =>
n.toLowerCase().includes($searchQuery.toLowerCase())
);
}
);
</script>svelte
<!-- 在组件中使用 Store -->
<script lang="ts">
import { notes, searchQuery, filteredNotes } from "../stores";
</script>
<input type="text" bind:value={$searchQuery} placeholder="过滤笔记..." />
<ul>
{#each $filteredNotes as note}
<li>{note}</li>
{/each}
</ul>生命周期与 Obsidian 事件
svelte
<script lang="ts">
import type { App } from "obsidian";
import { onMount, onDestroy } from "svelte";
export let app: App;
let eventRef: EventRef | null = null;
onMount(() => {
// 监听 Obsidian 事件
eventRef = app.vault.on("modify", (file) => {
console.log("文件修改:", file.path);
});
});
onDestroy(() => {
// 清理事件监听
if (eventRef) {
app.vault.offref(eventRef);
}
});
</script>条件渲染与动画
svelte
<script lang="ts">
let showDetails = false;
let items = ["Item 1", "Item 2", "Item 3"];
function addItem() {
items = [...items, `Item ${items.length + 1}`];
}
function removeItem(index: number) {
items = items.filter((_, i) => i !== index);
}
</script>
<button on:click={() => (showDetails = !showDetails)}>
{showDetails ? "隐藏" : "显示"}详情
</button>
{#if showDetails}
<div transition:slide={{ duration: 200 }}>
<ul>
{#each items as item, index}
<li>
{item}
<button on:click={() => removeItem(index)}>删除</button>
</li>
{/each}
</ul>
<button on:click={addItem}>添加</button>
</div>
{/if}调试技巧
开发者工具
- 在 Obsidian 中按
Ctrl/Cmd + Shift + I打开开发者工具 - 在 Sources 面板中找到你的 Svelte 组件
- 设置断点进行调试
Svelte DevTools
bash
# 安装 Svelte DevTools 浏览器扩展
# 在 Obsidian 中使用 --remote-debugging-port 标志启动常见问题
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 组件不渲染 | target 元素不存在 | 确保在 onOpen 后挂载 |
| 样式不生效 | CSS 作用域问题 | 使用 :global() 或内联样式 |
| 内存泄漏 | 未清理组件 | 在 onClose 中调用 $destroy() |
| 构建错误 | Svelte 预处理缺失 | 检查 esbuild 配置 |
发布注意事项
包体积优化
javascript
// esbuild.config.mjs
{
// 开启 tree-shaking
treeShaking: true,
// 压缩输出
minify: true,
// 排除 Obsidian API
external: ["obsidian", "electron", ...builtins],
}manifest.json
确保 manifest.json 正确配置:
json
{
"id": "my-svelte-plugin",
"name": "My Svelte Plugin",
"version": "1.0.0",
"minAppVersion": "1.0.0",
"description": "A plugin built with Svelte",
"author": "Your Name",
"authorUrl": "https://github.com/yourusername",
"isDesktopOnly": false
}相关资源
- 插件开发入门 — 基础开发指南
- 插件开发完整教程 — 详细开发教程
- API 参考 — Obsidian API 文档
- Svelte 官方文档 — Svelte 框架文档
- obsidian-svelte 模板 — 项目模板