likes
comments
collection
share

猎豹Cheetah插件开发 - uTools 篇

作者站长头像
站长
· 阅读数 3

本文主要讲解如何接入我们抽取的核心模块如何应用在 uTools 平台的插件开发中,并且介绍一下 uTools 插件开发用到的 API,以及 uTools 插件的发布流程。

项目配置

uTools 为开发者提供了一个工具插件,可以在插件市场直接搜索“uTools 开发者工具”安装,项目的新建、运行,发布都在这个插件内操作。

猎豹Cheetah插件开发 - uTools 篇

创建项目

安装完开发者工具后,直接在 uTools 输入框搜索打开开发者工具,点击左下角的新建项目,打开项目创建面板,根据提示完成项目创建。

猎豹Cheetah插件开发 - uTools 篇

根据官网文档指引,需要为项目新建一个 plugin.json 文件,用于配置项目信息,功能入口等等。 开发者可以围绕 plugin.json 文件创建项目,使用自己熟悉的构建工具完成项目资源构建,plugin.json 中的 preload 字段指向构建结果中的入口文件即可。

plugin 配置

{
  "version": "1.0.0", // 插件版本
  "preload": "index.js", // 插件功能入口
  "logo": "assets/logo.png", // 插件 Logo
  "platform": [ // 插件支持的平台
    "win32", // Windows
    "darwin" // Mac OS
  ],
  "features": [ // 插件应用功能
    {
      "code": "open", // 功能唯一标识,搜索项目并使用与命令相关的软件打开
      "explain": "搜索并打开项目",
      "cmds": [ // 搜索这些字符都可以进入 open 功能,其值将作为 action 参数传入后续执行的函数
        "open",
        "git_gui_open",
        "terminal_open",
        "folder_open",
        "set_application",
        "编辑器",
        "Git应用",
        "终端",
        "文件夹",
        "设置项目默认应用"
      ]
    },
    {
      "code": "setting", // 功能唯一标识,打开插件设置面板
      "explain": "打开设置面板",
      "cmds": [ // 这俩命令都是打开设置面板
        "setting",
        "设置"
      ]
    }
  ]
}

设置面板

从上面配置中可以看出,除了 open 功能外,还有一个打开设置面板功能,这么做的原因是 uTools 的插件管理没有提供现成的偏好设置功能,一些个性化的配置想要用户自己输入时比较麻烦,所以这边添加了一个命令,用于打开咱们自己开发的配置面板,配置的数据可以存储在 uTools 提供的存储环境中。

命令面板可以使用任意框架开发,最后构建成 htmlcssjs 即可通过 uTools 提供的 API utools.createBrowserWindow 打开窗口。

插一句:uTools 基于 Electron 开发,熟悉 Electron 的小伙伴应该很容易上手。

下面看一看设置面板打开以后得样子:

猎豹Cheetah插件开发 - uTools 篇

文件结构

主要的功能文件使用 rollup.js 构建,设置面板使用 vite + vue3,输出到 package 目录下,将 plugin.json 也放置其内,处理好引用关系,插件的项目配置就基本完成了,结构如下:

cheetah-for-utools
├─ lib
│  ├─ common
│  │  ├─ constant.ts
│  │  ├─ core.ts
│  │  ├─ index.ts
│  │  └─ utils.ts
│  ├─ index.ts
│  └─ mount.ts
├─ package
│  ├─ assets
│  │  ├─ empty.png
│  │  ├─ logo.png
│  │  ├─ refresh.png
│  │  └─ type
│  │     ├─ android.png
│  │     ├─ applescript.png
│  │     ├─ dart.png
│  │     ├─ hexo.png
│  │     ├─ javascript.png
│  │     ├─ nuxt.png
│  │     ├─ react.png
│  │     ├─ react_ts.png
│  │     ├─ rust.png
│  │     ├─ typescript.png
│  │     ├─ unknown.png
│  │     ├─ vscode.png
│  │     └─ vue.png
│  ├─ plugin.json
├─ package.json
├─ rollup.config.js
├─ tsconfig.json
├─ types.d.ts
├─ vite.config.ts
└─ window
   ├─ index.html
   └─ src
      ├─ App.vue
      ├─ assets
      │  └─ logo.png
      ├─ components
      │  ├─ app-choose-item.vue
      │  ├─ cell-button.vue
      │  └─ workspace-manager.vue
      ├─ env.d.ts
      ├─ main.ts
      ├─ pages
      │  └─ index.vue
      └─ router
         └─ index.ts

rollup 的配置与核心篇大同小异,最大的区别是,构建结果不能压缩,因为发布时要保证 preload.js 明文可读,否则审核会不通过,审核人员会查看插件是否会造成对用户系统的危害。

引入核心模块

核心篇中已经阐述了如何开发、发布插件,并且已经将猎豹的核心模块 cheetah-core 发布到 npm,现在直接在 uTools 插件项目中安装使用即可:

npm install cheetah-core
# or
yarn add cheetah-core

功能实现

设置面板

上面提到设置面板主要是为了让用户进行一些插件功能的设置,主要包括下面这些选项。

工作区设置

工具将在以下工作区目录搜索 Git 项目,可配置多个,每个工作区目录可以包含多个 Git 项目。

默认编辑器

open 命令选择项目默认由此编辑器打开

Git GUI 应用

git_gui_open 命令选择项目由此应用打开

终端

terminal_open 命令选择项目由此终端打开

以上这些设置以后通过 utools.dbStorage.setItem 写入本地数据库,方便 open 命令执行时获取使用。

那么怎么打开设置面板呢?

preload.ts 中添加:

window.exports = {
  open: {...}, // open 命令后续讲解
  setting: {
    mode: 'none',
    args: {
      enter: (action: any) => {
        const settingWindow = utools.createBrowserWindow(
          './window/index.html',
          {
            show: false,
            title: '设置',
            resizable: false,
            minimizable: false,
            maximizable: false,
            height: 800,
            width: 500,
            webPreferences: {
              preload: 'index.js',
            },
          },
          () => {
            settingWindow.show();
          }
        );
        utools.hideMainWindow();
      },
    },
  },
}

上面的代码表示在 uTools 输入框中输入 setting 相关命令并运行后,使用 utools.createBrowserWindow 打开咱们构建的页面,并且载入 preload 文件为 rollup 构建的入口文件。

在入口文件中引入了 mount.ts 文件,其作用是向 window 对象挂载一系列工具函数:

// index.ts
import mount.ts
...

// mount.ts
import {
  chooseFile,
  chooseFolder,
  getValue,
  setValue,
  notice,
  getAllDefaultApp,
  setDefaultApp,
  platform,
  onClearCache
} from './common';

Object.assign(window, {
  platform,
  chooseFile,
  chooseFolder,
  notice,
  getValue,
  setValue,
  getAllDefaultApp,
  setDefaultApp,
  onClearCache,
});

这样在设置面板中就可以直接使用 window 上挂载的函数完成配置了。

搜索项目

项目搜索的逻辑在核心篇中有详细讲解,此处不多赘述,只讲解用法。

其实前面 open 功能中定义的命令,触发的搜索都一致,只是在搜索结果选择以后执行的操作不同,所以不同的命令进来只要执行相同的搜索函数即可。

uTools 为开发者提供了几个插件模板,猎豹主要使用了 list 这个模板完成功能开发。

import cp from 'child_process';
import './mount'; // 在 window 挂载上挂载工具函数 
import {
  filterWithCache,
  filterWithSearchResult,
  updateHits,
  Project,
  ResultItem,
  COMMAND,
  getOpenCommand,
  setProjectApp,
} from 'cheetah-core'; // 从核心模块中引入
import {
  initCore,
  output,
  chooseFile,
  commandMap,
  getAllDefaultApp,
  errorHandle,
} from './common'; // uTools 功能适配的函数

const refreshKeyword = '[refresh]'; // 刷新关键字,搜索字符串中包含此关键字时会忽略缓存,重新搜索项目

/**
 * @description: 搜索项目
 * @param {any} action
 * @param {string} keyword 搜索关键字
 * @param {any} callbackSetList 渲染候选列表的回调
 * @return {*}
 */
async function search(
  action: any,
  keyword: string,
  callbackSetList: any
): Promise<void> {
  try {
    initCore();
    const needRefresh: boolean = keyword.includes(refreshKeyword);
    const searchKeyword = keyword.replace(refreshKeyword, '');
    if (!searchKeyword) return callbackSetList();
    let projects: Project[] = await filterWithCache(searchKeyword);
    let fromCache = true;
    // 如果缓存结果为空或者需要刷新缓存,则重新搜索
    if (!projects.length || needRefresh) {
      projects = await filterWithSearchResult(searchKeyword);
      fromCache = false;
    }

    const result: ResultItem[] = output(projects);

    if (fromCache) {
      result.push({
        title: '忽略缓存重新搜索',
        description: '以上结果从缓存中获得,选择本条将重新搜索项目并更新缓存',
        icon: 'assets/refresh.png',
        arg: searchKeyword,
      });
    }

    if (!result.length) {
      result.push({
        title: `没有找到名称包含 ${searchKeyword} 的项目`,
        description: '请尝试更换关键词,回车返回重新搜索',
        icon: 'assets/empty.png',
        type: 'empty',
      });
    }

    callbackSetList(result);
  } catch (error: any) {
    errorHandle(error);
  }
}

/**
 * @description: 处理点击结果,如果是重新搜索会返回跳过
 * @param {ResultItem} itemData 被点击的条目
 * @return {boolean} 是否需要跳过
 */
function commonSelect(itemData: ResultItem): boolean {
  const { arg, type } = itemData;
  // 搜索结果为空的条目被点击则置空输入框
  if (type === 'empty') {
    utools.setSubInputValue('');
    return true;
  }
  // 重新搜索时重置搜索框内容为 [refresh]+关键字
  const skip = arg !== null && arg !== undefined;
  if (skip) {
    utools.setSubInputValue(`${refreshKeyword}${arg}`);
  }
  return skip;
}

window.exports = {
  open: {
    mode: 'list',
    args: {
      search,
      // 在搜索列表内选择项目并回车以后,将执行此函数,action 为当前执行
      select: async (action: any, itemData: ResultItem) => {
        try {
          if (commonSelect(itemData)) return;
          initCore();
          const { payload }: { payload: string } = action;
          const commandType = commandMap[payload];

          if (commandType === COMMAND.SET_APPLICATION) {
            const appPath: string = chooseFile();
            setProjectApp(itemData.path!, appPath);
            utools.hideMainWindow();
            utools.outPlugin();
            return;
          }

          const defaultAppPath = getAllDefaultApp()?.[commandType] ?? '';
          
          const command = await getOpenCommand(
            itemData,
            commandType,
            defaultAppPath
          );

          cp.exec(command, { windowsHide: true }, (error: any) => {
            if (error) {
              utools.showNotification(error?.message ?? '未知错误');
            }
            updateHits(itemData.path!);
            utools.hideMainWindow();
            utools.outPlugin();
          });
        } catch (error: any) {
          errorHandle(error);
        }
      },
    },
  },
  setting: {...}
}

open 下的 mode 字段配置为 list 表示使用 list 模板, args 下为 list 模板在使用时触发的函数,定义如下:

search

uTools 插件子输入框内容变化时调用,第一个参数是描述当前打开 open 功能的命令,第二个参数是输入框的值,第三个参数为 Callback 函数,将列表按照文档要求传入即可渲染。

猎豹Cheetah插件开发 - uTools 篇

select

search 返回的列表中选择项目后回车时执行,第一个参数与 search 一致,第二个参数为被选择项目的详细信息。

明白了 list 模板的使用后,就能开始功能的开发了。

首先在 search 中使用核心模块提供的项目搜索函数,filterWithSearchResult,执行搜索后结果会被缓存,下一次使用时将直接调用 filterWithCache 从缓存中筛选结果,为了防止项目变动后搜索结果不准确,添加了一个手动刷新的候选项,最后将搜索结果通过 Callback 返回给 uTools 渲染列表。

猎豹Cheetah插件开发 - uTools 篇

在搜索结果中选择目标项目回车后执行 select 函数,根据 action.payload 的值以及 itemData 执行不同的操作:

  • itemData.typeempty 时,搜索结果为空,选择空提示项目时仅将子输入框内容置空,可以重新输入关键字搜索。
  • itemData.arg 不为空时,将子输入框内容改为 [refresh]${itemData.arg},触发新一轮搜索,因为关键词中包含刷新关键字,将直接调用 filterWithSearchResult 忽略缓存直接搜索项目。
  • 排除以上两种情况后,将根据 action.payload 的值使用关联的应用打开项目。

使用指定应用打开项目

Mac OS 下使用指定应用打开文件或者文件夹非常方便,使用以下命令即可:

open -a 应用名称 项目绝对路径
# 例
open -a "Visual Studio Code" /Users/***/Documents/works/test

Windows 下可用的软件比较少,编辑器可用的有 VSCodeWebStormSublime,其余未做更多测试,Git GUI 仅支持 Fork,终端仅 CMDPowerShell 可用,软件的使用命令为:

"应用路径" "项目路径"
# 例
"D:\Program Files JetBrains\WebStorm 2021.1.1\webstorm.exe" "D:\works\test"

Windows 下终端运行需要经过特殊处理:

/**
 * @description: windows 下终端命令处理
 * @param {string} realAppPath 应用路径
 * @param {string} projectPath 项目路径
 * @return {*}
 */
function windowsTerminal(realAppPath: string, projectPath: string): string {
  if (/powershell/i.test(realAppPath)) {
    return `start powershell -NoExit -Command "cd ${projectPath}"`;
  }

  if (/cmd/i.test(realAppPath)) {
    return `start /D ${projectPath}`;
  }

  throw new Error('109');
}

编辑器打开项目的优先级规则为:缓存文件内配置的项目编辑器 > 缓存文件内配置的项目类型编辑器 > 设置面板内配置的默认编辑器。 在没有配置任何编辑器时,兜底的是文件浏览器,Mac OS 下为 Finder,Windows 下为 explorer.exe

使用 Git GUI 与终端时也会从本地存储中读取设置面板中配置的相关应用。

清除缓存

设置面板最下方有一个清除缓存按钮,可以将缓存文件删除,核心模块中也提供了相关函数,在 uTools 项目中直接调用即可,调用成功后使用 uTools 提供的通知方式提示清除成功。

export async function onClearCache(): Promise<void> {
  try {
    initCore();
    await clearCache();
    notice('缓存清除成功');
  } catch (error: any) {
    errorHandle(error);
  }
}

为项目设置软件

猎豹可以为每个项目配置编辑器,在缓存的项目描述中有个字段是 idePath,将编辑器名称或者路径配置在此即可。 但是每次配置都要打开缓存文件找到项目,复制编辑器名称粘贴非常麻烦,所以直接配置了设置项目应用的命令,此命令也属于 open 功能下,在项目搜索列表选择项目后,直接调用 uTools 提供的文件选择 API utools.showOpenDialog,获取到应用信息后直接设置,方便快捷,老人小孩都会用。

export function chooseFile() {
  return utools.showOpenDialog({
    filters: [{ extensions: ['app', 'exe', 'lnk'] }],
    properties: ['openFile'],
  });
}

...
const { payload }: { payload: string } = action;
const commandType = commandMap[payload];

if (commandType === COMMAND.SET_APPLICATION) {
  const appPath: string = chooseFile();
  setProjectApp(itemData.path!, appPath); // 此函数由核心模块提供
  utools.hideMainWindow();
  utools.outPlugin();
  return;
}
...

错误处理

核心模块中函数会在某些情况下抛出错误,错误信息是以 code 及映射 Map 方式提供,方便后续做国际化,在获取到提示文本后,通过 uTools 提供的通知 API utools.showNotification 提示用户。

import { ErrorCodeMessage } from 'cheetah-core';

export function notice(message: string) {
  utools.showNotification(message)
}

export function errorHandle(error: any) {
  const errorCode: number = error.message;
  notice(ErrorCodeMessage[errorCode]);
  utools.hideMainWindow();
}

提交审核

完成功能开发后,后面的工作就是发布到插件市场啦~

发布流程如下:

  1. 构建项目
  2. 打开 uTools 开发者工具
  3. 选择需要发布的项目
  4. 选中“发布”选项卡
  5. 点击右下角的“发布版本”按钮
  6. 填写版本号,确认打包 plugin.json 所在目录
  7. 填写版本说明、插件介绍,上传插件截图
  8. 提交审核

⚠️ preload.js 需要保持明文可读,否则审核直接不通过。 提交以后就可以去 uTools 开发者交流群催管理员审核了,哈哈哈哈哈。

小结

至此 Cheetah for uTools 的开发就完成了,当前版本已经更新到 1.2.0,欢迎各位看官前往下载体验,Cheetah 项目现已开源,如果对插件安全方面有疑虑的可以逐行检查。 Cheetah 插件只使用了 uTools 很小一部分能力,还有很多强大的功能没有用到,有兴趣的朋友可以移步开发者文档。 下一篇将介绍猎豹 for Alfred 适配核心模块的过程,敬请期待~