黑苹果macOS Raycast扩展开发完全实战指南:从React组件到API集成与发布商店的现代化启动器开发
发布时间:2026年6月 | 分类:黑苹果 | 关键词:Raycast、扩展开发、React
前言:Raycast——现代macOS的效率核心
Raycast作为macOS平台最流行的启动器工具,已经超越了传统Launcher的范畴,演进为一个完整的生产力平台。对于黑苹果用户,Raycast不仅是Spotlight的替代品,更是整合各种开发工具和服务的统一入口。Raycast Store中已有数千个扩展覆盖开发、写作、效率、社交等各个领域,但更令人兴奋的是其开放的扩展开发框架——任何人都可以使用React和TypeScript开发自定义扩展。
本指南将系统讲解Raycast扩展开发的全流程,从开发环境搭建、React组件编写、API调用、偏好设置、本地存储到最终发布到Raycast Store。无论你是希望简化日常重复操作,还是构建面向特定工作流的复杂工具,本指南都能为你提供完整的知识体系。
第一部分:Raycast开发环境搭建
系统要求与前置条件
开始Raycast扩展开发前,需要准备:
- macOS 11.0+:Raycast支持Big Sur及以后版本
- Node.js 18+:Raycast CLI依赖较新版本的Node.js
- Raycast 1.50+:最新Raycast版本,需要支持最新API
- Xcode命令行工具:某些原生模块可能需要编译
安装Raycast CLI
在黑苹果上安装Raycast命令行工具:
# 使用Node包安装(推荐)
npm install -g @raycast/api
# 或者使用Homebrew
brew install raycast
# 验证安装
ray --version
# 登录Raycast账户
ray login
登录过程会打开浏览器跳转到Raycast账户认证页面,需要你有有效的Raycast账户(免费账户即可发布扩展)。
创建第一个扩展项目
使用官方脚手架创建项目:
cd ~/Developer
npx create-raycast-extension my-first-extension
# 脚手架会询问以下问题:
# - Template: 选择 "Show Detail" 或 "List with Detail"
# - Name: my-first-extension
# - Description: My first Raycast extension
# - Author: Your Name
# - Category: Productivity
# - License: MIT
# - Git: 是否初始化Git仓库
项目结构创建完成后,进入项目目录:
cd my-first-extension
ls -la
# src/
# my-first-command.tsx # 主命令文件
# package.json # 项目配置
# tsconfig.json # TypeScript配置
# README.md # 扩展说明
# CHANGELOG.md # 变更日志
第二部分:Raycast扩展基础架构
package.json元数据
package.json是Raycast扩展的核心配置,定义了扩展的所有元数据:
{
"name": "my-first-extension",
"title": "My First Extension",
"description": "A simple example extension",
"icon": "icon.png",
"author": "your-username",
"license": "MIT",
"commands": [
{
"name": "my-command",
"title": "My Command",
"description": "Performs my custom action",
"mode": "view"
}
],
"dependencies": {
"@raycast/api": "^1.50.0",
"@raycast/utils": "^1.10.0"
},
"devDependencies": {
"@types/node": "^20.0.0",
"@types/react": "^18.2.0",
"typescript": "^5.0.0"
}
}
核心API组件
Raycast扩展基于React开发,使用@raycast/api提供的核心组件:
import { List, ActionPanel, Action, Detail } from "@raycast/api";
export default function Command() {
return (
<List>
<List.Item
title="Item 1"
subtitle="First item"
actions={
<ActionPanel>
<Action title="Open" onAction={() => console.log("opened")} />
<Action title="Delete" onAction={() => console.log("deleted")} />
</ActionPanel>
}
/>
</List>
);
}
第三部分:常用UI组件实战
List列表视图
List是Raycast扩展最常用的视图,适合展示可搜索的列表数据:
import { List, ActionPanel, Action, Icon } from "@raycast/api";
import { useState } from "react";
interface Item {
id: string;
title: string;
subtitle?: string;
url?: string;
}
const items: Item[] = [
{ id: "1", title: "GitHub", subtitle: "Code hosting", url: "https://github.com" },
{ id: "2", title: "Raycast", subtitle: "Productivity tool", url: "https://raycast.com" },
{ id: "3", title: "Hacker News", subtitle: "Tech news", url: "https://news.ycombinator.com" }
];
export default function Command() {
const [searchText, setSearchText] = useState("");
const filteredItems = items.filter(item =>
item.title.toLowerCase().includes(searchText.toLowerCase()) ||
item.subtitle?.toLowerCase().includes(searchText.toLowerCase())
);
return (
<List
searchBarPlaceholder="搜索网站..."
onSearchTextChange={setSearchText}
searchBarAccessory={
<List.Dropdown
tooltip="Filter"
storeValue={true}
onChange={(value) => setSearchText(value)}
>
<List.Dropdown.Item title="All" value="all" />
<List.Dropdown.Item title="Tech" value="tech" />
</List.Dropdown>
}
>
{filteredItems.map((item) => (
<List.Item
key={item.id}
title={item.title}
subtitle={item.subtitle}
icon={Icon.Globe}
accessories={[{ text: item.url }]}
actions={
<ActionPanel>
<Action.OpenInBrowser url={item.url} />
<Action.CopyToClipboard content={item.url} />
<Action.Push
title="查看详情"
target={<Detail markdown={`# ${item.title}\n\n${item.subtitle}`} />}
/>
</ActionPanel>
}
/>
))}
</List>
);
}
Detail详情视图
Detail视图展示Markdown格式的详细内容:
import { Detail, ActionPanel, Action, Icon } from "@raycast/api";
export default function Command() {
const markdown = `
# Raycast 扩展开发指南
## 项目结构
- \`src/\` - 源代码目录
- \`assets/\` - 图标和图片资源
- \`package.json\` - 扩展配置
## 核心概念
1. **Commands** - 扩展的主要功能
2. **Actions** - 列表项的操作
3. **Preferences** - 用户偏好设置
4. **Storage** - 本地数据存储
## 调试技巧
- 使用 \`ray develop\` 在开发模式下运行
- 使用 \`ray build\` 构建扩展
- 使用 \`ray publish\` 发布到Store
\`\`\`bash
# 开发模式
ray develop
\`\`\`
`;
return (
<Detail
markdown={markdown}
metadata={
<Detail.Metadata>
<Detail.Metadata.Label title="作者" text="Your Name" />
<Detail.Metadata.Label title="版本" text="1.0.0" />
<Detail.Metadata.Link
title="文档"
target="https://developers.raycast.com"
text="官方文档"
/>
<Detail.Metadata.TagList title="标签">
<Detail.Metadata.TagList.Item text="React" color="#61dafb" />
<Detail.Metadata.TagList.Item text="TypeScript" color="#3178c6" />
</Detail.Metadata.TagList>
</Detail.Metadata>
}
actions={
<ActionPanel>
<Action.OpenInBrowser url="https://developers.raycast.com" />
<Action.CopyToClipboard content="https://developers.raycast.com" />
</ActionPanel>
}
/>
);
}
Form表单视图
Form用于收集用户输入:
import { Form, ActionPanel, Action, showToast, Toast } from "@raycast/api";
import { useState } from "react";
interface FormValues {
name: string;
email: string;
description: string;
type: string;
}
export default function Command() {
const [nameError, setNameError] = useState<string | undefined>();
const [emailError, setEmailError] = useState<string | undefined>();
function handleSubmit(values: FormValues) {
if (!values.name) {
setNameError("名称不能为空");
return;
}
if (!values.email || !values.email.includes("@")) {
setEmailError("邮箱格式不正确");
return;
}
showToast({
style: Toast.Style.Success,
title: "提交成功",
message: `已提交: ${values.name}`
});
}
return (
<Form
actions={
<ActionPanel>
<Action.SubmitForm title="提交" onSubmit={handleSubmit} />
</ActionPanel>
}
>
<Form.TextField
id="name"
title="名称"
placeholder="请输入名称"
error={nameError}
onChange={() => setNameError(undefined)}
/>
<Form.TextField
id="email"
title="邮箱"
placeholder="example@email.com"
error={emailError}
onChange={() => setEmailError(undefined)}
/>
<Form.TextArea
id="description"
title="描述"
placeholder="详细描述..."
/>
<Form.Dropdown id="type" title="类型">
<Form.Dropdown.Item value="bug" title="Bug反馈" />
<Form.Dropdown.Item value="feature" title="功能建议" />
<Form.Dropdown.Item value="other" title="其他" />
</Form.Dropdown>
<Form.Separator />
<Form.Checkbox id="subscribe" title="订阅通知" label="同意接收邮件通知" />
<Form.DatePicker id="dueDate" title="截止日期" />
</Form>
);
}
第四部分:API集成实战
使用useFetch Hook
@raycast/utils提供了强大的数据获取Hook:
import { List, ActionPanel, Action, Icon, showToast, Toast } from "@raycast/api";
import { useFetch } from "@raycast/utils";
interface Repo {
id: number;
name: string;
full_name: string;
description: string;
stargazers_count: number;
language: string;
html_url: string;
}
interface SearchResponse {
total_count: number;
items: Repo[];
}
export default function Command() {
const { isLoading, data, error } = useFetch<SearchResponse>(
"https://api.github.com/search/repositories?q=raycast&sort=stars",
{
headers: {
"Accept": "application/vnd.github.v3+json"
},
onError: (error) => {
showToast({
style: Toast.Style.Failure,
title: "获取失败",
message: error.message
});
}
}
);
if (error) {
return <List><List.Item title="加载失败" /></List>;
}
return (
<List isLoading={isLoading} searchBarPlaceholder="搜索GitHub仓库...">
{data?.items.map((repo) => (
<List.Item
key={repo.id}
title={repo.full_name}
subtitle={repo.description}
icon={Icon.Code}
accessories={[
{ text: `⭐ ${repo.stargazers_count}` },
{ text: repo.language }
]}
actions={
<ActionPanel>
<Action.OpenInBrowser url={repo.html_url} />
<Action.CopyToClipboard content={repo.html_url} />
</ActionPanel>
}
/>
))}
</List>
);
}
OAuth认证集成
对于需要用户授权的API,使用OAuth flow:
import { OAuth } from "@raycast/api";
const client = new OAuth.PKCEClient({
providerName: "GitHub",
providerIcon: "github-icon", // 需要提供图标
description: "连接你的GitHub账户以访问仓库",
});
const clientId = "YOUR_GITHUB_CLIENT_ID";
const scope = "repo user";
export async function authorize() {
const tokenSet = await client.getTokens();
if (tokenSet?.accessToken) {
return tokenSet.accessToken;
}
const authRequest = await client.authorizeRequest({
endpoint: "https://github.com/login/oauth/authorize",
clientId: clientId,
scope: scope,
});
const { authorizationCode } = await client.authorize(authRequest);
const tokens = await client.exchangeAuthorizationCode(authRequest, authorizationCode);
await client.setTokens(tokens);
return tokens.accessToken;
}
export async function fetchGitHubAPI(path: string) {
const token = await authorize();
const response = await fetch(`https://api.github.com${path}`, {
headers: {
Authorization: `token ${token}`,
Accept: "application/vnd.github.v3+json"
}
});
return response.json();
}
第五部分:本地数据存储
使用LocalStorage
Raycast提供简单的键值存储:
import { LocalStorage } from "@raycast/api";
export async function saveBookmark(name: string, url: string) {
const bookmarks = await getBookmarks();
const id = Date.now().toString();
bookmarks[id] = { name, url, createdAt: new Date().toISOString() };
await LocalStorage.setItem("bookmarks", JSON.stringify(bookmarks));
return id;
}
export async function getBookmarks(): Promise<Record<string, any>> {
const stored = await LocalStorage.getItem<string>("bookmarks");
return stored ? JSON.parse(stored) : {};
}
export async function deleteBookmark(id: string) {
const bookmarks = await getBookmarks();
delete bookmarks[id];
await LocalStorage.setItem("bookmarks", JSON.stringify(bookmarks));
}
用户偏好设置
通过package.json定义偏好设置:
{
"commands": [
{
"name": "my-command",
"title": "My Command",
"description": "Command with preferences",
"preferences": [
{
"name": "apiKey",
"title": "API Key",
"description": "Your API key for the service",
"type": "password",
"required": true
},
{
"name": "language",
"title": "Language",
"description": "Display language",
"type": "dropdown",
"default": "en",
"data": [
{ "title": "English", "value": "en" },
{ "title": "中文", "value": "zh" }
]
},
{
"name": "maxResults",
"title": "Max Results",
"description": "Maximum number of results to show",
"type": "textfield",
"default": "10"
}
]
}
]
}
在代码中读取偏好:
import { getPreferenceValues } from "@raycast/api";
interface Preferences {
apiKey: string;
language: string;
maxResults: string;
}
const preferences = getPreferenceValues<Preferences>();
console.log(preferences.apiKey, preferences.language, preferences.maxResults);
第六部分:调试与发布
开发模式
使用开发模式实时调试:
# 启动开发模式
ray develop
# 调试时启用开发者工具
# 在Raycast中按 Cmd+, 打开设置 → Advanced → Show Debug Menu
# 然后在扩展中右键选择"Inspect Element"
开发模式支持热重载,修改代码后自动应用变更。
构建与发布
开发完成后,发布到Raycast Store:
# 1. 在package.json中更新版本
# "version": "1.0.0"
# 2. 验证扩展
ray lint
# 3. 登录Raycast账户(如未登录)
ray login
# 4. 发布到Store
ray publish
# 发布过程中会询问:
# - 是否包含二进制依赖
# - 是否公开发布(Public)或私有(Private)
# - 首次发布需要填写扩展分类、截图等信息
版本管理
维护扩展的版本迭代:
# 1.0.0 - 首次发布
# 1.0.1 - Bug修复
# 1.1.0 - 新功能
# 2.0.0 - 重大更新
# 更新版本号
npm version patch # 1.0.0 → 1.0.1
npm version minor # 1.0.1 → 1.1.0
npm version major # 1.1.0 → 2.0.0
# 重新发布
ray publish
第七部分:黑苹果Raycast开发特殊说明
Intel架构的注意事项
黑苹果的Intel x86_64架构对Raycast开发影响极小,但有一些细节:
- Node.js版本:使用Intel版本Node.js,避免与Apple Silicon特定包的兼容问题
- 原生模块编译:少数扩展依赖原生Node模块(如better-sqlite3),需要x86_64的node-gyp工具链
- 性能表现:黑苹果的强劲CPU让ray develop的TypeScript编译和热重载非常迅速
- OpenCore无影响:Raycast作为纯应用层工具,不受OpenCore EFI配置影响
开发效率提升技巧
- 使用TypeScript严格模式:在tsconfig.json中启用strict,避免常见错误
- VS Code扩展:安装"Raycast"官方扩展,提供代码片段和实时预览
- 复用组件库:将常用UI模式封装为可复用组件
- 使用AI辅助开发:Raycast内置AI功能,开发AI扩展特别方便
结语:从使用者到创造者
Raycast扩展开发将React、TypeScript和macOS生态完美结合,让任何有前端经验的开发者都能快速构建强大的macOS工具。从简单的命令面板到集成复杂API的工作流自动化,Raycast扩展的可能性几乎是无限的。
在黑苹果上,你不仅可以使用现成的扩展提升日常效率,更可以开发专属工具解决个性化的工作流痛点。从今天开始,尝试开发你的第一个Raycast扩展,开启从使用者到创造者的进化之旅。


评论(0)