创建 Convex 组件
创建具有清晰边界和小型面向应用 API 的可复用 Convex 组件。
何时使用
- 在现有应用中创建新的 Convex 组件
- 将可复用后端逻辑提取到组件中
- 构建应当拥有自身表和工作流的第三方集成
- 打包 Convex 功能以便在多个应用中复用
何时不使用
- 属于主应用的一次性业务逻辑
- 不需要 Convex 表或函数的薄工具函数
- 应保留在
convex/中的应用级编排 - 普通 TypeScript 库已经足够的场景
工作流
- 询问用户正在构建什么以及最终目标是什么。如果仓库已经使答案显而易见,请说明这一点,并在继续之前确认。
- 使用下面的决策树选择形态,并阅读匹配的参考文件。
- 判断是否有必要使用组件。如果功能不需要隔离的表、后端函数或可复用持久状态,优先使用普通应用代码或普通库。
- 为以下内容制定简短计划:
- 组件拥有哪些表
- 它暴露哪些公共函数
- 必须从应用传入哪些数据(认证、环境变量、父级 ID)
- 哪些内容作为包装器或 HTTP 挂载保留在应用中
- 使用
convex.config.ts、schema.ts和函数文件创建组件结构。 - 使用组件自身的
./_generated/server导入实现函数,而不是应用的生成文件。 - 使用
app.use(...)将组件接入应用。如果应用尚没有convex/convex.config.ts,请创建它。 - 在应用中使用
ctx.runQuery、ctx.runMutation或ctx.runAction通过components.<name>调用组件。 - 如果 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 作为字符串跨边界传递,因为在面向应用的
ComponentApi中,Id类型会变成普通字符串。 - 不要在组件参数或 schema 中对应用拥有的表使用
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(),
}
高级模式
有关其他模式,包括用于回调的函数句柄、从 schema 派生验证器、使用 globals 表进行静态配置,以及基于类的客户端包装器,请参见 references/advanced-patterns.md。
验证
按此顺序尝试验证:
npx convex codegen --component-dir convex/components/<name>npx convex codegennpx convex dev
重要:
- 新仓库在配置
CONVEX_DEPLOYMENT之前可能会使这些命令失败。 - 在代码生成运行之前,组件本地的
./_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并修复代码生成或类型问题