关于 convex-component-authoring
convex-component-authoring 是构建独立 Convex 组件的技能,涵盖组件自有的架构、函数和类型化客户端封装。它说明组件边界,以及父应用如何注册和调用组件。
主要功能
- 01组件自有架构与函数
- 02类型化客户端封装
- 03父应用注册方式
- 04边界与 ID 处理规则
- 05发布指南
适用场景
- 01将可复用后端功能提取为包
- 02为 Convex 目录构建组件
- 03在多个应用间复用集成
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ waynesutton/ builder-skills --skill convex-component-authoring安装后在 Claude Code 中输入/convex-component-authoring确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
agents/openai.yamlassets/small-logo.svgreferences/publishing.md详细介绍
Convex 组件编写
生成一个自包含组件:包含自己的 convex.config.ts、schema、函数,以及供父应用调用的类型化客户端封装。唯一的规则是:组件无法读取父应用的表、ctx.auth 或 process.env。组件所需的一切都由应用作为参数传入。
何时使用组件
- 将某项功能(计数器、速率限制、审计日志、通知)提取为一个包
- 为 Convex 组件目录构建组件
- 父应用的
components.<name>中缺少某个组件的函数 - 第三方集成需要管理自己的表和后台作业
- 多个应用需要复用同一个后端模块
组件还是辅助函数
| 需求 | 使用 |
|---|---|
| 共享操作应用自身表的逻辑 | convex/lib/ 中的普通 TypeScript 辅助函数 |
| 需要自己的表,且应用无法访问这些表 | 组件 |
| 在独立事务中运行的隔离函数 | 组件 |
| 通过一行安装命令供多个应用复用 | 组件 |
| 仅需 React hook 或客户端工具 | npm 库,无需组件 |
如果该功能不需要在 API 边界后保存持久状态,辅助函数的工作量更少,也更容易进行类型定义。
文件夹布局
my-component/
package.json
src/
component/
convex.config.ts # defineComponent("counter")
schema.ts # tables only this component can see
public.ts # queries, mutations, actions
_generated/ # created by npx convex codegen
client/
index.ts # wrapper class the app imports
example/
convex/
convex.config.ts # app.use(counter) for local testing
对于仅本地使用的组件,将 component/ 中的内容放到 convex/components/<name>/,并跳过 client/ 和 package.json。
定义组件
// src/component/convex.config.ts
import { defineComponent } from "convex/server";
const component = defineComponent("counter");
export default component;
// src/component/schema.ts
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
export default defineSchema({
counters: defineTable({
name: v.string(),
value: v.number(),
}).index("by_name", ["name"]),
});
函数应从组件自身的 _generated/server 导入 query 和 mutation,而不是从应用导入。每个公开函数都需要 args 和 returns 验证器;否则父应用会将其视为 any。
// src/component/public.ts
import { v } from "convex/values";
import { mutation, query } from "./_generated/server.js";
export const add = mutation({
args: { name: v.string(), amount: v.number() },
returns: v.number(),
handler: async (ctx, args) => {
const existing = await ctx.db
.query("counters")
.withIndex("by_name", (q) => q.eq("name", args.name))
.unique();
if (!existing) {
await ctx.db.insert("counters", { name: args.name, value: args.amount });
return args.amount;
}
const value = existing.value + args.amount;
await ctx.db.patch(existing._id, { value });
return value;
},
});
export const get = query({
args: { name: v.string() },
returns: v.number(),
handler: async (ctx, args) => {
const counter = await ctx.db
.query("counters")
.withIndex("by_name", (q) => q.eq("name", args.name))
.unique();
return counter?.value ?? 0;
},
});
在父应用中注册
// convex/convex.config.ts (parent app)
import { defineApp } from "convex/server";
import counter from "@acme/counter/convex.config.js";
// local component: import counter from "./components/counter/convex.config.js";
const app = defineApp();
app.use(counter);
// A second instance under a different name:
// app.use(counter, { name: "pageViews" });
export default app;
运行 npx convex dev。该命令会在应用的 convex/_generated/api 中生成 components.counter,并生成组件自身的 _generated/ 文件夹。引用路径与文件对应:public.ts 中的函数对应 components.counter.public.add。
在父应用中,组件函数会作为内部引用提供。使用 ctx.runQuery 和 ctx.runMutation 调用它们;客户端无法直接调用这些函数。
客户端封装类
封装类在应用的环境中运行,因此可以读取 ctx.auth 和 process.env,并传入组件所需的内容。它先接收组件引用,再接收选项。
// src/client/index.ts
import type {
GenericDataModel,
GenericMutationCtx,
GenericQueryCtx,
} from "convex/server";
import type { ComponentApi } from "../component/_generated/component.js";
// Pick only the capabilities each method needs so any ctx with runQuery works
type QueryCtx = Pick<GenericQueryCtx<GenericDataModel>, "runQuery">;
type MutationCtx = Pick<GenericMutationCtx<GenericDataModel>, "runMutation">;
export class Counter {
constructor(
public component: ComponentApi,
private options: { prefix?: string } = {},
) {}
private key(name: string): string {
return this.options.prefix ? `${this.options.prefix}:${name}` : name;
}
async add(ctx: MutationCtx, name: string, amount = 1): Promise<number> {
return await ctx.runMutation(this.component.public.add, {
name: this.key(name),
amount,
});
}
async get(ctx: QueryCtx, name: string): Promise<number> {
return await ctx.runQuery(this.component.public.get, { name: this.key(name) });
}
}
// convex/counter.ts (parent app)
import { v } from "convex/values";
import { mutation, query } from "./_generated/server";
import { components } from "./_generated/api";
import { Counter } from "@acme/counter";
const counter = new Counter(components.counter, { prefix: "app" });
// The app owns auth. The component only sees the string it is handed.
export const incrementMine = mutation({
args: {},
returns: v.number(),
handler: async (ctx) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) throw new Error("Not authenticated");
return await counter.add(ctx, identity.subject);
},
});
export const getMine = query({
args: {},
returns: v.number(),
handler: async (ctx) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) throw new Error("Not authenticated");
return await counter.get(ctx, identity.subject);
},
});
React 通过 api.counter.incrementMine 调用的就是这些应用侧函数。由应用决定身份验证和速率限制;组件则保持通用。
边界规则
- 不能访问父应用的表。 组件中的
v.id("users")指向名为users的组件表,而不是应用的表。父应用 ID 应使用v.string()接收。 - ID 会变成字符串。 组件中的每个
Id<"counters">在ComponentApi中都是string。应有意将它们作为字符串返回。 - 不能使用
ctx.auth。 在应用中解析用户,并传入userId。 - 不能访问应用的
process.env。 在defineComponent("name", { env: { API_KEY: v.string() } })中声明有类型的环境变量,并让应用通过app.use(c, { env: { API_KEY: ... } })提供,或者将值作为函数参数传入。 .paginate()无法跨越边界工作。 在组件内部使用convex-helpers中的paginator。- HTTP 路由:应用通过
httpPrefix挂载组件http.ts中的路由。组件 HTTP action 没有ctx.auth。 - 回调到应用:通过函数句柄传递:在应用中使用
createFunctionHandle(internal.x.y),将其存储为v.string(),再在组件中将其转换为FunctionHandle<"mutation">。
发布到 npm(exports map、构建顺序、README、版本管理)的内容见 references/publishing.md;如果组件将通过包而非本地文件夹安装,请阅读该文档。
常见错误
| 错误 | 出错原因 | 正确做法 |
|---|---|---|
components.<name> 中缺少函数 | 未添加 app.use(...),或之后未运行 npx convex dev | 在 convex.config.ts 中注册,运行开发命令,并检查引用路径是否与文件名匹配 |
从应用的 _generated/server 导入 mutation | 函数注册在应用中,而不是组件中 | 从组件自己的 ./_generated/server.js 导入 |
在 React 中调用 api.counter.add | 组件函数在父级中是内部引用 | 编写应用 mutation,调用 ctx.runMutation(components.counter.public.add) |
在组件参数中使用 v.id("users") | 每个组件中的表编号不同,导致验证失败 | 在边界处使用 v.string() |
在组件中调用 ctx.auth.getUserIdentity() | 始终返回无用户 | 在应用中进行身份验证,并传入 userId |
在组件中使用 process.env.MY_KEY | 运行时值为 undefined | 在 defineComponent 中声明环境变量,或将其作为参数传入 |
公共函数没有 returns | 父级会看到 any,失去类型安全性 | 为每个公共函数添加验证器 |
| 在模块作用域读取组件环境变量 | 部署分析期间值为 undefined | 在处理函数中读取 env.X |
检查清单
- 已确定需要组件,而不是辅助函数
-
convex.config.ts调用defineComponent("<name>")并导出该组件 - Schema 位于组件中;任何位置都没有针对父级表的
v.id() - 函数从组件自己的
_generated/server导入 - 每个公共函数都有
args和returns验证器 - 父级通过
app.use(...)注册组件,并且npx convex dev运行正常 - 客户端封装函数先接收
ComponentApi,再接收选项,并使用Pickctx 类型 - 应用侧封装函数负责处理身份验证,并以字符串形式传递 ID
- 组件中没有
ctx.auth、process.env或.paginate() - 已通过安装该组件的示例应用进行测试