Obsidian UI 技术原理

在插件开发的过程中发现 Obsidian 有比较复杂的 UI 设计,不同的功能被归属到不同的类中,同时为了方便管理还引入了 workspace、leaf 等概念。为了能够更加高效的进行 Obsidian 插件开发和个性化定制,有必要对这一技术细节进行梳理总结。

UI 概览

Obsidian UI 中主要分为:

  1. Ribbon,上面主要是一些功能/插件的按钮
  2. Right Sidebar/Left Sidebar,左右侧边栏
  3. Tab view,主要的显示区域
  4. Editor,是 view 的成员,用来与 CodeMirror 交互操作 markdown 文本
  5. Status bar,是右下角的状态栏
overview of Obsidian UI

workspace

与 VS Code 类似,Obsidian 支持在任何时候配置哪些内容是否可见,例如在不需要时可以配置隐藏文件资源管理器,例如可以并排显示多个文档。整个 Obsidian 应用程序窗口中可见内容的配置称为工作区(workspace)。这就意味着,workspace 中会包含上文提到的所有元素,如 view、editor、sidebar、statusbar 等。

workspace 数据结构

workspace 的底层实现是一个树形结构,树的节点是 WorkspaceItem,它包括两种类型:WorkspaceParentWorkspaceLeaf

WorkspaceParent 与 WorkspaceLeaf 区别

  • WorkspaceParent 可以包括子节点(包括 WorkspaceParent 类型),有两种类型: WorkspaceSplitsWorkspaceTabs
  • WorkspaceLeaf 不包括任何子节点

WorkspaceSplit 与 WorkspaceTabs 区别

  • WorkspaceSplits 沿着垂直或水平方向一个接一个地排列它的子项
  • WorkspaceTabs 一次只显示一个子项,并隐藏其他的子项

workspace 示意图

workspace tree struct of Obsidian

Class 定义

上面提到的 workspace 相关的类型在 Obsidian API 中都有详细的定义,可以参考:obsidian-api/obsidian.d.ts

/**
 * @public
 */
export class WorkspaceLeaf extends WorkspaceItem {
    /**
     * @public
     */
    view: View;
    /**
     * By default, `openFile` will also make the leaf active.
     * Pass in `{ active: false }` to override.
     *
     * @public
     */
    openFile(file: TFile, openState?: OpenViewState): Promise<void>;
    /**
     * @public
     */
    open(view: View): Promise<View>;
    /**
     * @public
     */
    getViewState(): ViewState;
    /**
     * @public
     */
    setViewState(viewState: ViewState, eState?: any): Promise<void>;
    /**
     * @public
     */
    getEphemeralState(): any;
    /**
     * @public
     */
    setEphemeralState(state: any): void;
    /**
     * @public
     */
    togglePinned(): void;
    /**
     * @public
     */
    setPinned(pinned: boolean): void;
    /**
     * @public
     */
    setGroupMember(other: WorkspaceLeaf): void;
    /**
     * @public
     */
    setGroup(group: string): void;
    /**
     * @public
     */
    detach(): void;
    /**
     * @public
     */
    getIcon(): IconName;
    /**
     * @public
     */
    getDisplayText(): string;
    /**
     * @public
     */
    onResize(): void;
    /**
     * @public
     */
    on(name: 'pinned-change', callback: (pinned: boolean) => any, ctx?: any): EventRef;
    /**
     * @public
     */
    on(name: 'group-change', callback: (group: string) => any, ctx?: any): EventRef;
}

view

view 决定了 Obisidian 如何显示内容,文件资源管理器,Graph 视图和 Markdown 视图都是一种视图,在插件开发的过程中如果需要自定义显示的内容还可以创建自己的 ItemViewItemView 继承自 View,关于 View class 的具体定义可以参考:obsidian-api View

自定义 View

如下是一个扩展示例:

import { ItemView, WorkspaceLeaf } from "obsidian";
export const VIEW_TYPE_EXAMPLE = "example-view";

export class ExampleView extends ItemView {
  constructor(leaf: WorkspaceLeaf) {
    super(leaf);
  }
  getViewType() {
    return VIEW_TYPE_EXAMPLE;
  }
  getDisplayText() {
    return "Example view";
  }
  async onOpen() {
    const container = this.containerEl.children[1];
    container.empty();
    container.createEl("h4", { text: "Example view" });
  }
  async onClose() {
    // Nothing to clean up.
  }
}

view 和 workspace 的关系

根据 WorkspaceLeaf class 定义可以确认 view 是 WorkspaceLeaf 的成员,同样的 View 也引用了对应的 WorkspaceLeaf 成员。

实用 Obsidian UI 编程

1. 当前 workspace 检查

this.app.workspace.iterateAllLeaves((leaf) => {
	// 遍历 workspace 树形结构,可以获取没有节点对应的 view
	// view 可以对显示内容进行控制和管理
    console.log(leaf.getViewState().type);
});

2. view 生命周期

import { Plugin } from "obsidian";
import { ExampleView, VIEW_TYPE_EXAMPLE } from "./view";

export default class ExamplePlugin extends Plugin {
  async onload() {
    this.registerView(
      VIEW_TYPE_EXAMPLE,
      (leaf) => new ExampleView(leaf)
    );

    this.addRibbonIcon("dice", "Activate view", () => {
      this.activateView();
    });
  }
  async onunload() {
    this.app.workspace.detachLeavesOfType(VIEW_TYPE_EXAMPLE);
  }
  async activateView() {
    this.app.workspace.detachLeavesOfType(VIEW_TYPE_EXAMPLE);

    await this.app.workspace.getRightLeaf(false).setViewState({
      type: VIEW_TYPE_EXAMPLE,
      active: true,
    });
    this.app.workspace.revealLeaf(
      this.app.workspace.getLeavesOfType(VIEW_TYPE_EXAMPLE)[0]
    );
  }
}

开发实践

利用 QuickAdd 提供的执行 js 脚本的能力实现操作 UI 显示当前笔记对应的 graph 视图,效果如下:

0:00
/
local graph with Obsidian UI equipment

References

  1. User interface | Obsidian Plugin Developer Docs
  2. Workspace | Obsidian Plugin Developer Docs