Skip to content

Svelte 插件开发

Svelte 是开发 Obsidian 插件的热门框架选择,其编译时优化的特性和简洁的语法非常适合构建插件界面。本文介绍如何使用 Svelte 开发 Obsidian 插件。

为什么选择 Svelte?

特性SvelteReact原生 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}

调试技巧

开发者工具

  1. 在 Obsidian 中按 Ctrl/Cmd + Shift + I 打开开发者工具
  2. 在 Sources 面板中找到你的 Svelte 组件
  3. 设置断点进行调试

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
}

相关资源