猎豹Cheetah插件开发 - uTools 篇
序
本文主要讲解如何接入我们抽取的核心模块如何应用在 uTools 平台的插件开发中,并且介绍一下 uTools 插件开发用到的 API,以及 uTools 插件的发布流程。
项目配置
uTools 为开发者提供了一个工具插件,可以在插件市场直接搜索“uTools 开发者工具”安装,项目的新建、运行,发布都在这个插件内操作。
创建项目
安装完开发者工具后,直接在 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 提供的存储环境中。
命令面板可以使用任意框架开发,最后构建成 html
、css
、js
即可通过 uTools 提供的 API utools.createBrowserWindow
打开窗口。
插一句:uTools 基于 Electron 开发,熟悉 Electron 的小伙伴应该很容易上手。
下面看一看设置面板打开以后得样子:
文件结构
主要的功能文件使用 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
函数,将列表按照文档要求传入即可渲染。
select
在 search
返回的列表中选择项目后回车时执行,第一个参数与 search
一致,第二个参数为被选择项目的详细信息。
明白了 list 模板的使用后,就能开始功能的开发了。
首先在 search 中使用核心模块提供的项目搜索函数,filterWithSearchResult
,执行搜索后结果会被缓存,下一次使用时将直接调用 filterWithCache
从缓存中筛选结果,为了防止项目变动后搜索结果不准确,添加了一个手动刷新的候选项,最后将搜索结果通过 Callback
返回给 uTools 渲染列表。
在搜索结果中选择目标项目回车后执行 select
函数,根据 action.payload
的值以及 itemData
执行不同的操作:
- 在
itemData.type
为empty
时,搜索结果为空,选择空提示项目时仅将子输入框内容置空,可以重新输入关键字搜索。 - 当
itemData.arg
不为空时,将子输入框内容改为[refresh]${itemData.arg}
,触发新一轮搜索,因为关键词中包含刷新关键字,将直接调用filterWithSearchResult
忽略缓存直接搜索项目。 - 排除以上两种情况后,将根据
action.payload
的值使用关联的应用打开项目。
使用指定应用打开项目
Mac OS 下使用指定应用打开文件或者文件夹非常方便,使用以下命令即可:
open -a 应用名称 项目绝对路径
# 例
open -a "Visual Studio Code" /Users/***/Documents/works/test
Windows 下可用的软件比较少,编辑器可用的有 VSCode
,WebStorm
,Sublime
,其余未做更多测试,Git GUI
仅支持 Fork
,终端仅 CMD
、PowerShell
可用,软件的使用命令为:
"应用路径" "项目路径"
# 例
"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();
}
提交审核
完成功能开发后,后面的工作就是发布到插件市场啦~
发布流程如下:
- 构建项目
- 打开 uTools 开发者工具
- 选择需要发布的项目
- 选中“发布”选项卡
- 点击右下角的“发布版本”按钮
- 填写版本号,确认打包
plugin.json
所在目录 - 填写版本说明、插件介绍,上传插件截图
- 提交审核
⚠️ preload.js
需要保持明文可读,否则审核直接不通过。
提交以后就可以去 uTools 开发者交流群催管理员审核了,哈哈哈哈哈。
小结
至此 Cheetah for uTools 的开发就完成了,当前版本已经更新到 1.2.0
,欢迎各位看官前往下载体验,Cheetah 项目现已开源,如果对插件安全方面有疑虑的可以逐行检查。
Cheetah 插件只使用了 uTools 很小一部分能力,还有很多强大的功能没有用到,有兴趣的朋友可以移步开发者文档。
下一篇将介绍猎豹 for Alfred 适配核心模块的过程,敬请期待~