本文为稀土技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!
序
我们好,本文将继续讲解 Cheetah for Raycast 扩展的开发流程以及 Raycast 开发扩展的一些注意事项。
Raycast 介绍
Raycast 是一个速度极快、彻底可扩展的启动器,它能够快速查找本地运用、文件等等,经过装置第三方扩展完结各种各样的功用。
Raycast 还能够自定义快速连接、脚本指令等,完结一系列自动化操作。
Raycast 的扩展开发文档十分具体,让开发者清晰地了解开发扩展相关的常识、流程,快速上手。
官网:www.raycast.com
创立项目
让我们来创立一个 Raycast 扩展吧~
Create Extension
Raycast 供给了便利快捷的指令帮助开发者创立扩展,在 Raycast 输入框内输入 Create Extension
回车即可翻开创立面板。
Template:挑选一个扩展模板,当时挑选的是一个在 npm 官网查找依赖库的比如。
Categories:扩展的分类。
Location:是扩展项目存放的目录。
Command Name:是扩展内指令的称号,在 Raycast 输入框中查找即可运转此指令。
填写完信息后点击 Create Extension
即可完结项目创立,生成项目结构如下:
Cheetah
├─ .eslintrc.json
├─ .gitignore
├─ .prettierrc
├─ CHANGELOG.md
├─ README.md
├─ assets
│ ├─ command-icon.png
│ └─ list-icon.png
├─ package.json
├─ src
│ └─ index.tsx
└─ tsconfig.json
终端进入项目后履行:
npm install
npm run dev
此刻如果现已翻开 Raycast,会自动唤出输入面板并且本地扩展中的指令会置顶展现。
指令装备
Raycast 的扩展装备集成在 package.json
内,包括基本信息、指令进口、偏好设置等等。
{
"$schema": "https://www.raycast.com/schemas/extension.json",
"version": "1.0.0",
"name": "cheetah",
"title": "Cheetah",
"description": "Search for a local Git project and open it with the specified application.",
"icon": "logo.png",
"author": "ronglecat",
"categories": [
"Developer Tools"
],
"license": "MIT",
"commands": [ // 本扩展包括的一切指令
{
"name": "editor", // 指令的唯一标识,运转指令时匹配 src 目录下的同名文件并履行。
"title": "Open With Editor", // 用户看到的指令称号
"description": "Open the specified project using the editor.", // 指令的描述
"mode": "view", // 为 “views” 时指令运转后展现可视化界面,进口文件为 tsx
"icon": "command/editor.png", // 指令专属icon
"keywords": [ // 除了输入title外,这些关键词也能够匹配到当时指令
"editor",
"Editor"
]
},
{
"name": "git",
"title": "Open With Git GUI",
"description": "Open the specified project using the Git GUI.",
"mode": "view",
"icon": "command/git.png",
"keywords": [
"git",
"Git"
]
},
{
"name": "terminal",
"title": "Open With Terminal",
"description": "Open the specified project using the terminal.",
"mode": "view",
"icon": "command/terminal.png",
"keywords": [
"terminal",
"Terminal"
]
},
{
"name": "finder",
"title": "Open With Finder",
"description": "Open the specified project using the Finder.",
"mode": "view",
"icon": "command/finder.png",
"keywords": [
"finder",
"Finder"
]
},
{
"name": "chooseApp",
"title": "Choose Application",
"description": "Specifying applications for projects.",
"mode": "view",
"icon": "command/app.png"
},
{
"name": "editCache",
"title": "Edit Cache File",
"description": "Open the cache file using the default editor.",
"mode": "no-view", // 为 “no-view” 时,表明仅履行,无可视化界面反馈,进口文件 ts 即可
"icon": "command/editCache.png"
},
{
"name": "clearCache",
"title": "Clear Cache",
"description": "Clear cache files.",
"mode": "no-view",
"icon": "command/clearCache.png"
}
],
"preferences": [ // 偏好设置
{
"name": "workspaces", // 终究存储的 key 值,运用 getPreferenceValues API 读取
"description": "The tool will search for projects in the working directory you set, with multiple directories separated by commas, Need to configure the absolute path of the directory, This directory should contain projects managed with Git.",
// 此装备的具体说明
"required": true, // 是否为强制要求
"type": "textfield", // 输入的类型,此处为文本框
"title": "Setting working directory" // 用户看到的标题
},
{
"name": "defaultEditor",
"description": "The default editor used to open the project.",
"required": false,
"type": "appPicker", // Raycast 供给的运用挑选器
"title": "Choose default Editor"
},
{
"name": "defaultGitApp",
"description": "The Git GUI application used to open the project.",
"required": false,
"type": "appPicker",
"title": "Choose Git GUI"
},
{
"name": "defaultTerminalApp",
"description": "The terminal application used to open the project.",
"required": false,
"type": "appPicker",
"title": "Choose Terminal App"
}
],
"dependencies": {
...
},
"devDependencies": {
...
},
"scripts": {
...
}
}
commands
数组内是当时扩展所包括的一切指令,其内字段意义能够看数组第一个元素中的注释,装备完结后 Raycast 中即可看到对应的指令了,别忘了在 src
下创立同名进口文件~
preferences
数组内是扩展可装备的偏好设置,用户在装备后能够经过 getPreferenceValues API 获取。
调用扩展时设置项的 required
为 true
且未装备的情况下会展现一个装备面板,提示用户完结装备。
引进中心模块
与前面两篇一样,Cheetah for Raycast 也需求引进中心模块。
装置模块
npm install cheetah-core
# or
yarn add cheetah-core
初始化模块
在运用到项目查找、缓存相关函数时需求提早初始化中心模块,这儿运用了 React 的自定义 Effect
完结初始化:
// useInitCore.ts
// 初始化猎豹中心模块
import { init, HOME_PATH } from "cheetah-core";
import { cachePath } from "../constant";
import { errorHandle, getConfig } from "../utils";
export default async () => {
const { workspaces } = getConfig();
if (!workspaces) {
errorHandle(new Error("103"));
return;
}
init({
cachePath,
workspaces: workspaces.replace(/~/gm, HOME_PATH),
});
};
React 初学者也不知道自定义 Effect
这么用规不标准,请各位老师指教。
功用完结
查找项目并…
下面 5 个指令都用到了查找项目,只是在挑选项目后履行的操作不同,所以将查找相关的逻辑封装了一下,各个进口调用下面的函数,回来一个 List
列表组件用于烘托:
// src/lib/components/searchList.tsx
import { List } from "@raycast/api";
import { COMMAND } from "cheetah-core";
import { useEffect, useState } from "react";
import useInitCore from "../effects/useInitCore";
import useProjectFilter from "../effects/useProjectFilter";
import SearchListItem from "./searchListItem";
export default (command: COMMAND, appPath: string, forced = false) => {
useInitCore();
const [searchText, setSearchText] = useState("");
const [data, loading, filterProject] = useProjectFilter();
useEffect(() => {
filterProject(searchText);
}, [searchText]);
return (
<List
isLoading={loading}
onSearchTextChange={setSearchText}
searchBarPlaceholder="Search your project..."
throttle
enableFiltering={false}
>
<List.Section title="Results" subtitle={data?.length + ""}>
{data?.map((searchResult, index) => (
<SearchListItem
key={searchResult.name + index}
searchResult={searchResult}
appPath={appPath}
forced={forced}
filterProject={filterProject}
commandType={command}
/>
))}
</List.Section>
</List>
);
};
列表函数中调用了 useProjectFilter
自定义函数,用来查找项目。
// src/lib/effects/useProjectFilter.ts
// 查找与关键词匹配的项目
import { useState } from "react";
import { Project, filterWithSearchResult, filterWithCache } from "cheetah-core";
import { refreshKeyword } from "../constant";
import { ResultItem } from "../types";
import { output } from "../core";
import { environment } from "@raycast/api";
import { errorHandle } from "../utils";
export default (): [
ResultItem[],
boolean,
(keyword: string) => Promise<void>
] => {
const [resultList, setResultList] = useState<ResultItem[]>([]);
const [loading, setLoading] = useState(true);
/**
* @description: 查找项目
* @param {string} keyword 用户输入的关键词
* @return {*}
*/
async function filterProject(keyword: string): Promise<void> {
try {
const needRefresh: boolean = keyword.includes(refreshKeyword);
const searchKeyword = keyword.replace(refreshKeyword, "");
setLoading(true);
let projects: Project[] = await filterWithCache(searchKeyword);
let fromCache = true;
// 如果缓存结果为空或者需求改写缓存,则重新查找
if (!projects.length || needRefresh) {
projects = await filterWithSearchResult(searchKeyword);
fromCache = false;
}
const result: ResultItem[] = await output(projects);
if (fromCache) {
result.push({
name: "Ignore cache re search",
description:
"Ignore the cache and search for items in the working directory again",
icon: `${environment.assetsPath}/refresh.png`,
arg: searchKeyword,
refresh: true,
});
}
setResultList(result);
setLoading(false);
} catch (error: unknown) {
errorHandle(error);
}
}
return [resultList, loading, filterProject];
};
输入框内关键词发生变化后会履行 useProjectFilter
回来的回调函数,触发查找并回来筛选后的项目合集,searchList
依据项目调集循环烘托 searchListItem
。
红框是整个 searchList
,蓝框是 searchListItem
,组件代码如下:
// src/lib/components/searchListItem.tsx
import { List } from "@raycast/api";
import { COMMAND, HOME_PATH } from "cheetah-core";
import { ResultItem } from "../types";
import Actions from "./actions";
export default ({
searchResult,
appPath,
forced,
commandType,
filterProject,
}: {
searchResult: ResultItem;
appPath: string;
forced: boolean;
commandType: COMMAND;
filterProject: (keyword: string) => Promise<void>;
}) => {
const finalAppPath =
(forced ? appPath : searchResult.idePath || appPath) || "Finder";
return (
<List.Item
title={searchResult.name}
subtitle={
searchResult.path?.replace(HOME_PATH, "~") || searchResult.description
}
accessoryTitle={searchResult.hits}
icon={searchResult.icon}
actions={
<Actions
searchResult={searchResult}
finalAppPath={finalAppPath}
filterProject={filterProject}
commandType={commandType}
/>
}
/>
);
};
注意看,searchListItem
中还引用了一个 Actions
组件,用于烘托行为面板。
运用 Command + K
组合键能够翻开 List.Item
组件关联的行为面板,行为面板中的第一项会出现在上图蓝框位置,直接按回车即可履行。
// src/lib/components/actions.tsx
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { Action, ActionPanel, useNavigation, Icon } from "@raycast/api";
import { COMMAND, updateHits } from "cheetah-core";
import { refreshKeyword } from "../constant";
import { ResultItem } from "../types";
import ApplicationList from "./applicationList";
export default ({
searchResult,
finalAppPath,
filterProject,
commandType,
}: {
searchResult: ResultItem;
finalAppPath: string;
filterProject: (keyword: string) => Promise<void>;
commandType: COMMAND;
}) => {
if (searchResult.refresh) {
return (
<ActionPanel>
<Action
title="Refresh Cache"
icon={Icon.Repeat}
onAction={() => {
filterProject(`${refreshKeyword}${searchResult.arg}`);
}}
/>
</ActionPanel>
);
}
if (commandType === COMMAND.SET_APPLICATION) {
const { push } = useNavigation();
return (
<ActionPanel>
<Action
title="Choose Application"
icon={Icon.Box}
onAction={() =>
push(<ApplicationList projectPath={searchResult.path!} />)
}
/>
</ActionPanel>
);
}
return (
<ActionPanel>
<ActionPanel.Section>
<Action.Open
title={`Open in ${finalAppPath}`}
target={searchResult.path!}
application={finalAppPath}
onOpen={async () => {
await updateHits(searchResult.path!);
}}
/>
</ActionPanel.Section>
<ActionPanel.Section>
<Action.CopyToClipboard
title="Copy Project Path"
content={searchResult.path!}
shortcut={{ modifiers: ["cmd"], key: "." }}
/>
</ActionPanel.Section>
</ActionPanel>
);
};
Actions
内依据传入的 commandType
履行不同操作,面板中除了为项目设置这个指令外,都添加了复制项目路径的条目,便利用户运用。
Open With Editor
运转此指令将调用查找列表,在回来的项目列表中挑选项目回车,将运用编辑器翻开项目。
终究运用的编辑器优先级为:缓存内项目信息中的 idePath
> 缓存内项目类型编辑器 > 偏好设置内挑选的 defaultEditor
。
进口文件代码如下:
// src/editor.tsx
import { Application } from "@raycast/api";
import { COMMAND } from "cheetah-core";
import searchList from "./lib/components/searchList";
import { errorHandle, getConfig } from "./lib/utils";
const command = COMMAND.OPEN; // 指令类型:编辑器翻开
const forced = false; // 是否强制运用偏好设置内装备的运用
export default () => {
const { defaultEditor }: { defaultEditor: Application } = getConfig();
const appPath = defaultEditor?.name ?? "";
if (!appPath) {
errorHandle(new Error("112"));
return;
}
return searchList(command, appPath, forced);
};
传入 searchList 的参数意义为:
command:当时指令类型。
appPath:偏好设置内挑选的编辑器运用称号。
forced:是否强制运用偏好设置内装备的运用。
下图红框处会展现终究将运用什么运用翻开项目,当默许编辑器未装备时将依照错误代码 112
处理,错误处理后续具体说明。
Action.Open
是 Raycast 供给的特别组件,能够直接运用指定运用翻开方针文件、文件夹。
// src/lib/components/actions.tsx 节选
<Action.Open
title={`Open in ${finalAppPath}`}
target={searchResult.path!}
application={finalAppPath}
onOpen={async () => {
await updateHits(searchResult.path!);
}}
/>
onOpen
为 Action
回车履行后的回调,在此处调用 updateHits
即可完结对项目点击量的更新。
Open With Git GUI
此指令基本上与运用编辑器翻开的功用一致,履行后将运用偏好设置内装备的 Git GUI
运用翻开项目。
// src/git.tsx
import { Application } from "@raycast/api";
import { COMMAND } from "cheetah-core";
import searchList from "./lib/components/searchList";
import { errorHandle, getConfig } from "./lib/utils";
const command = COMMAND.GIT_GUI_OPEN; // 指令类型:Git GUI翻开
const forced = true; // 是否强制运用偏好设置内装备的运用
export default () => {
const { defaultGitApp }: { defaultGitApp: Application } = getConfig();
const appPath = defaultGitApp?.name ?? "";
if (!appPath) {
errorHandle(new Error("113"));
return;
}
return searchList(command, appPath, forced);
};
仅 appPath
与 forced
的值有所不同,错误代码为 113
。
Open With Terminal
此指令也类似,履行后将运用偏好设置内装备的终端运用翻开项目。
// src/terminal.tsx
import { Application } from "@raycast/api";
import { COMMAND } from "cheetah-core";
import searchList from "./lib/components/searchList";
import { errorHandle, getConfig } from "./lib/utils";
const command = COMMAND.TERMINAL_OPEN; // 指令类型:终端翻开
const forced = true; // 是否强制运用偏好设置内装备的运用
export default () => {
const { defaultTerminalApp }: { defaultTerminalApp: Application } =
getConfig();
const appPath = defaultTerminalApp?.name ?? "";
if (!appPath) {
errorHandle(new Error("114"));
return;
}
return searchList(command, appPath, forced);
};
仅 appPath
与 forced
的值有所不同,错误代码为 114
。
Open With Finder
运转此指令将运用 Finder
翻开项目。
// src/finder.tsx
import { COMMAND } from "cheetah-core";
import searchList from "./lib/components/searchList";
const command = COMMAND.FOLDER_OPEN; // 指令类型:Finder 翻开
const forced = true; // 是否强制运用偏好设置内装备的运用
export default () => {
return searchList(command, "Finder", forced);
};
Choose Application
运转此指令挑选项目今后回车将烘托当时体系中装置的运用列表,挑选运用后将挑选的运用写入缓存中对应项目的 idePath
中。
经过 Raycast 供给的 API getApplications 能够获得当时体系中装置的运用合集,添加自定义 Effect
如下:
// src/lib/effects/useGetApplicationList.ts
// 获取体系已装置的运用列表
import { Application, getApplications } from "@raycast/api";
import { useState } from "react";
export default (): [Application[], boolean, () => Promise<void>] => {
const [resultList, setResultList] = useState<Application[]>([]);
const [loading, setLoading] = useState(true);
async function getApplicationList() {
setLoading(true);
const applications: Application[] = await getApplications();
setResultList(applications);
setLoading(false);
}
return [resultList, loading, getApplicationList];
};
获取到运用列表后再经过 List
烘托,添加 applicationList
组件:
// src/lib/components/applicationList.tsx
import {
ActionPanel,
Action,
List,
Application,
open,
showHUD,
} from "@raycast/api";
import { setProjectApp } from "cheetah-core";
import { useEffect } from "react";
import useGetApplicationList from "../effects/useGetApplicationList";
export default ({ projectPath }: { projectPath: string }) => {
const [applicationList, isLoading, getApplicationList] =
useGetApplicationList();
useEffect(() => {
getApplicationList();
}, []);
return (
<List
isLoading={isLoading}
searchBarPlaceholder="Search application name..."
throttle
>
<List.Section title="Results" subtitle={applicationList?.length + ""}>
{applicationList?.map((searchResult: Application) => (
<SearchListItem
key={searchResult.name}
searchResult={searchResult}
projectPath={projectPath}
/>
))}
</List.Section>
</List>
);
};
function SearchListItem({
searchResult,
projectPath,
}: {
searchResult: Application;
projectPath: string;
}) {
return (
<List.Item
title={searchResult.name}
subtitle={searchResult.path}
accessoryTitle={searchResult.bundleId}
actions={
<ActionPanel>
<ActionPanel.Section>
<Action
title="Choose and Complete"
onAction={async () => {
await setProjectApp(projectPath, searchResult.name);
await open(projectPath, searchResult.name);
await showHUD("The application is set up and tries to open");
}}
/>
</ActionPanel.Section>
</ActionPanel>
}
/>
);
}
在 searchListItem
的 Actions
组件中判断如果 command
类型为挑选运用,则经过 Raycast 中页面栈操作 API useNavigation 将 applicationList
烘托出来。
// src/lib/components/actions.tsx 节选
...
const { push } = useNavigation();
...
<ActionPanel>
<Action
title="Choose Application"
icon={Icon.Box}
onAction={() =>
push(<ApplicationList projectPath={searchResult.path!} />)
}
/>
</ActionPanel>
...
下图为运用列表,挑选运用后回车即可为项目设置运用。
Edit Cache File
此指令为无视图指令,履行后将运用偏好设置中装备的默许编辑器翻开装备文件。
// src/editCache.ts
import { open } from "@raycast/api";
import { errorHandle, getConfig } from "./lib/utils";
import { cachePath } from "./lib/constant";
export default async () => {
const { defaultEditor } = getConfig();
const appPath = defaultEditor?.name ?? "";
if (!appPath) {
errorHandle(new Error("112"));
return;
}
await open(cachePath, appPath);
};
除了 Action.Open 外,Raycast 还为开发者供给了运用指定运用翻开文件、文件夹的 API open,只需求传入方针文件、文件夹称号,运用称号或绝对路径即可。
Clear Cache
此指令也是无视图指令,履行后将删除缓存文件,由中心模块供给支持。
// src/clearCache.ts
import { confirmAlert, showHUD } from "@raycast/api";
import { clearCache } from "cheetah-core";
import { errorHandle } from "./lib/utils";
import useInitCore from "./lib/effects/useInitCore";
export default async () => {
try {
if (
await confirmAlert({
title: "Sure to clear the cache?",
message:
"After clearing the cache, the type application configuration, the project application configuration, and the number of hits will all disappear.",
})
) {
useInitCore();
await clearCache();
await showHUD("Cache cleared");
}
} catch (error) {
errorHandle(error);
}
};
删除缓存是个十分有破坏性的操作,所以加了二次确认,运用了 Raycast 供给的 confirmAlert
API。
错误处理
上面的指令中许多当地都运用了 errorHandle
函数,它的作用是依据代码运转中抛出的错误代码处理错误信息。
/**
* @description: 错误处理并输出
* @param {any} error
* @return {*}
*/
export async function errorHandle(error: any): Promise<void> {
const errorCode: string = error.message;
const needHandleCodes = ["112", "113", "114", "103"];
await showHUD(ErrorCodeMessage[errorCode]);
popToRoot({ clearSearchBar: true });
if (needHandleCodes.includes(errorCode)) {
openExtensionPreferences();
}
}
在错误代码为 103
、112
、113
、114
时表明偏好装备不完整,将跳转到扩展的偏好设置界面,便利用户进行装备。
虽然在中心模块中有定义过错误代码对应的报错信息,可是 Raycast 要求扩展最好运用单一语言,美式英语最佳,所以机翻了一份错误信息:
// 错误代码对应的文字提示
export const ErrorCodeMessage: { [code: string]: string } = {
"100": "File read failure",
"101": "File write failure",
"102": "File deletion failed",
"103": "Working directory not configured",
"104": "Cache file path not configured",
"105": "System platform not configured",
"106": "Environment variable read failure",
"107": "Cache file write failure",
"108": "Failed to read folder",
"109": "Unknown terminal program, downgraded to folder open",
"110": "No such item in the cache",
"111": "Application path is empty",
"112": "Please configure the default editor first",
"113": "Please configure the Git GUI application first",
"114": "Please configure the terminal application first",
};
发布审核
完结功用开发后,需求将扩展发布到运用商场。
自检
发布前,先运用 Raycast 扩展项目中装备好的脚本指令进行检查。
// package.json
{
...
"scripts": {
"build": "ray build -e dist",
"dev": "ray develop",
"fix-lint": "ray lint --fix",
"lint": "ray lint",
"publish": "ray publish"
}
...
}
// 终端运转
yarn fix-lint
履行结果悉数为 ready
时,发布前准备工作就算完结了
Pull Request
与 uTools 的发布模式不同,Raycast 运用 git 库房办理扩展商场,开发者先 fork 库房,将开发好的扩展添加到库房内再建议 Pull Request,建议后 git 将运转 Checks,两项检查经过将符号此 Pull Request 为 new extension
。
具体的发布流程能够查阅官方文档。
之后审核员将进行 Code Review,指出扩展的问题,修复完后审核员会将代码兼并,兼并后即可在 Raycast Store 中查找扩展了。
tips:审核时间很长,需求耐性等待,Cheetah 扩展提审到上架大概花费了 1 周时间。
小结
U1S1,Raycast 的开发体会比 uTools 好了不少,可是审核的流程略拖沓,没有 uTools 直接艾特审核员这么便利(手动狗头)。
本篇为本系列正文的结束篇,希望看官们能够下载 Cheetah 试试效果,万一能进步一点点功率呢~
后续可能还有番外篇,想到了一个玩具项目,Cheetah for VSCode,是不是有点套娃的意思了,哈哈哈哈哈。
很感谢我们能够看到这儿,有缘再会~