mcpField
C
components-guide

components-guide

github.com/get-convex开发117189 次安装1 周前更新
代码仓库

关于 components-guide

介绍如何用独立组件组织 Convex 后端的指南技能,面向希望在应用中实现功能复用、数据隔离和清晰 API 边界的开发者。

主要功能

  1. 01组件拥有独立架构与数据
  2. 02组件配置与集成
  3. 03主应用到组件的通信模式
  4. 04创建可复用组件的指导
  5. 05从单体结构逐步迁移

适用场景

  1. 01从大型后端拆出存储或计费功能
  2. 02组合多个同级组件实现应用功能
  3. 03创建带有独立架构的可复用组件
  4. 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 - 将文件上传到 R2
  • analytics - 事件跟踪与指标
  • 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