mcpField
C
convex-create-component

convex-create-component

github.com/convex开发69132.0k 次安装1 天前更新
代码仓库

关于 convex-create-component

用于设计和实现可复用 Convex 组件的技能,强调应用与组件之间的清晰边界。它指导开发者选择本地、打包或混合结构,并完成接入与验证。

主要功能

  1. 01选择本地、打包或混合组件
  2. 02规划数据表与公开 API
  3. 03创建组件文件并接入应用
  4. 04将认证和环境访问留在应用侧
  5. 05使用 Convex codegen 和 dev 验证

适用场景

  1. 01从应用中提取可复用后端逻辑
  2. 02构建拥有独立数据表和工作流的集成
  3. 03在多个应用间共享 Convex 功能

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/convex/agent-skills --skill convex-create-component

安装后在 Claude Code 中输入/convex-create-component确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

agents/
openai.yaml
assets/
icon.svg
references/
advanced-patterns.md
hybrid-components.md
local-components.md
packaged-components.md

详细介绍

Convex 创建组件

创建边界清晰、面向应用的 API 简洁且可复用的 Convex 组件。

适用场景

  • 在现有应用中创建新的 Convex 组件
  • 将可复用的后端逻辑提取到组件中
  • 构建应拥有自己的表和工作流的第三方集成
  • 打包 Convex 功能,以便在多个应用中复用

不适用场景

  • 属于主应用的一次性业务逻辑
  • 不需要 Convex 表或函数的轻量工具
  • 应留在 convex/ 中的应用级编排
  • 普通 TypeScript 库就足以满足需求的情况

工作流

  1. 询问用户正在构建什么以及最终目标是什么。如果仓库中已有的信息足以明确答案,请说明并在继续前确认。
  2. 根据下面的决策树选择组件形态,并阅读对应的参考文件。
  3. 判断是否有必要使用组件。如果功能不需要隔离的表、后端函数或可复用的持久化状态,请优先使用普通应用代码或常规库。
  4. 制定简短计划,说明:
    • 组件拥有哪些表
    • 它公开哪些函数
    • 必须从应用传入哪些数据(身份验证、环境变量、父级 ID)
    • 哪些内容作为封装函数或 HTTP 挂载留在应用中
  5. 使用 convex.config.ts、schema.ts 和函数文件创建组件结构。
  6. 实现函数时,使用组件自己的 ./_generated/server 导入,而不是应用生成的文件。
  7. 使用 app.use(...) 将组件接入应用。如果应用尚无 convex/convex.config.ts,请创建该文件。
  8. 在应用中通过 components.<name>,使用 ctx.runQuery、ctx.runMutation 或 ctx.runAction 调用组件。
  9. 如果 React 客户端、HTTP 调用方或公开 API 需要访问组件,请在应用中创建封装函数,而不要直接公开组件函数。
  10. 运行 npx convex dev,并在完成前修复代码生成、类型或边界问题。

选择组件形态

询问用户,然后选择一条路径:

目标形态参考文件
仅供此应用使用的组件本地references/local-components.md
发布或在多个应用间共享打包references/packaged-components.md
用户明确需要本地代码和共享库代码混合references/hybrid-components.md
不确定默认选择本地references/local-components.md

继续前,只阅读一个参考文件。

默认做法

除非用户明确需要 npm 包,否则默认使用本地组件:

  • 将其放在 convex/components/<componentName>/ 下
  • 在其自己的 convex.config.ts 中使用 defineComponent(...) 定义组件
  • 在应用的 convex/convex.config.ts 中通过 app.use(...) 安装组件
  • 让 npx convex dev 生成组件自己的 _generated/ 文件

组件骨架

一个包含一个表和两个函数的最小本地组件,以及应用接入代码。

// convex/components/notifications/convex.config.ts
import { defineComponent } from "convex/server";

export default defineComponent("notifications");
// convex/components/notifications/schema.ts
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";

export default defineSchema({
  notifications: defineTable({
    userId: v.string(),
    message: v.string(),
    read: v.boolean(),
  }).index("by_user_read", ["userId", "read"]),
});
// convex/components/notifications/lib.ts
import { v } from "convex/values";
import { mutation, query } from "./_generated/server.js";

export const send = mutation({
  args: { userId: v.string(), message: v.string() },
  returns: v.id("notifications"),
  handler: async (ctx, args) => {
    return await ctx.db.insert("notifications", {
      userId: args.userId,
      message: args.message,
      read: false,
    });
  },
});

export const listUnread = query({
  args: { userId: v.string() },
  returns: v.array(
    v.object({
      _id: v.id("notifications"),
      _creationTime: v.number(),
      userId: v.string(),
      message: v.string(),
      read: v.boolean(),
    }),
  ),
  handler: async (ctx, args) => {
    return await ctx.db
      .query("notifications")
      .withIndex("by_user_read", (q) =>
        q.eq("userId", args.userId).eq("read", false),
      )
      .collect();
  },
});
// convex/convex.config.ts
import { defineApp } from "convex/server";
import notifications from "./components/notifications/convex.config.js";

const app = defineApp();
app.use(notifications);

export default app;
// convex/notifications.ts  (app-side wrapper)
import { v } from "convex/values";
import { mutation, query } from "./_generated/server";
import { components } from "./_generated/api";
import { getAuthUserId } from "@convex-dev/auth/server";

export const sendNotification = mutation({
  args: { message: v.string() },
  returns: v.null(),
  handler: async (ctx, args) => {
    const userId = await getAuthUserId(ctx);
    if (!userId) throw new Error("Not authenticated");

    await ctx.runMutation(components.notifications.lib.send, {
      userId,
      message: args.message,
    });
    return null;
  },
});

export const myUnread = query({
  args: {},
  handler: async (ctx) => {
    const userId = await getAuthUserId(ctx);
    if (!userId) throw new Error("Not authenticated");

    return await ctx.runQuery(components.notifications.lib.listUnread, {
      userId,
    });
  },
});

请注意引用路径的形式:应用中调用 convex/components/notifications/lib.ts 中的函数时,路径为 components.notifications.lib.send。

关键规则

  • 将身份验证留在应用中,因为组件内部无法使用 ctx.auth。
  • 将环境变量访问留在应用中,因为组件函数无法读取 process.env。
  • 跨边界传递父应用 ID 时,将其作为字符串,因为 Id 类型在面向应用的 ComponentApi 中会变成普通字符串。
  • 不要在组件参数或架构中对应用拥有的表使用 v.id("parentTable"),因为组件无法访问应用的表命名空间。
  • 从组件自己的 ./_generated/server 导入 query、mutation 和 action,不要从应用生成的文件中导入。
  • 不要直接向客户端公开组件函数。需要客户端访问时,请创建应用包装函数,因为组件是内部的,需要应用提供身份验证和环境变量接入。
  • 如果组件定义了 HTTP 处理程序,请在应用的 convex/http.ts 中挂载路由,因为组件无法注册自己的 HTTP 路由。
  • 如果组件需要分页,请使用 convex-helpers 中的 paginator,而不要使用内置的 .paginate(),因为 .paginate() 无法跨越组件边界。
  • 为查询字段定义索引,而不要在数据库查询后使用 Convex .filter()。
  • 为所有公开的组件函数添加 args 和 returns 验证器,因为组件边界要求显式的类型契约。

模式

身份验证和环境变量访问
// Bad: component code cannot rely on app auth or env
const identity = await ctx.auth.getUserIdentity();
const apiKey = process.env.OPENAI_API_KEY;
// Good: the app resolves auth and env, then passes explicit values
const userId = await getAuthUserId(ctx);
if (!userId) throw new Error("Not authenticated");

await ctx.runAction(components.translator.translate, {
  userId,
  apiKey: process.env.OPENAI_API_KEY,
  text: args.text,
});
面向客户端的 API
// Bad: assuming a component function is directly callable by clients
export const send = components.notifications.send;
// Good: re-export through an app mutation or query
export const sendNotification = mutation({
  args: { message: v.string() },
  returns: v.null(),
  handler: async (ctx, args) => {
    const userId = await getAuthUserId(ctx);
    if (!userId) throw new Error("Not authenticated");

    await ctx.runMutation(components.notifications.lib.send, {
      userId,
      message: args.message,
    });
    return null;
  },
});
跨边界传递 ID
// Bad: parent app table IDs are not valid component validators
args: {
  userId: v.id("users"),
}
// Good: treat parent-owned IDs as strings at the boundary
args: {
  userId: v.string(),
}
高级模式

有关其他模式,包括用于回调的函数句柄、从架构派生验证器、使用 globals 表进行静态配置,以及基于类的客户端包装器,请参阅 references/advanced-patterns.md。

验证

请按以下顺序尝试验证:

  1. npx convex codegen --component-dir convex/components/<name>
  2. npx convex codegen
  3. npx convex dev

重要提示:

  • 新建的代码库可能会在配置 CONVEX_DEPLOYMENT 之前无法运行这些命令。
  • 在 codegen 运行之前,组件本地的 ./_generated/* 导入和应用端的 components.<name>... 引用都无法通过类型检查。
  • 如果验证因 Convex 登录或部署设置而受阻,请停止并向用户询问该确切步骤,不要猜测。

参考文件

用户确认目标后,请从以下文件中准确阅读一个:

  • references/local-components.md
  • references/packaged-components.md
  • references/hybrid-components.md

官方文档: 编写组件

检查清单

  • 已询问用户想要构建什么,并确认了具体形式
  • 已阅读相应的参考文件
  • 已确认组件是合适的抽象
  • 已规划表、公开 API、边界和应用包装函数
  • 组件位于 convex/components/<name>/ 下(如果要发布,则采用包布局)
  • 组件从自己的 ./_generated/server 导入
  • 身份验证、环境变量访问和 HTTP 路由都保留在应用中
  • 父应用 ID 以 v.string() 跨越边界
  • 公开函数具有 args 和 returns 验证器
  • 已运行 npx convex dev 并修复 codegen 或类型问题

评论

支持 Markdown