关于 convex-create-component
用于设计和实现可复用 Convex 组件的技能,强调应用与组件之间的清晰边界。它指导开发者选择本地、打包或混合结构,并完成接入与验证。
主要功能
- 01选择本地、打包或混合组件
- 02规划数据表与公开 API
- 03创建组件文件并接入应用
- 04将认证和环境访问留在应用侧
- 05使用 Convex codegen 和 dev 验证
适用场景
- 01从应用中提取可复用后端逻辑
- 02构建拥有独立数据表和工作流的集成
- 03在多个应用间共享 Convex 功能
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ convex/ agent-skills --skill convex-create-component安装后在 Claude Code 中输入/convex-create-component确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
agents/openai.yamlassets/icon.svgreferences/advanced-patterns.mdhybrid-components.mdlocal-components.mdpackaged-components.md详细介绍
Convex 创建组件
创建边界清晰、面向应用的 API 简洁且可复用的 Convex 组件。
适用场景
- 在现有应用中创建新的 Convex 组件
- 将可复用的后端逻辑提取到组件中
- 构建应拥有自己的表和工作流的第三方集成
- 打包 Convex 功能,以便在多个应用中复用
不适用场景
- 属于主应用的一次性业务逻辑
- 不需要 Convex 表或函数的轻量工具
- 应留在
convex/中的应用级编排 - 普通 TypeScript 库就足以满足需求的情况
工作流
- 询问用户正在构建什么以及最终目标是什么。如果仓库中已有的信息足以明确答案,请说明并在继续前确认。
- 根据下面的决策树选择组件形态,并阅读对应的参考文件。
- 判断是否有必要使用组件。如果功能不需要隔离的表、后端函数或可复用的持久化状态,请优先使用普通应用代码或常规库。
- 制定简短计划,说明:
- 组件拥有哪些表
- 它公开哪些函数
- 必须从应用传入哪些数据(身份验证、环境变量、父级 ID)
- 哪些内容作为封装函数或 HTTP 挂载留在应用中
- 使用
convex.config.ts、schema.ts和函数文件创建组件结构。 - 实现函数时,使用组件自己的
./_generated/server导入,而不是应用生成的文件。 - 使用
app.use(...)将组件接入应用。如果应用尚无convex/convex.config.ts,请创建该文件。 - 在应用中通过
components.<name>,使用ctx.runQuery、ctx.runMutation或ctx.runAction调用组件。 - 如果 React 客户端、HTTP 调用方或公开 API 需要访问组件,请在应用中创建封装函数,而不要直接公开组件函数。
- 运行
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。
验证
请按以下顺序尝试验证:
npx convex codegen --component-dir convex/components/<name>npx convex codegennpx convex dev
重要提示:
- 新建的代码库可能会在配置
CONVEX_DEPLOYMENT之前无法运行这些命令。 - 在 codegen 运行之前,组件本地的
./_generated/*导入和应用端的components.<name>...引用都无法通过类型检查。 - 如果验证因 Convex 登录或部署设置而受阻,请停止并向用户询问该确切步骤,不要猜测。
参考文件
用户确认目标后,请从以下文件中准确阅读一个:
references/local-components.mdreferences/packaged-components.mdreferences/hybrid-components.md
官方文档: 编写组件
检查清单
- 已询问用户想要构建什么,并确认了具体形式
- 已阅读相应的参考文件
- 已确认组件是合适的抽象
- 已规划表、公开 API、边界和应用包装函数
- 组件位于
convex/components/<name>/下(如果要发布,则采用包布局) - 组件从自己的
./_generated/server导入 - 身份验证、环境变量访问和 HTTP 路由都保留在应用中
- 父应用 ID 以
v.string()跨越边界 - 公开函数具有
args和returns验证器 - 已运行
npx convex dev并修复 codegen 或类型问题
评论
支持 Markdown