名称:convex-component-authoring 显示名称:Convex组件创作 描述:如何创建、结构化和发布具有适当隔离、导出和依赖管理的自包含Convex组件 版本:1.0.0 作者:Convex 标签:[convex, components, reusable, packages, npm]
Convex组件创作
创建自包含、可重用的Convex组件,具有适当的隔离、导出和依赖管理,以便在项目间共享。
文档来源
在实施前,请勿假设;获取最新文档:
- 主要:https://docs.convex.dev/components
- 组件创作:https://docs.convex.dev/components/authoring
- 更广泛背景:https://docs.convex.dev/llms.txt
说明
什么是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
- 在发布前独立测试组件
常见陷阱
- 跨表引用 - 组件表应自包含
- 缺少类型导出 - 导出所有必要类型
- 硬编码配置 - 使用组件选项进行自定义
- 没有版本控制 - 遵循语义化版本控制
- 文档不足 - 记录所有公共 API
参考资料
- Convex 文档:https://docs.convex.dev/
- Convex LLMs.txt:https://docs.convex.dev/llms.txt
- 组件:https://docs.convex.dev/components
- 组件创作:https://docs.convex.dev/components/authoring