关于 components-guide
介绍如何用独立组件组织 Convex 后端的指南技能,面向希望在应用中实现功能复用、数据隔离和清晰 API 边界的开发者。
主要功能
- 01组件拥有独立架构与数据
- 02组件配置与集成
- 03主应用到组件的通信模式
- 04创建可复用组件的指导
- 05从单体结构逐步迁移
适用场景
- 01从大型后端拆出存储或计费功能
- 02组合多个同级组件实现应用功能
- 03创建带有独立架构的可复用组件
- 04逐步迁移单体后端功能
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ get-convex/ convex-agent-plugins --skill components-guide安装后在 Claude Code 中输入/components-guide确认已加载。
详细介绍
Convex 组件指南
使用组件封装功能,构建易于维护且可复用的后端。
什么是 Convex 组件?
组件是自包含的小型后端,包含:
- 自己的数据库架构
- 自己的函数(查询、变更、操作)
- 自己的数据(相互隔离的表)
- 明确的 API 边界
**可以把它们看作:**后端的 npm 包,或无需承担部署复杂性的微服务。
为什么要使用组件?
传统方式(单体架构)
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
快速入门
1. 安装组件
# Official components from npm
npm install @convex-dev/ratelimiter
2. 在 convex.config.ts 中配置
import { defineApp } from "convex/server";
import ratelimiter from "@convex-dev/ratelimiter/convex.config";
export default defineApp({
components: {
ratelimiter,
},
});
3. 在代码中使用
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);
},
});
同级组件模式
多个组件在同一层级协同工作:
// 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;
},
});
实现效果:
- ✅ 每个组件专注于一项职责
- ✅ 组件可在不同功能中重复使用
- ✅ 易于替换实现方式(更换邮件服务提供商)
- ✅ 可独立更新组件
- ✅ 职责分离清晰
官方组件
浏览组件目录:
身份验证
- @convex-dev/better-auth - Better Auth 集成
存储
- @convex-dev/r2 - Cloudflare R2 文件存储
- @convex-dev/storage - 文件上传/下载
支付
- @convex-dev/polar - Polar 计费与订阅
AI
- @convex-dev/agent - AI Agent 工作流
- @convex-dev/embeddings - 向量存储与搜索
后端实用工具
- @convex-dev/ratelimiter - 速率限制
- @convex-dev/aggregate - 数据聚合
- @convex-dev/action-cache - 缓存操作结果
- @convex-dev/sharded-counter - 分布式计数器
- @convex-dev/migrations - 架构迁移
- @convex-dev/workflow - 工作流编排
创建自己的组件
何时创建组件
适合的情况:
- 功能自成一体
- 你会在多个项目中复用它
- 想与团队或社区共享
- 功能复杂,且有自己的数据模型
- 封装第三方集成
不适合的情况:
- 一次性的业务逻辑
- 与主应用紧密耦合
- 简单的实用函数
结构
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;
},
});
使用自己的组件
// convex.config.ts
import { defineApp } from "convex/server";
import notifications from "./components/notifications/convex.config";
export default defineApp({
components: {
notifications, // Your local component
},
});
// convex/tasks.ts - main app code
import { components } from "./_generated/api";
export const completeTask = mutation({
args: { taskId: v.id("tasks") },
handler: async (ctx, args) => {
const task = await ctx.db.get(args.taskId);
await ctx.db.patch(args.taskId, { completed: true });
// Use your component
await components.notifications.send(ctx, {
userId: task.userId,
message: `Task "${task.title}" completed!`,
});
},
});
组件通信模式
✅ 父级 → 组件(推荐)
// 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
❌ 同级组件 → 同级组件(不推荐)
组件无法直接相互调用。如果有此需求,应将它们放在主应用中,或重构设计。
实际示例
多租户 SaaS
// convex.config.ts
export default defineApp({
components: {
auth: "@convex-dev/better-auth",
organizations: "./components/organizations",
billing: "./components/billing",
storage: "@convex-dev/r2",
analytics: "./components/analytics",
emails: "./components/emails",
},
});
每个组件:
auth- 用户身份验证与会话organizations- 租户隔离与权限billing- Stripe 集成与订阅storage- 将文件上传到 R2analytics- 事件跟踪与指标emails- 通过 SendGrid 发送电子邮件
电商平台
export default defineApp({
components: {
cart: "./components/cart",
inventory: "./components/inventory",
orders: "./components/orders",
payments: "@convex-dev/polar",
shipping: "./components/shipping",
recommendations: "./components/recommendations",
},
});
AI 应用
export default defineApp({
components: {
agent: "@convex-dev/agent",
embeddings: "./components/embeddings",
documents: "./components/documents",
chat: "./components/chat",
workflow: "@convex-dev/workflow",
},
});
从单体架构迁移
第 1 步:识别功能
Current monolith:
- File uploads (mixed with main app)
- Rate limiting (scattered everywhere)
- Analytics (embedded in functions)
第 2 步:提取一项功能
# Create component
mkdir -p convex/components/storage
# Move storage code to component
# Update imports in main app
第 3 步:独立测试
# Component has its own tests
# No coupling to main app
第 4 步:重复 逐步提取其他功能。
最佳实践
1. 单一职责
每个组件都应做好一件事:
- ✅ storage 组件负责处理文件
- ✅ auth 组件负责身份验证
- ❌ 不要创建包含所有内容的“utils”组件
2. 明确的 API 范围
// Export only what's needed
export { upload, download, delete } from "./storage";
// Keep internals private
// (Don't export helper functions)
3. 尽量减少耦合
// ✅ 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)
4. 为组件进行版本管理
{
"name": "@yourteam/notifications-component",
"version": "1.0.0"
}
5. 编写组件文档
在 README 中说明:
- 组件的功能
- 安装方法
- 使用方法
- API 参考
- 示例
故障排查
找不到组件
# Make sure component is in convex.config.ts
# Run: npx convex dev
无法访问父级表
This is by design! Components are sandboxed.
Pass data as arguments instead.
组件冲突
Each component has isolated tables.
Components can't see each other's data.
了解更多
检查清单
- 浏览组件目录,查找现有解决方案
- 通过 npm 安装组件:
npm install @convex-dev/component-name - 在
convex.config.ts中进行配置 - 使用同级组件封装功能
- 为可复用功能创建自己的组件
- 让组件专注于单一职责
- 单独测试组件
- 编写组件 API 文档
- 为组件进行适当的版本管理
**请记住:**组件旨在实现封装和复用。如有疑问,优先使用组件,而不是编写庞大的单体代码!
评论
支持 Markdown