黑苹果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扩展,开启从使用者到创造者的进化之旅。

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。