Obsidian 个人插件开发纪实——0x02
8 min read

Obsidian 个人插件开发纪实——0x02

阶段性总结 Obsidian Plugin 插件开发过程中学习到的东西、碰到的印象深刻的问题、后续重点思考和解决的问题等。
Obsidian 个人插件开发纪实——0x02
Photo by Yancy Min / Unsplash

Obsidian 插件开发已经阶段性 Release 并且提交社区 Review,作为阶段性总结我打算梳理一下这个过程中学习到的东西、碰到的印象深刻的问题、后续重点思考和解决的问题等。

插件开发带来的成长

作为自己第一个纯开源的前端技术栈的项目,这里面碰到的技术栈对我来讲几乎都是全新的,项目开发基本处于边学边用的状态。这里面很多 ABC 的内容很多对我一个搞基础 OS 安全的工程师来讲很新鲜,作为积累做一个简单的总结。

Typescript 项目工具链

Nodejs 研发环境

我的 Nodejs 研发环境采用的是 VSCode + SSH Remote + Docker container,SSH Remote 可以在 VSCode 的插件市场找到,Docker container 可以参考下面的 Dockerfile。

# Nodejs develop environment for macbook M1/M2.
# if you are running in x86 arch, change the `--platrfrom` as amd64 or x86_64
FROM --platform=arm64 ubuntu:latest

RUN apt update && apt install openssh-server sudo git vim make curl -y

# install Node 18.x(LTS)
RUN curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash - && sudo apt-get install -y nodejs
# install yarn
RUN npm install --global yarn

RUN echo "export PS1='[\[\e[32m\]\u@\[\e[36m\]\h \[\e[35m\]\W\[\e[37m\]:\[\e[31m\]\#\[\e[0m\]]\$ \n>> '" >> /root/.bashrc

# add init git ssh config tools
RUN echo '#!/bin/bash
mkdir -p /root/.ssh && cp /root/ssh-host/id_rsa_ob /root/.ssh/ && cp /root/ssh-host/id_rsa_ob.pub /root/.ssh/ && cp /root/ssh-host/config /root/.ssh/' > /root/init-git-ssh.sh
RUN chmod u+x /root/init-git-ssh.sh

RUN echo "PasswordAuthentication yes" >> /etc/ssh/sshd_config

RUN echo "PermitRootLogin yes" >> /etc/ssh/sshd_config

RUN ssh-keygen -A

RUN service ssh start

EXPOSE 22

CMD ["/usr/sbin/sshd","-D"]

特别说明:init-git-ssh.sh 工具是将本地 .ssh 配置复制到 Nodejs 开发环境中,主要体现在 container 启动的时候增加了一个挂载参数:-v ~/.ssh:/root/ssh-host

npm、npx、yarn

关于 Nodejs 包管理有几个常见的命令行:npm、npx、yarn,在插件开发过程中对这个三个工具有了一定的理解。首先对它们进行简单的分类:

  1. npm、yarn,Nodejs 包管理器;
  2. npx,Nodejs 包执行器;
npm 与 yarn

npm 作为 Nodejs 原生的包管理器毋庸置疑是不可或缺的,同时由于历史原因 npm 自身也存在一些缺陷:

同一个项目,安装的时候无法保持一致性。由于 package.json 文件中版本号的特点,下面三个版本号在安装的时候代表不同的含义:

  • “5.0.3”表示安装指定的5.0.3版本
  • “~5.0.3”表示安装5.0.X中最新的版本
  • “^5.0.3”表示安装5.X.X中最新的版本

这就麻烦了,常常会出现同一个项目,有的同事是 OK 的,有的同事会由于安装的版本不一致出现 bug。

安装的时候,包会在同一时间下载和安装,中途某个时候,一个包抛出了一个错误,但是 npm 会继续下载和安装其他包。因为 npm 会把所有的日志输出到终端,有关错误包的错误信息就会在一大堆 npm 打印的警告中丢失掉,并且你甚至永远不会注意到实际发生的错误。

npx 与 npm

中文网站有很多关于 npm 和 npx 的比较,大多都陷入细节的比较,我认为从字面上就能很好的理解两者的关系:

npx, Run a command from a local or remote npm package

npx 就是一个 Javascript 包的执行器
npm, The package manager for the Node JavaScript platform

npm 就是一个 Javascript 包的管理器

package.json

Javascript/Typescript 项目进行管理涉及到依赖、打包、发布等,这些管理和配置的时候均需要用到 package.json,它定义了一套规范可以方便的是用 npm 工具链进行管理。

具体 package.json 的规范字段可以参考:package.json docs

esbuild

esbuild 其实代表的是 Javascript 的打包器,打包器的作用主要有三个:

  1. 把多个 js 文件合并成一个 js 文件
  2. 为了支持 ES6 需要将新的 js 代码编程浏览器支持的老的 js 代码,Typescript 代码需要变成 js 代码,js 中需要支持其他类型的文件(例如 css),出于上述的需求需要打包器
  3. 混淆代码、简化代码(参考 mini js)需要用到打包器

常见的 js 打包器有:

  • grunt, gulp
  • vite
  • snowpack
  • parcel
  • esbuild
  • rollup
  • webpack

Github 项目工作流

GitHub Actions

Actions 是 Github 提供的一个 CI/CD 平台,对于开源社区项目的 SDLC 具有非常重要的意义,例如利用 Actions 进行自动化的测试、发布、部署等。由于有丰富的社区 workflow 支持,Github Action 有非常多的使用场景,例如松烟阁的自动发布 webhook 就是借助 Actions 来完成的。

开源项目的软件研发生命周期中包括测试、发布等自动化任务是保证软件质量,提升效率的重要手段,Github Actions 正好提供这方面能力,简单易用,只要按照语法定义编写 yaml 文件描述自己的任务即可完成自动化执行。

Project

开源项目托管在 Github 上研发时常用到的东西包括:issue,PR,milestone,Projects 等。这些能力我打算从项目管理的角度来讲述一下:

  • issue 用来做需求分析、功能设计、Bug 上报等;
  • Pull Request(PR) 用来往项目的主干分支提交代码,通常 PR 需要与 issue 关联,Github 通过 Projects 的 workflow 可以配置自动关联;
  • Projects 是 Github 项目管理的看板,有助了解项目研发进展、人力投入、优先级安排等;
  • milestone 用于管理版本发布的时间节点;

印象深刻的问题

const object 可以修改

插件代码在测试的过程碰到一个诡异的问题,const 变量的内容可以被修改。问出题的代码块如下,DEFAULT_SETTINGS 变量在经过 Obsidian 设置之后默认值被修改了。这个诡异的现象不难排查和解决,这种问题一般是考虑变量赋值和引用导致的,令我印象深刻的地方在于 Javascript/Typescript const 变量居然是可以改变的,详细的内容可以参考这里:const object and arrays

export const DEFAULT_SETTINGS: PluginManagerSettings = {
    debug: true,
    targetPath: "2.fleeting/fleeting-thoughts/",
    fileFormat: "YYYY-MM-DD",
    localGraph: {
        notice: "show current note grah view",
        type: "popover",
        depth: 2,
        showTags: true,
        showAttach: true,
        showNeighbor: true,
        collapse: false,
        resizeStyle: {
            width: 550,
            height: 500,
            left:475,
            top: 255
        }
    },
    memos: {
        resizeStyle: {
            width: 550,
            height: 500,
            left:475,
            top: 255
        }
    },
    enableGraphColors: false,
    colorGroups: [
        {
            query: "./",
            color: {
                a: 1,
                rgb: 6617700,
            }
        }
    ]
}

// ...
new Setting(containerEl)
    .setName(nameEl)
    .setDesc('This will be the Color used in the graph view.')
    .addText(text => {
        text
        .setValue(plugin.settings.colorGroups[idx].query)
        .onChange(async (value) => {
            plugin.settings.colorGroups[idx].query = value;
            await this.plugin.saveSettings();
        })})
    .addButton(btn => {
        .setButtonText("Change Color");
            new Picker({
                parent: btn.buttonEl,
                onDone: async (color) => {
                    // hex format color: #00000000, [0] '#', [1-6] rgb, [7-8] alpha
                    let hexColor = color.hex.split('#')[1];
                    this.plugin.log(hexColor);
                    this.plugin.log("length= ", hexColor.length);
                    // only get the color value without alpha, obsidian set alpha as 0xff by default
                    if(hexColor.length === 8) {
                        hexColor = hexColor.substring(0, 6);
                    }
                    this.plugin.log("hexColor without alpha = ", hexColor);
                    this.plugin.settings.colorGroups[idx].color.rgb = parseInt(hexColor, 16);
                        await this.plugin.saveSettings();
                        this.display();
                    },
                    popup: "left",
                    color: colorRgb,
                    alpha: false,
                });
    })
    .addExtraButton(btn => {
	    btn.setIcon("trash").setTooltip("Remove").onClick(async () => {
        this.plugin.settings.colorGroups.remove(colorGroup);
            await this.plugin.saveSettings();
            this.display();
        });
        if (this.plugin.settings.colorGroups.length === 1) {
	        btn.setDisabled(true);
        }
    })
    .addExtraButton(btn => {
        btn.setIcon("reset").setTooltip("Reset to default").onClick(async () => {
            this.plugin.settings.colorGroups[idx] = DEFAULT_SETTINGS.colorGroups[0] ?? "#ffffff";
            await this.plugin.saveSettings();
            this.display();
        });
    })

typescript 项目 release

可能因为是初次接触 Typescript 的项目,我发现一个非常有用的命令行工具:yarn version,yarn 内置的 yarn version 命令会提供一个交互式命令行会话让你升级当前包的版本并且更新 package.json 文件对应的字段。

另外,还有一个非常 tricky 的地方,就是利用 yarn run 有一个特性就是 built-in 命令行优先级高于 package.json 中定义的同名 script,这样就可以实现一个功能:一个命令行就可以的实现交互式升级版本同时执行自定义脚本,例如更新文件、创建 commit 等。

Stay tuned

随着我的插件提交 Obsidian Community Review,阶段性的工作已经完成,后面打算做好该插件的运营和迭代,这意味着自己在插件产品设计和个人技术提升上需要多下点功夫思考和学习,下一次的插件开发纪实会主要总结一下自己在这两大部分的思考:

  1. 产品设计
  • 插件的价值到底是什么
  • 插件产品设计怎么做

2. 前端技术

  • HTML/CSS 知识点很多,常见问题怎么解决需要有一个可查找的知识库
  • Typescript 项目开发和管理
  • 代码质量,lint、测试框架、e2e 测试等
  • nodejs

References

  1. npx | npm Docs
  2. 前端核心工具:yarn、npm、cnpm三者如何优雅的在一起使用?
  3. package.json | npm Docs
  4. 文件 package.json 的说明文档
  5. javascript 的打包工具
  6. esbuild - An extremely fast bundler for the web

Public discussion