辛宝Otto

辛宝Otto 的玄酒清谈

北漂前端程序员儿 / 探索新事物 / Web Worker 主播之一/内向话痨
xiaoyuzhou
email

速通 Obsidian Docs - 側重插件開發

image.png

內部關聯:

  • [[【技術角度折騰 xlog】更順暢的使用體驗 3 實現一個 obsidian 插件]]

關聯:

官方內容很多:

  • Plugin 插件開發,本次核心
  • Theme 主題開發。本次忽略
  • Refer 列舉 api
  • 其他

可以翻到下面看快速代碼片段

插件開發#

我想通過介紹一個純 js 的簡單插件來講解其結構和基本用法。這個插件非常實用,可以幫助我們更好地開發和調試。為了方便大家使用,我建議先安裝一個名為 "hot-reload" 的插件,你可以在 https://github.com/pjeby/hot-reload 上找到它。

對於使用 vue 的用戶,我還推薦一個模板,它結合了 vue3、vite4 和 obsidian 插件,可以讓你更輕鬆地開始開發。你可以在 https://github.com/Otto-J/Obsidian-Vue-Starter 上找到這個模板。

在講解插件的具體用法之前,我想先介紹一下插件的生命週期。插件的生命週期包括 onload 和 onunload 兩個階段。

移動端插件忽略。

使用 vue 開發的頁面是掛在到 ItemView 上的。這意味著我們可以在 ItemView 中使用 vue 的各種功能來開發自定義頁面。

  • 開始
    • 通過介紹一個純 js 的簡單插件,介紹結構和基本用法
    • 一定盡早安裝 https://github.com/pjeby/hot-reload 方便開發和調試
    • vue 用戶用這個模板,vue3+vite4+obsidian plugin
    • 生命週期 onload - onunload
    • 移動端插件,提供了如何開啟模擬,知道就行
    • 使用 vue 開發的頁面是掛在到 ItemView 上的
  • UI
    • UI 結構介紹,對齊不同區域的叫法
      • 放出一張圖,介紹了不同的位置,sidebar 叫 ribbon actions 有點奇怪
      • 這是圖片Pasted image 20230909121152
    • 【命令面板】
      • 類似 vsCode 使用 cmd + p 可以調出命令面板 command palette,插件開發在 onload 裡調用 this.addCommand 方法註冊,不複雜,比如處理當前激活編輯器的內容
      • 命令可以加前置判斷,使用 checkCallback 方法,先校驗是否運行,都加上把,省心
      • 可以訪問編輯器,有 editorCallback 方法,參數裡有 edtor, view 兩個參數,當然也有 editorCheckCallback 選項
      • 可以註冊 hotkeys,增加一種觸發方式。細節忽略
    • 【右鍵菜單】
      • 導入obsidian.Menu,實例化 menu.addItem 可以補充,這是放到 sidebar 裡的
      • 也可以放到 文件列表 file-menu 和 editor-menu 編輯器右上角Pasted image 20230909125832
    • 【html 容器】可以在插件設置裡放 html 容器,在 PluginSettingTab 裡
      • 可以用傳統的 js createEl ,類似於 h 渲染函數。
      • 插件根目錄的 styles.css,用 vue 吧省事
    • 【icons】從這裡面找,https://lucide.dev/ 支持 flutter 挺好
      • 自定義的圖標需要準備 svg 原內容,不用管 vue icons 一套走
    • 【modals】彈窗讓用戶輸入信息
      • 從 Obsidian.Modal 導入 interface,需要實現 onOpen 和 onClose 方法
      • 點擊提交時候有對應的回調
      • 還有一種 SuggestModal 輔助填寫,和 FuzzySuggestModal 先忽略
    • 【sidebar】官方叫法是 ribbon actions
      • 用法就是 demo 一定會提到的 this.addRibbonIcon
    • 【settings】也就是公開的設置,插件填寫配置的地方
      • 插件的設置是持久化的,所以流程都是先載入 this.loadData 合併得到配置結果
      • 官方實例的代碼和文檔一樣
    • 【status bar】右下角的狀態欄
      • this.addStatusBarItem 添加
      • 可以添加多個
    • 【views】資源管理、編輯器、預覽都是 view
      • 獲取 view ID getViewType
    • 【workspace】沒看懂,樹狀結構
      • 可以並排打開過個文檔,分割 split 的方式
      • 看不懂,文檔寫的爛
      • 關鍵詞是 Leaf 啥啥啥
      • 直接看下面的快捷操作的代碼片段把
  • Editor
    • 【Editor】Class 獲取、編輯模式的 MD
      • 看下面的代碼 p
    • 【Markdown 後處理】預覽模式下的 md 是 html,可以調整 html
      • 例子 1,把 替換為 emoji 圖標
      • 例子 2,把特殊語法的內容解析、添加,哦,就是數據轉化,比如 csv 轉成 table
    • 【編輯器拓展】可以先不看,介紹 CodeMirror6 的內容
      • 底層依賴的是 codeMirror6 也就是寫拓展,這裡不看了
      • 【裝飾】給上面編輯器拓展的內容補充樣式更沒關
        • 先不看
      • 【State fields】編輯器拓展的,看不懂
      • 【State management】編輯器拓展的狀態管理
        • 如果要撤銷,需要保留狀態
      • 【View Plugins】編輯器拓展插件,可以訪問 viewport 忽略
      • 【viewport】高性能的編輯器少不了虛擬滾動,避免卡頓,滾動文檔時候 viewport 會更新重新計算
      • 【編輯器通信】 忽略
  • Releasing 發布
    • CI 操作 - 文檔提供了一個 Github action ,用來輔助構建插件
    • 提交到官方市場之前,提供 Readme.md 許可證、mainfest.json 官方示例都有
    • 按照文檔進行操作就行,官方維護了一個 json,提 pr 就行
    • 注意 node 和 electron 只允許桌面使用
    • 如果是 beta 版本,可以考慮 brat 插件
  • Event
    • 我們可能想要定時器,比如 setTimeInterval 用來試試更新一些東西,這裡有一個專門的 this.registerInterval 方法 this.registerInterval( window.setInterval(() => this.updateStatusBar(), 1000) );
    • 時間格式化,內置了 obsidian.moment
  • Valut
    • 筆記的集合,封裝了這些 fs 操作
    • 文檔只讀不操作用 cachedRead ()
    • 文檔讀取並修改可以 read ()
    • 修改文件使用 vault.modify () 這個是覆蓋操作
    • valut.process 回調地址裡有 data 表示當前內容,只支持同步操作
    • 異步修改考慮 vault.cachedRead () - 異步操作 - vault.process () 更新
    • 刪除文件 trash () 是垃圾箱,徹底刪除是 delete ()
    • app.vault.getAbstractFileByPath("folderOrFile") 绝对路径的结果可能是文件、文件夹,通过 instanceof TFile/ TFolder 判断

常用片段#

有兩種方法可以讀取文件的內容:read() 和 cachedRead()。

  • 如果只想向用戶顯示內容,則 用於避免多次從磁碟讀取文件。cachedRead()
  • 如果要讀取內容,請更改它,然後將其寫回磁碟,然後使用 以避免可能用過時的副本覆蓋文件。read()
// 獲取激活文本的內容
app.workspace.getActiveFileView().data

// 也可以
app.workspace.activeEditor

// 用戶激活的光標位置
app.workspace.getActiveFileView().editor.getCursor()
// {line:2,ch:2}

// 指定位置追加文本 比如當前日期啥的,或者 template
app.workspace.getActiveFileView().editor.replaceRange('222',{line: 2, ch: 2})

// 正在激活的編輯器
const editor = app.workspace.getActiveFileView().editor

// 用戶選中了一部分內容
app.workspace.getActiveFileView().editor.getSelection()

// 替換內容,比如上傳後變成 cdn地址sha
editor.replaceSelection('xxx')

// 獲取所有md文檔
app.vault.getMarkdownFiles()

// 獲取所有文件,之後過濾
app.vault.getFile()

// 獲取文本內容
var file = app.vault.getMarkdownFiles()[0]
const content = await app.vault.cachedRead(file)

// 修改內容,這是同步操作
app.vault.process(file, (data) => { return data.replace(":)", "🙂"); })


this.app.workspace.on('editor-paste', (evt, editor, view) => {
if (evt.clipboardData) {}
}))

// 可以通過獲得當前文件 file
this.app.workspace.on("file-menu", (menu, file) => {
if (file instanceof TFile) {}
});

// 給定一個字符串路徑,判斷文件是否存在,更安全
const isExist = await this.app.vault.adapter.exists('xx.md')

// 給定字符串,查找最佳匹配文件
app.metadataCache.getFirstLinkpathDest('文件夾/下載.png','')

// 得到 TFile 讀取內容
app.metadataCache.getFileCache(file)
// 其中 embeds 表示關聯的圖片,但不包含外鏈標準語法,要結合使用
// frontmatter 是其中的 fm
// frontmatterPosition 是 fm 的起止位置,可以刪除後替換
// links 表示當前文章的外鏈


// 讀取圖片二進制內容,後面把 blob 展示和上傳
const conArrayBuffer = await plugin.app.vault.readBinary(TFile);
// 轉成二進制,通過 post 上傳
const blob = new Blob([conArrayBuffer], {
    type: "image/" + obInnerFile.extension,
});



操作 front-matter 比較常見和麻煩,提供一個常見方案

export const useObsidianFrontmatter = (file: TFile, app: App) => {
  // 使用更具語義化的函數名
  const doesFileExist = () => !!app.metadataCache.getFileCache(file);

  const currentFrontMatter = () =>
    app.metadataCache.getFileCache(file)?.frontmatter ?? {};
  const addOrUpdateFrontMatter = async (obj: Record<string, string>) => {
    const fileCache = app.metadataCache.getFileCache(file);
    // 如果文件不存在,直接返回
    if (!fileCache) {
      new Notice("文件不存在");
      return;
    }

    const currentFrontMatter = fileCache?.frontmatter ?? {};
    const newFrontMatter = `---\n${stringifyYaml({
      ...currentFrontMatter,
      ...obj,
    })}\n---\n`;

    // const { frontmatterPosition } = fileCache;
    // readcontent or con = vault.cachedRead(file)
    const fileContents = await app.vault.read(file);

    const frontmatterPosition = fileCache.frontmatterPosition ?? {
      start: {
        line: 0,
        col: 0,
        offset: 0,
      },
      end: {
        line: 0,
        col: 0,
        offset: 0,
      },
    };

    // 這裡邏輯比較繞,目的是重寫文件內容,後面如果有 api 可能就一行代碼解決了,類似 metadataCache.update
    const {
      start: { offset: deleteFrom },
      end: { offset: deleteTo },
    } = frontmatterPosition;

    const newFileContents =
      fileContents.slice(0, deleteFrom) +
      newFrontMatter +
      fileContents.slice(deleteTo);

    await app.vault.modify(file, newFileContents);
  };

  return {
    doesFileExist,
    addOrUpdateFrontMatter,
    currentFrontMatter,
  };
};

ts api 快速熟悉#

官方文檔寫的特別差,周邊也沒看到有好的介紹。

快速羅列

  • abstract-input-suggest 在 input 輸入框裡給一些提示的,比如歷史的 tags 之類的
  • abstract-text-component 看着是文本輸入框相關的抽象類
  • app 應用級別的屬性,可以拿到文件、vault、熱鍵、工作區之類的
  • base-component 可以設置 disabled
  • class BlockCache extends CacheItem
  • block-sub-path-result
  • button-component 按鈕組件,不用看,統一封裝
  • cached-metadata--interface
  • cache-item--interface
  • closeable-component--class 提供關閉方法,不用看,也都封裝
  • ColorComponent--class,顏色選擇器,可以獲得顏色 hex,可以被封裝
  • Commond--interface 註冊命令相關的方法
  • component--class 核心入口組件
  • data-adapter--interface,處理文件和溫酒,推薦 Vault 的 api
  • data-write-options--interface
  • debouncer--interface
  • dropdown-component--class 可以封裝
  • Editor--class,抹平 codemirror 版本差異的接口,應該可以提供很多輔助功能
  • editor-change--interface
  • editor-position--interface
  • editor-range--interface
  • editor-range-or-caret--interface
  • editor-scroll-info
  • editorxxx 略過
  • events--class 事件相關
  • extra-button-component--class 被封裝
  • file-manager--class,從 UI 角度管理文件創建、刪除、重命名
  • file-stats--interface
  • file-system-adapter implements DataAdapter--class
載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。