components-guide by get-convex/agent-skills
npx skills add https://github.com/get-convex/agent-skills --skill components-guide使用组件来封装功能,构建可维护、可复用的后端系统。
组件是自包含的迷你后端,它们捆绑了:
可以将它们视为: 你后端的 npm 包,或者无需复杂部署的微服务。
convex/
users.ts (500 行)
files.ts (600 行 - 上传、存储、权限、速率限制)
payments.ts (400 行 - Stripe、webhooks、计费)
notifications.ts (300 行)
analytics.ts (200 行)
总计:一个庞大的代码库,所有内容混杂在一起
convex/
components/
storage/ (文件上传 - 可复用)
billing/ (支付 - 可复用)
notifications/ (通知 - 可复用)
analytics/ (分析 - 可复用)
convex.config.ts (连接组件)
domain/ (你的实际业务逻辑)
users.ts (50 行 - 使用组件)
projects.ts (75 行 - 使用组件)
总计:清晰、专注、可复用
# 从 npm 安装官方组件
npm install @convex-dev/ratelimiter
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
import { defineApp } from "convex/server";
import ratelimiter from "@convex-dev/ratelimiter/convex.config";
export default defineApp({
components: {
ratelimiter,
},
});
import { components } from "./_generated/api";
export const createPost = mutation({
handler: async (ctx, args) => {
// 使用组件
await components.ratelimiter.check(ctx, {
key: `user:${ctx.user._id}`,
limit: 10,
period: 60000, // 每分钟 10 次请求
});
return await ctx.db.insert("posts", args);
},
});
多个组件在同一层级协同工作:
// convex.config.ts
export default defineApp({
components: {
// 同级组件 - 每个处理一个关注点
auth: authComponent,
storage: storageComponent,
payments: paymentsComponent,
emails: emailComponent,
analytics: analyticsComponent,
},
});
// convex/subscriptions.ts
import { components } from "./_generated/api";
export const subscribe = mutation({
args: { plan: v.string() },
handler: async (ctx, args) => {
// 1. 验证身份验证 (auth 组件)
const user = await components.auth.getCurrentUser(ctx);
// 2. 创建支付 (payments 组件)
const subscription = await components.payments.createSubscription(ctx, {
userId: user._id,
plan: args.plan,
amount: getPlanAmount(args.plan),
});
// 3. 跟踪转化 (analytics 组件)
await components.analytics.track(ctx, {
event: "subscription_created",
userId: user._id,
plan: args.plan,
});
// 4. 发送确认邮件 (emails 组件)
await components.emails.send(ctx, {
to: user.email,
template: "subscription_welcome",
data: { plan: args.plan },
});
// 5. 在主应用中存储订阅信息
await ctx.db.insert("subscriptions", {
userId: user._id,
paymentId: subscription.id,
plan: args.plan,
status: "active",
});
return subscription;
},
});
浏览 组件目录:
好的理由:
不好的理由:
mkdir -p convex/components/notifications
// 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.id("users"),
message: v.string(),
read: v.boolean(),
createdAt: v.number(),
})
.index("by_user", ["userId"])
.index("by_user_and_read", ["userId", "read"]),
});
// convex/components/notifications/send.ts
import { mutation } from "./_generated/server";
import { v } from "convex/values";
export const send = mutation({
args: {
userId: v.id("users"),
message: v.string(),
},
handler: async (ctx, args) => {
await ctx.db.insert("notifications", {
userId: args.userId,
message: args.message,
read: false,
createdAt: Date.now(),
});
},
});
export const markRead = mutation({
args: { notificationId: v.id("notifications") },
handler: async (ctx, args) => {
await ctx.db.patch(args.notificationId, { read: true });
},
});
// convex/components/notifications/read.ts
import { query } from "./_generated/server";
import { v } from "convex/values";
export const list = query({
args: { userId: v.id("users") },
handler: async (ctx, args) => {
return await ctx.db
.query("notifications")
.withIndex("by_user", q => q.eq("userId", args.userId))
.order("desc")
.collect();
},
});
export const unreadCount = query({
args: { userId: v.id("users") },
handler: async (ctx, args) => {
const unread = await ctx.db
.query("notifications")
.withIndex("by_user_and_read", q =>
q.eq("userId", args.userId).eq("read", false)
)
.collect();
return unread.length;
},
});
// 主应用调用组件
await components.storage.upload(ctx, file);
await components.analytics.track(ctx, event);
// 主应用编排多个组件
await components.auth.verify(ctx);
const file = await components.storage.upload(ctx, data);
await components.notifications.send(ctx, message);
// 将父级表的 ID 传递给组件
await components.audit.log(ctx, {
userId: user._id, // 来自父级的 users 表
action: "delete",
resourceId: task._id, // 来自父级的 tasks 表
});
// 组件将这些存储为字符串/ID
// 但不直接访问父级表
// 在组件代码内部 - 不要这样做
const user = await ctx.db.get(userId); // 错误!无法访问父级表
组件不能直接互相调用。如果你需要这种功能,它们应该放在主应用中,或者重新设计。
每个组件做好一件事:
// 只导出需要的内容
export { upload, download, delete } from "./storage";
// 保持内部函数私有
// (不要导出辅助函数)
// 良好:将数据作为参数传递
await components.audit.log(ctx, {
userId: user._id,
action: "delete"
});
// 错误:组件访问父级表
// (虽然不可能,但说明了原则)
{
"name": "@yourteam/notifications-component",
"version": "1.0.0"
}
包含 README,说明:
npm install @convex-dev/component-nameconvex.config.ts 中配置每周安装量
546
代码仓库
GitHub 星标
15
首次出现
2026年2月18日
安全审计
安装于
opencode540
github-copilot540
codex540
kimi-cli539
amp539
gemini-cli539
Use components to encapsulate features and build maintainable, reusable backends.
Components are self-contained mini-backends that bundle:
Think of them as: npm packages for your backend, or microservices without the deployment complexity.
convex/
users.ts (500 lines)
files.ts (600 lines - upload, storage, permissions, rate limiting)
payments.ts (400 lines - Stripe, webhooks, billing)
notifications.ts (300 lines)
analytics.ts (200 lines)
Total: One big codebase, everything mixed together
convex/
components/
storage/ (File uploads - reusable)
billing/ (Payments - reusable)
notifications/ (Alerts - reusable)
analytics/ (Tracking - reusable)
convex.config.ts (Wire components together)
domain/ (Your actual business logic)
users.ts (50 lines - uses components)
projects.ts (75 lines - uses components)
Total: Clean, focused, reusable
# Official components from npm
npm install @convex-dev/ratelimiter
import { defineApp } from "convex/server";
import ratelimiter from "@convex-dev/ratelimiter/convex.config";
export default defineApp({
components: {
ratelimiter,
},
});
import { components } from "./_generated/api";
export const createPost = mutation({
handler: async (ctx, args) => {
// Use the component
await components.ratelimiter.check(ctx, {
key: `user:${ctx.user._id}`,
limit: 10,
period: 60000, // 10 requests per minute
});
return await ctx.db.insert("posts", args);
},
});
Multiple components working together at the same level:
// convex.config.ts
export default defineApp({
components: {
// Sibling components - each handles one concern
auth: authComponent,
storage: storageComponent,
payments: paymentsComponent,
emails: emailComponent,
analytics: analyticsComponent,
},
});
// convex/subscriptions.ts
import { components } from "./_generated/api";
export const subscribe = mutation({
args: { plan: v.string() },
handler: async (ctx, args) => {
// 1. Verify authentication (auth component)
const user = await components.auth.getCurrentUser(ctx);
// 2. Create payment (payments component)
const subscription = await components.payments.createSubscription(ctx, {
userId: user._id,
plan: args.plan,
amount: getPlanAmount(args.plan),
});
// 3. Track conversion (analytics component)
await components.analytics.track(ctx, {
event: "subscription_created",
userId: user._id,
plan: args.plan,
});
// 4. Send confirmation (emails component)
await components.emails.send(ctx, {
to: user.email,
template: "subscription_welcome",
data: { plan: args.plan },
});
// 5. Store subscription in main app
await ctx.db.insert("subscriptions", {
userId: user._id,
paymentId: subscription.id,
plan: args.plan,
status: "active",
});
return subscription;
},
});
Browse Component Directory:
Good reasons:
Not good reasons:
mkdir -p convex/components/notifications
// 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.id("users"),
message: v.string(),
read: v.boolean(),
createdAt: v.number(),
})
.index("by_user", ["userId"])
.index("by_user_and_read", ["userId", "read"]),
});
// convex/components/notifications/send.ts
import { mutation } from "./_generated/server";
import { v } from "convex/values";
export const send = mutation({
args: {
userId: v.id("users"),
message: v.string(),
},
handler: async (ctx, args) => {
await ctx.db.insert("notifications", {
userId: args.userId,
message: args.message,
read: false,
createdAt: Date.now(),
});
},
});
export const markRead = mutation({
args: { notificationId: v.id("notifications") },
handler: async (ctx, args) => {
await ctx.db.patch(args.notificationId, { read: true });
},
});
// convex/components/notifications/read.ts
import { query } from "./_generated/server";
import { v } from "convex/values";
export const list = query({
args: { userId: v.id("users") },
handler: async (ctx, args) => {
return await ctx.db
.query("notifications")
.withIndex("by_user", q => q.eq("userId", args.userId))
.order("desc")
.collect();
},
});
export const unreadCount = query({
args: { userId: v.id("users") },
handler: async (ctx, args) => {
const unread = await ctx.db
.query("notifications")
.withIndex("by_user_and_read", q =>
q.eq("userId", args.userId).eq("read", false)
)
.collect();
return unread.length;
},
});
// Main app calls component
await components.storage.upload(ctx, file);
await components.analytics.track(ctx, event);
// Main app orchestrates multiple components
await components.auth.verify(ctx);
const file = await components.storage.upload(ctx, data);
await components.notifications.send(ctx, message);
// Pass IDs from parent's tables to component
await components.audit.log(ctx, {
userId: user._id, // From parent's users table
action: "delete",
resourceId: task._id, // From parent's tasks table
});
// Component stores these as strings/IDs
// but doesn't access parent tables directly
// Inside component code - DON'T DO THIS
const user = await ctx.db.get(userId); // Error! Can't access parent tables
Components can't call each other directly. If you need this, they should be in the main app or refactor the design.
Each component does ONE thing well:
// Export only what's needed
export { upload, download, delete } from "./storage";
// Keep internals private
// (Don't export helper functions)
// Good: Pass data as arguments
await components.audit.log(ctx, {
userId: user._id,
action: "delete"
});
// Bad: Component accesses parent tables
// (Not even possible, but shows the principle)
{
"name": "@yourteam/notifications-component",
"version": "1.0.0"
}
Include README with:
npm install @convex-dev/component-nameconvex.config.tsWeekly Installs
546
Repository
GitHub Stars
15
First Seen
Feb 18, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykWarn
Installed on
opencode540
github-copilot540
codex540
kimi-cli539
amp539
gemini-cli539
React 组合模式指南:Vercel 组件架构最佳实践,提升代码可维护性
106,200 周安装