mcpField
C
convex-component-authoring

convex-component-authoring

github.com/waynesutton开发4051.1k 次安装4 周前更新
代码仓库

关于 convex-component-authoring

convex-component-authoring 是构建独立 Convex 组件的技能,涵盖组件自有的架构、函数和类型化客户端封装。它说明组件边界,以及父应用如何注册和调用组件。

主要功能

  1. 01组件自有架构与函数
  2. 02类型化客户端封装
  3. 03父应用注册方式
  4. 04边界与 ID 处理规则
  5. 05发布指南

适用场景

  1. 01将可复用后端功能提取为包
  2. 02为 Convex 目录构建组件
  3. 03在多个应用间复用集成

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/waynesutton/builder-skills --skill convex-component-authoring

安装后在 Claude Code 中输入/convex-component-authoring确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

agents/
openai.yaml
assets/
small-logo.svg
references/
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,再接收选项,并使用 Pick ctx 类型
  • 应用侧封装函数负责处理身份验证,并以字符串形式传递 ID
  • 组件中没有 ctx.auth、process.env 或 .paginate()
  • 已通过安装该组件的示例应用进行测试

文档

评论

支持 Markdown