Convex组件创作Skill convex-component-authoring

这个技能教授如何使用Convex框架创建、结构化和发布自包含的组件,包括数据库表隔离、查询和变异函数定义、TypeScript类型导出以及前端钩子,适用于跨项目共享和构建模块化的后端应用。关键词:Convex, 组件开发, 数据库管理, TypeScript, 可重用性, 后端框架。

后端开发 0 次安装 0 次浏览 更新于 3/17/2026

名称:convex-component-authoring 显示名称:Convex组件创作 描述:如何创建、结构化和发布具有适当隔离、导出和依赖管理的自包含Convex组件 版本:1.0.0 作者:Convex 标签:[convex, components, reusable, packages, npm]

Convex组件创作

创建自包含、可重用的Convex组件,具有适当的隔离、导出和依赖管理,以便在项目间共享。

文档来源

在实施前,请勿假设;获取最新文档:

说明

什么是Convex组件?

Convex组件是自包含的包,包括:

  • 数据库表(与主应用隔离)
  • 函数(查询、变异、动作)
  • TypeScript类型和验证器
  • 可选的前端钩子

组件结构

my-convex-component/
├── package.json
├── tsconfig.json
├── README.md
├── src/
│   ├── index.ts           # 主导出
│   ├── component.ts       # 组件定义
│   ├── schema.ts          # 组件模式
│   └── functions/
│       ├── queries.ts
│       ├── mutations.ts
│       └── actions.ts
└── convex.config.ts       # 组件配置

创建组件

1. 组件配置

// convex.config.ts
import { defineComponent } from "convex/server";

export default defineComponent("myComponent");

2. 组件模式

// src/schema.ts
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";

export default defineSchema({
  // 表隔离到此组件
  items: defineTable({
    name: v.string(),
    data: v.any(),
    createdAt: v.number(),
  }).index("by_name", ["name"]),
  
  config: defineTable({
    key: v.string(),
    value: v.any(),
  }).index("by_key", ["key"]),
});

3. 组件定义

// src/component.ts
import { defineComponent, ComponentDefinition } from "convex/server";
import schema from "./schema";
import * as queries from "./functions/queries";
import * as mutations from "./functions/mutations";

const component = defineComponent("myComponent", {
  schema,
  functions: {
    ...queries,
    ...mutations,
  },
});

export default component;

4. 组件函数

// src/functions/queries.ts
import { query } from "../_generated/server";
import { v } from "convex/values";

export const list = query({
  args: {
    limit: v.optional(v.number()),
  },
  returns: v.array(v.object({
    _id: v.id("items"),
    name: v.string(),
    data: v.any(),
    createdAt: v.number(),
  })),
  handler: async (ctx, args) => {
    return await ctx.db
      .query("items")
      .order("desc")
      .take(args.limit ?? 10);
  },
});

export const get = query({
  args: { name: v.string() },
  returns: v.union(v.object({
    _id: v.id("items"),
    name: v.string(),
    data: v.any(),
  }), v.null()),
  handler: async (ctx, args) => {
    return await ctx.db
      .query("items")
      .withIndex("by_name", (q) => q.eq("name", args.name))
      .unique();
  },
});
// src/functions/mutations.ts
import { mutation } from "../_generated/server";
import { v } from "convex/values";

export const create = mutation({
  args: {
    name: v.string(),
    data: v.any(),
  },
  returns: v.id("items"),
  handler: async (ctx, args) => {
    return await ctx.db.insert("items", {
      name: args.name,
      data: args.data,
      createdAt: Date.now(),
    });
  },
});

export const update = mutation({
  args: {
    id: v.id("items"),
    data: v.any(),
  },
  returns: v.null(),
  handler: async (ctx, args) => {
    await ctx.db.patch(args.id, { data: args.data });
    return null;
  },
});

export const remove = mutation({
  args: { id: v.id("items") },
  returns: v.null(),
  handler: async (ctx, args) => {
    await ctx.db.delete(args.id);
    return null;
  },
});

5. 主导出

// src/index.ts
export { default as component } from "./component";
export * from "./functions/queries";
export * from "./functions/mutations";

// 为消费者导出类型
export type { Id } from "./_generated/dataModel";

使用组件

// 在消费应用的 convex/convex.config.ts
import { defineApp } from "convex/server";
import myComponent from "my-convex-component";

const app = defineApp();

app.use(myComponent, { name: "myComponent" });

export default app;
// 在消费应用的代码中
import { useQuery, useMutation } from "convex/react";
import { api } from "../convex/_generated/api";

function MyApp() {
  // 通过应用的API访问组件函数
  const items = useQuery(api.myComponent.list, { limit: 10 });
  const createItem = useMutation(api.myComponent.create);
  
  return (
    <div>
      {items?.map((item) => (
        <div key={item._id}>{item.name}</div>
      ))}
      <button onClick={() => createItem({ name: "New", data: {} })}>
        添加项目
      </button>
    </div>
  );
}

组件配置选项

// convex/convex.config.ts
import { defineApp } from "convex/server";
import myComponent from "my-convex-component";

const app = defineApp();

// 基本使用
app.use(myComponent);

// 使用自定义名称
app.use(myComponent, { name: "customName" });

// 多个实例
app.use(myComponent, { name: "instance1" });
app.use(myComponent, { name: "instance2" });

export default app;

提供组件钩子

// src/hooks.ts
import { useQuery, useMutation } from "convex/react";
import { FunctionReference } from "convex/server";

// 为组件消费者提供的类型安全钩子
export function useMyComponent(api: {
  list: FunctionReference<"query">;
  create: FunctionReference<"mutation">;
}) {
  const items = useQuery(api.list, {});
  const createItem = useMutation(api.create);
  
  return {
    items,
    createItem,
    isLoading: items === undefined,
  };
}

发布组件

package.json

{
  "name": "my-convex-component",
  "version": "1.0.0",
  "description": "A reusable Convex component",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "files": [
    "dist",
    "convex.config.ts"
  ],
  "scripts": {
    "build": "tsc",
    "prepublishOnly": "npm run build"
  },
  "peerDependencies": {
    "convex": "^1.0.0"
  },
  "devDependencies": {
    "convex": "^1.17.0",
    "typescript": "^5.0.0"
  },
  "keywords": [
    "convex",
    "component"
  ]
}

tsconfig.json

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "declaration": true,
    "outDir": "dist",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

示例

限流器组件

// rate-limiter/src/schema.ts
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";

export default defineSchema({
  requests: defineTable({
    key: v.string(),
    timestamp: v.number(),
  })
    .index("by_key", ["key"])
    .index("by_key_and_time", ["key", "timestamp"]),
});
// rate-limiter/src/functions/mutations.ts
import { mutation } from "../_generated/server";
import { v } from "convex/values";

export const checkLimit = mutation({
  args: {
    key: v.string(),
    limit: v.number(),
    windowMs: v.number(),
  },
  returns: v.object({
    allowed: v.boolean(),
    remaining: v.number(),
    resetAt: v.number(),
  }),
  handler: async (ctx, args) => {
    const now = Date.now();
    const windowStart = now - args.windowMs;
    
    // 清理旧条目
    const oldEntries = await ctx.db
      .query("requests")
      .withIndex("by_key_and_time", (q) => 
        q.eq("key", args.key).lt("timestamp", windowStart)
      )
      .collect();
    
    for (const entry of oldEntries) {
      await ctx.db.delete(entry._id);
    }
    
    // 计算当前窗口
    const currentRequests = await ctx.db
      .query("requests")
      .withIndex("by_key", (q) => q.eq("key", args.key))
      .collect();
    
    const remaining = Math.max(0, args.limit - currentRequests.length);
    const allowed = remaining > 0;
    
    if (allowed) {
      await ctx.db.insert("requests", {
        key: args.key,
        timestamp: now,
      });
    }
    
    const oldestRequest = currentRequests[0];
    const resetAt = oldestRequest 
      ? oldestRequest.timestamp + args.windowMs 
      : now + args.windowMs;
    
    return { allowed, remaining: remaining - (allowed ? 1 : 0), resetAt };
  },
});
// 在消费应用中使用
import { useMutation } from "convex/react";
import { api } from "../convex/_generated/api";

function useRateLimitedAction() {
  const checkLimit = useMutation(api.rateLimiter.checkLimit);
  
  return async (action: () => Promise<void>) => {
    const result = await checkLimit({
      key: "user-action",
      limit: 10,
      windowMs: 60000,
    });
    
    if (!result.allowed) {
      throw new Error(`Rate limited. Try again at ${new Date(result.resetAt)}`);
    }
    
    await action();
  };
}

最佳实践

  • 除非明确指示,否则永远不要运行 npx convex deploy
  • 除非明确指示,否则永远不要运行任何 git 命令
  • 保持组件表隔离(不要引用主应用表)
  • 为消费者导出清晰的 TypeScript 类型
  • 记录所有公共函数及其参数
  • 为组件发布使用语义化版本控制
  • 包含带有示例的全面 README
  • 在发布前独立测试组件

常见陷阱

  1. 跨表引用 - 组件表应自包含
  2. 缺少类型导出 - 导出所有必要类型
  3. 硬编码配置 - 使用组件选项进行自定义
  4. 没有版本控制 - 遵循语义化版本控制
  5. 文档不足 - 记录所有公共 API

参考资料