convex-components
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseAuthoring Convex Components
Convex组件编写指南
Overview
概述
Convex components are isolated, reusable backend modules that can be packaged and shared. Each component has its own schema, functions, and namespace, enabling modular architecture and library distribution via NPM.
Convex组件是可打包共享的独立、可复用后端模块。每个组件都有自己的Schema、函数和命名空间,支持模块化架构,并可通过NPM进行库分发。
TypeScript: NEVER Use any
Type
anyTypeScript:禁止使用any
类型
anyCRITICAL RULE: This codebase has enabled. Using will cause build failures.
@typescript-eslint/no-explicit-anyany关键规则: 此代码库启用了规则。使用会导致构建失败。
@typescript-eslint/no-explicit-anyanyWhen to Use This Skill
适用场景
Use this skill when:
- Building reusable backend modules for your app
- Packaging Convex functionality for NPM distribution
- Creating isolated sub-systems with separate schemas
- Integrating third-party Convex components
- Building libraries with Convex backends (rate limiters, workpools, etc.)
在以下场景中使用本指南:
- 为你的应用构建可复用后端模块
- 为NPM分发打包Convex功能
- 创建带有独立Schema的隔离子系统
- 集成第三方Convex组件
- 构建基于Convex后端的库(如速率限制器、工作池等)
Component Types
组件类型
| Type | Use Case | Location |
|---|---|---|
| Local | Private modules within your app | |
| Packaged | Published NPM packages | |
| Hybrid | Shared internal packages | |
| 类型 | 适用场景 | 位置 |
|---|---|---|
| 本地组件 | 应用内部的私有模块 | |
| 打包组件 | 发布为NPM包的组件 | |
| 混合组件 | 内部共享的包 | |
Component Anatomy
组件结构
Directory Structure
目录结构
my-component/
├── package.json # NPM package config
├── src/
│ └── component/
│ ├── convex.config.ts # Component definition
│ ├── schema.ts # Component schema
│ ├── public.ts # Public API functions
│ ├── internal.ts # Internal functions
│ └── _generated/ # Generated types (gitignored)
└── src/
└── client/
└── index.ts # Client-side wrapper classmy-component/
├── package.json # NPM包配置
├── src/
│ └── component/
│ ├── convex.config.ts # 组件定义文件
│ ├── schema.ts # 组件Schema
│ ├── public.ts # 公共API函数
│ ├── internal.ts # 内部函数
│ └── _generated/ # 生成的类型文件(已加入git忽略)
└── src/
└── client/
└── index.ts # 客户端封装类convex.config.ts
convex.config.ts
The component definition file:
typescript
// src/component/convex.config.ts
import { defineComponent } from "convex/server";
export default defineComponent("myComponent");组件定义文件示例:
typescript
// src/component/convex.config.ts
import { defineComponent } from "convex/server";
export default defineComponent("myComponent");Component Schema
组件Schema
typescript
// src/component/schema.ts
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
export default defineSchema({
items: defineTable({
key: v.string(),
value: v.string(),
expiresAt: v.optional(v.number()),
}).index("by_key", ["key"]),
});typescript
// src/component/schema.ts
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
export default defineSchema({
items: defineTable({
key: v.string(),
value: v.string(),
expiresAt: v.optional(v.number()),
}).index("by_key", ["key"]),
});Public API Functions
公共API函数
typescript
// src/component/public.ts
import { mutation, query } from "./_generated/server";
import { v } from "convex/values";
export const set = mutation({
args: {
key: v.string(),
value: v.string(),
ttl: v.optional(v.number()),
},
returns: v.null(),
handler: async (ctx, args) => {
const existing = await ctx.db
.query("items")
.withIndex("by_key", (q) => q.eq("key", args.key))
.unique();
const data = {
key: args.key,
value: args.value,
expiresAt: args.ttl ? Date.now() + args.ttl : undefined,
};
if (existing) {
await ctx.db.replace(existing._id, data);
} else {
await ctx.db.insert("items", data);
}
return null;
},
});
export const get = query({
args: { key: v.string() },
returns: v.union(v.string(), v.null()),
handler: async (ctx, args) => {
const item = await ctx.db
.query("items")
.withIndex("by_key", (q) => q.eq("key", args.key))
.unique();
if (!item) return null;
if (item.expiresAt && item.expiresAt < Date.now()) return null;
return item.value;
},
});typescript
// src/component/public.ts
import { mutation, query } from "./_generated/server";
import { v } from "convex/values";
export const set = mutation({
args: {
key: v.string(),
value: v.string(),
ttl: v.optional(v.number()),
},
returns: v.null(),
handler: async (ctx, args) => {
const existing = await ctx.db
.query("items")
.withIndex("by_key", (q) => q.eq("key", args.key))
.unique();
const data = {
key: args.key,
value: args.value,
expiresAt: args.ttl ? Date.now() + args.ttl : undefined,
};
if (existing) {
await ctx.db.replace(existing._id, data);
} else {
await ctx.db.insert("items", data);
}
return null;
},
});
export const get = query({
args: { key: v.string() },
returns: v.union(v.string(), v.null()),
handler: async (ctx, args) => {
const item = await ctx.db
.query("items")
.withIndex("by_key", (q) => q.eq("key", args.key))
.unique();
if (!item) return null;
if (item.expiresAt && item.expiresAt < Date.now()) return null;
return item.value;
},
});Installing Components in Host App
在宿主应用中安装组件
Host App convex.config.ts
宿主应用的convex.config.ts
typescript
// convex/convex.config.ts
import { defineApp } from "convex/server";
import myComponent from "@org/my-component/convex.config";
const app = defineApp();
app.use(myComponent, { name: "cache" }); // Mount with a name
export default app;typescript
// convex/convex.config.ts
import { defineApp } from "convex/server";
import myComponent from "@org/my-component/convex.config";
const app = defineApp();
app.use(myComponent, { name: "cache" }); // 为组件指定挂载名称
export default app;Accessing Component Functions
访问组件函数
typescript
// convex/myFunctions.ts
import { mutation } from "./_generated/server";
import { components } from "./_generated/api";
import { v } from "convex/values";
export const cacheValue = mutation({
args: { key: v.string(), value: v.string() },
returns: v.null(),
handler: async (ctx, args) => {
// Call component function via components.<name>.<module>.<function>
await ctx.runMutation(components.cache.public.set, {
key: args.key,
value: args.value,
ttl: 3600000, // 1 hour
});
return null;
},
});typescript
// convex/myFunctions.ts
import { mutation } from "./_generated/server";
import { components } from "./_generated/api";
import { v } from "convex/values";
export const cacheValue = mutation({
args: { key: v.string(), value: v.string() },
returns: v.null(),
handler: async (ctx, args) => {
// 通过components.<挂载名>.<模块>.<函数>调用组件函数
await ctx.runMutation(components.cache.public.set, {
key: args.key,
value: args.value,
ttl: 3600000, // 1小时
});
return null;
},
});Key Differences from Regular Convex
与常规Convex的主要区别
1. Id Types Are Opaque
1. ID类型为不透明类型
Component IDs are branded strings, not :
Id<"table">typescript
// ❌ WRONG: Can't use Id<"items"> from component
import { Id } from "./_generated/dataModel";
const id: Id<"items"> = ...; // Error!
// ✅ CORRECT: Use string or branded type from component
export const processItem = mutation({
args: { itemId: v.string() }, // Accept as string
returns: v.null(),
handler: async (ctx, args) => {
// Pass to component functions as-is
await ctx.runMutation(components.myComponent.public.process, {
itemId: args.itemId,
});
return null;
},
});组件的ID是带标识的字符串,而非类型:
Id<"table">typescript
// ❌ 错误:无法使用组件的Id<"items">类型
import { Id } from "./_generated/dataModel";
const id: Id<"items"> = ...; // 报错!
// ✅ 正确:使用字符串或组件提供的带标识类型
export const processItem = mutation({
args: { itemId: v.string() }, // 以字符串形式接收
returns: v.null(),
handler: async (ctx, args) => {
// 直接传递给组件函数
await ctx.runMutation(components.myComponent.public.process, {
itemId: args.itemId,
});
return null;
},
});2. No Environment Variables
2. 无法访问环境变量
Components cannot access . Pass configuration explicitly:
process.envtypescript
// ✅ CORRECT: Component accepts config via function args
// src/component/public.ts
export const initialize = mutation({
args: {
apiKey: v.string(),
endpoint: v.string(),
},
returns: v.null(),
handler: async (ctx, args) => {
await ctx.db.insert("config", {
apiKey: args.apiKey,
endpoint: args.endpoint,
});
return null;
},
});
// Host app passes env vars
export const setup = mutation({
args: {},
returns: v.null(),
handler: async (ctx) => {
await ctx.runMutation(components.myComponent.public.initialize, {
apiKey: process.env.API_KEY!,
endpoint: process.env.API_ENDPOINT!,
});
return null;
},
});组件无法访问,需显式传递配置:
process.envtypescript
// ✅ 正确:组件通过函数参数接收配置
// src/component/public.ts
export const initialize = mutation({
args: {
apiKey: v.string(),
endpoint: v.string(),
},
returns: v.null(),
handler: async (ctx, args) => {
await ctx.db.insert("config", {
apiKey: args.apiKey,
endpoint: args.endpoint,
});
return null;
},
});
// 宿主应用传递环境变量
export const setup = mutation({
args: {},
returns: v.null(),
handler: async (ctx) => {
await ctx.runMutation(components.myComponent.public.initialize, {
apiKey: process.env.API_KEY!,
endpoint: process.env.API_ENDPOINT!,
});
return null;
},
});3. No ctx.auth
3. 无ctx.auth上下文
Components don't have access to authentication context. Pass user info explicitly:
typescript
// ❌ WRONG: Components can't access ctx.auth
export const createItem = mutation({
args: { data: v.string() },
returns: v.null(),
handler: async (ctx, args) => {
const identity = await ctx.auth.getUserIdentity(); // Error!
return null;
},
});
// ✅ CORRECT: Accept userId as argument
export const createItem = mutation({
args: {
userId: v.string(),
data: v.string(),
},
returns: v.id("items"),
handler: async (ctx, args) => {
return await ctx.db.insert("items", {
userId: args.userId,
data: args.data,
});
},
});
// Host app handles auth and passes user info
export const createItemWithAuth = mutation({
args: { data: v.string() },
returns: v.string(),
handler: async (ctx, args) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) throw new Error("Unauthorized");
const itemId = await ctx.runMutation(
components.myComponent.public.createItem,
{
userId: identity.subject,
data: args.data,
}
);
return itemId; // Return as string
},
});组件无法访问认证上下文,需显式传递用户信息:
typescript
// ❌ 错误:组件无法访问ctx.auth
export const createItem = mutation({
args: { data: v.string() },
returns: v.null(),
handler: async (ctx, args) => {
const identity = await ctx.auth.getUserIdentity(); // 报错!
return null;
},
});
// ✅ 正确:接收userId作为参数
export const createItem = mutation({
args: {
userId: v.string(),
data: v.string(),
},
returns: v.id("items"),
handler: async (ctx, args) => {
return await ctx.db.insert("items", {
userId: args.userId,
data: args.data,
});
},
});
// 宿主应用处理认证并传递用户信息
export const createItemWithAuth = mutation({
args: { data: v.string() },
returns: v.string(),
handler: async (ctx, args) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) throw new Error("未授权");
const itemId = await ctx.runMutation(
components.myComponent.public.createItem,
{
userId: identity.subject,
data: args.data,
}
);
return itemId; // 以字符串形式返回
},
});4. Pagination Cursors Are Strings
4. 分页游标为字符串类型
Component pagination uses string cursors:
typescript
// src/component/public.ts
export const list = query({
args: {
cursor: v.optional(v.string()),
limit: v.number(),
},
returns: v.object({
items: v.array(
v.object({
id: v.string(),
data: v.string(),
})
),
nextCursor: v.union(v.string(), v.null()),
}),
handler: async (ctx, args) => {
const results = await ctx.db
.query("items")
.paginate({ cursor: args.cursor ?? null, numItems: args.limit });
return {
items: results.page.map((item) => ({
id: item._id,
data: item.data,
})),
nextCursor: results.continueCursor,
};
},
});组件分页使用字符串游标:
typescript
// src/component/public.ts
export const list = query({
args: {
cursor: v.optional(v.string()),
limit: v.number(),
},
returns: v.object({
items: v.array(
v.object({
id: v.string(),
data: v.string(),
})
),
nextCursor: v.union(v.string(), v.null()),
}),
handler: async (ctx, args) => {
const results = await ctx.db
.query("items")
.paginate({ cursor: args.cursor ?? null, numItems: args.limit });
return {
items: results.page.map((item) => ({
id: item._id,
data: item.data,
})),
nextCursor: results.continueCursor,
};
},
});Client Code Patterns
客户端代码模式
Class-Based Client Wrapper
基于类的客户端封装
Provide a convenient client class for host apps:
typescript
// src/client/index.ts
import {
FunctionReference,
GenericMutationCtx,
GenericQueryCtx,
} from "convex/server";
import { api } from "../component/_generated/api";
// Re-export the component config
export { default } from "../component/convex.config";
// Type for the installed component
type ComponentApi = typeof api;
export class MyComponentClient {
private component: ComponentApi;
constructor(component: ComponentApi) {
this.component = component;
}
// Wrapper methods for cleaner API
async set(
ctx: GenericMutationCtx<Record<string, never>>,
key: string,
value: string,
ttl?: number
): Promise<void> {
await ctx.runMutation(this.component.public.set, { key, value, ttl });
}
async get(
ctx: GenericQueryCtx<Record<string, never>>,
key: string
): Promise<string | null> {
return await ctx.runQuery(this.component.public.get, { key });
}
}为宿主应用提供便捷的客户端类:
typescript
// src/client/index.ts
import {
FunctionReference,
GenericMutationCtx,
GenericQueryCtx,
} from "convex/server";
import { api } from "../component/_generated/api";
// 重新导出组件配置
export { default } from "../component/convex.config";
// 已安装组件的类型
type ComponentApi = typeof api;
export class MyComponentClient {
private component: ComponentApi;
constructor(component: ComponentApi) {
this.component = component;
}
// 封装方法以简化API调用
async set(
ctx: GenericMutationCtx<Record<string, never>>,
key: string,
value: string,
ttl?: number
): Promise<void> {
await ctx.runMutation(this.component.public.set, { key, value, ttl });
}
async get(
ctx: GenericQueryCtx<Record<string, never>>,
key: string
): Promise<string | null> {
return await ctx.runQuery(this.component.public.get, { key });
}
}Host App Usage
宿主应用使用示例
typescript
// convex/cache.ts
import { MyComponentClient } from "@org/my-component";
import { components } from "./_generated/api";
import { mutation, query } from "./_generated/server";
import { v } from "convex/values";
const cache = new MyComponentClient(components.cache);
export const setValue = mutation({
args: { key: v.string(), value: v.string() },
returns: v.null(),
handler: async (ctx, args) => {
await cache.set(ctx, args.key, args.value, 3600000);
return null;
},
});
export const getValue = query({
args: { key: v.string() },
returns: v.union(v.string(), v.null()),
handler: async (ctx, args) => {
return await cache.get(ctx, args.key);
},
});typescript
// convex/cache.ts
import { MyComponentClient } from "@org/my-component";
import { components } from "./_generated/api";
import { mutation, query } from "./_generated/server";
import { v } from "convex/values";
const cache = new MyComponentClient(components.cache);
export const setValue = mutation({
args: { key: v.string(), value: v.string() },
returns: v.null(),
handler: async (ctx, args) => {
await cache.set(ctx, args.key, args.value, 3600000);
return null;
},
});
export const getValue = query({
args: { key: v.string() },
returns: v.union(v.string(), v.null()),
handler: async (ctx, args) => {
return await cache.get(ctx, args.key);
},
});Building & Publishing
构建与发布
package.json Setup
package.json配置
json
{
"name": "@org/my-component",
"version": "1.0.0",
"type": "module",
"main": "./dist/client/index.js",
"types": "./dist/client/index.d.ts",
"exports": {
".": {
"types": "./dist/client/index.d.ts",
"import": "./dist/client/index.js"
},
"./convex.config": {
"types": "./dist/component/convex.config.d.ts",
"import": "./dist/component/convex.config.js"
}
},
"files": ["dist", "src"],
"scripts": {
"build": "npm run build:esm && npm run build:cjs",
"build:esm": "tsc --project tsconfig.json",
"typecheck": "tsc --noEmit",
"prepare": "npm run build"
},
"dependencies": {
"convex": "^1.17.0"
},
"devDependencies": {
"typescript": "^5.0.0"
},
"peerDependencies": {
"convex": "^1.17.0"
}
}json
{
"name": "@org/my-component",
"version": "1.0.0",
"type": "module",
"main": "./dist/client/index.js",
"types": "./dist/client/index.d.ts",
"exports": {
".": {
"types": "./dist/client/index.d.ts",
"import": "./dist/client/index.js"
},
"./convex.config": {
"types": "./dist/component/convex.config.d.ts",
"import": "./dist/component/convex.config.js"
}
},
"files": ["dist", "src"],
"scripts": {
"build": "npm run build:esm && npm run build:cjs",
"build:esm": "tsc --project tsconfig.json",
"typecheck": "tsc --noEmit",
"prepare": "npm run build"
},
"dependencies": {
"convex": "^1.17.0"
},
"devDependencies": {
"typescript": "^5.0.0"
},
"peerDependencies": {
"convex": "^1.17.0"
}
}tsconfig.json
tsconfig.json
json
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}json
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}Generate Types Before Publishing
发布前生成类型文件
bash
undefinedbash
undefinedIn component directory
进入组件目录
cd src/component
npx convex codegen
cd src/component
npx convex codegen
Build the package
构建包
cd ../..
npm run build
cd ../..
npm run build
Publish
发布
npm publish
undefinednpm publish
undefinedTesting Components
组件测试
Using convex-test
使用convex-test
typescript
// tests/component.test.ts
import { convexTest } from "convex-test";
import { describe, expect, it } from "vitest";
import schema from "../src/component/schema";
import { api } from "../src/component/_generated/api";
describe("MyComponent", () => {
it("should set and get values", async () => {
const t = convexTest(schema);
// Set a value
await t.mutation(api.public.set, {
key: "test-key",
value: "test-value",
});
// Get the value
const result = await t.query(api.public.get, {
key: "test-key",
});
expect(result).toBe("test-value");
});
it("should respect TTL expiration", async () => {
const t = convexTest(schema);
// Set with TTL of 0 (immediate expiry)
await t.mutation(api.public.set, {
key: "expiring-key",
value: "expiring-value",
ttl: -1000, // Already expired
});
// Should return null for expired item
const result = await t.query(api.public.get, {
key: "expiring-key",
});
expect(result).toBeNull();
});
});typescript
// tests/component.test.ts
import { convexTest } from "convex-test";
import { describe, expect, it } from "vitest";
import schema from "../src/component/schema";
import { api } from "../src/component/_generated/api";
describe("MyComponent", () => {
it("应该能设置并获取值", async () => {
const t = convexTest(schema);
// 设置值
await t.mutation(api.public.set, {
key: "test-key",
value: "test-value",
});
// 获取值
const result = await t.query(api.public.get, {
key: "test-key",
});
expect(result).toBe("test-value");
});
it("应该遵守TTL过期规则", async () => {
const t = convexTest(schema);
// 设置TTL为0(立即过期)
await t.mutation(api.public.set, {
key: "expiring-key",
value: "expiring-value",
ttl: -1000, // 已过期
});
// 过期项应返回null
const result = await t.query(api.public.get, {
key: "expiring-key",
});
expect(result).toBeNull();
});
});Testing with Mock Data
使用模拟数据测试
typescript
// tests/with-data.test.ts
import { convexTest } from "convex-test";
import { describe, expect, it } from "vitest";
import schema from "../src/component/schema";
import { api } from "../src/component/_generated/api";
describe("MyComponent with data", () => {
it("should handle existing data", async () => {
const t = convexTest(schema);
// Seed data directly
await t.run(async (ctx) => {
await ctx.db.insert("items", {
key: "seeded-key",
value: "seeded-value",
});
});
// Query should find seeded data
const result = await t.query(api.public.get, {
key: "seeded-key",
});
expect(result).toBe("seeded-value");
});
});typescript
// tests/with-data.test.ts
import { convexTest } from "convex-test";
import { describe, expect, it } from "vitest";
import schema from "../src/component/schema";
import { api } from "../src/component/_generated/api";
describe("带数据的MyComponent", () => {
it("应该能处理已有数据", async () => {
const t = convexTest(schema);
// 直接植入测试数据
await t.run(async (ctx) => {
await ctx.db.insert("items", {
key: "seeded-key",
value: "seeded-value",
});
});
// 查询应能找到植入的数据
const result = await t.query(api.public.get, {
key: "seeded-key",
});
expect(result).toBe("seeded-value");
});
});Local Component Pattern
本地组件模式
For app-internal modularity without NPM publishing:
convex/
├── convex.config.ts # Main app config
├── schema.ts # Main app schema
├── myLocalComponent/ # Local component
│ ├── convex.config.ts # Component definition
│ ├── schema.ts # Component schema
│ └── functions.ts # Component functions
└── functions.ts # Main app functionstypescript
// convex/convex.config.ts
import { defineApp } from "convex/server";
import myLocalComponent from "./myLocalComponent/convex.config";
const app = defineApp();
app.use(myLocalComponent, { name: "localCache" });
export default app;无需发布到NPM,仅用于应用内部模块化:
convex/
├── convex.config.ts # 主应用配置
├── schema.ts # 主应用Schema
├── myLocalComponent/ # 本地组件
│ ├── convex.config.ts # 组件定义
│ ├── schema.ts # 组件Schema
│ └── functions.ts # 组件函数
└── functions.ts # 主应用函数typescript
// convex/convex.config.ts
import { defineApp } from "convex/server";
import myLocalComponent from "./myLocalComponent/convex.config";
const app = defineApp();
app.use(myLocalComponent, { name: "localCache" });
export default app;Common Pitfalls
常见陷阱
Pitfall 1: Using Id Types from Component
陷阱1:使用组件的Id类型
❌ WRONG:
typescript
import { Id } from "./_generated/dataModel";
export const getItem = query({
args: { itemId: v.id("items") }, // ❌ Not accessible!
handler: async (ctx, args) => {
return await ctx.db.get(args.itemId);
},
});✅ CORRECT:
typescript
export const getItem = query({
args: { itemId: v.string() }, // ✅ Accept as string
returns: v.union(
v.object({
id: v.string(),
data: v.string(),
}),
v.null()
),
handler: async (ctx, args) => {
// Look up by index or iterate
const item = await ctx.db
.query("items")
.filter((q) => q.eq(q.field("_id"), args.itemId))
.first();
if (!item) return null;
return { id: item._id, data: item.data };
},
});❌ 错误:
typescript
import { Id } from "./_generated/dataModel";
export const getItem = query({
args: { itemId: v.id("items") }, // ❌ 无法访问!
handler: async (ctx, args) => {
return await ctx.db.get(args.itemId);
},
});✅ 正确:
typescript
export const getItem = query({
args: { itemId: v.string() }, // ✅ 以字符串形式接收
returns: v.union(
v.object({
id: v.string(),
data: v.string(),
}),
v.null()
),
handler: async (ctx, args) => {
// 通过索引查询或遍历查找
const item = await ctx.db
.query("items")
.filter((q) => q.eq(q.field("_id"), args.itemId))
.first();
if (!item) return null;
return { id: item._id, data: item.data };
},
});Pitfall 2: Accessing process.env in Component
陷阱2:在组件中访问process.env
❌ WRONG:
typescript
// src/component/functions.ts
export const callApi = action({
args: {},
returns: v.null(),
handler: async () => {
// ❌ Components can't access process.env!
const apiKey = process.env.API_KEY;
await fetch("...", { headers: { Authorization: apiKey } });
return null;
},
});✅ CORRECT:
typescript
// src/component/functions.ts
export const callApi = action({
args: { apiKey: v.string() }, // ✅ Accept as argument
returns: v.null(),
handler: async (ctx, args) => {
await fetch("...", { headers: { Authorization: args.apiKey } });
return null;
},
});
// Host app passes env var
await ctx.runAction(components.myComponent.functions.callApi, {
apiKey: process.env.API_KEY!,
});❌ 错误:
typescript
// src/component/functions.ts
export const callApi = action({
args: {},
returns: v.null(),
handler: async () => {
// ❌ 组件无法访问process.env!
const apiKey = process.env.API_KEY;
await fetch("...", { headers: { Authorization: apiKey } });
return null;
},
});✅ 正确:
typescript
// src/component/functions.ts
export const callApi = action({
args: { apiKey: v.string() }, // ✅ 通过参数接收
returns: v.null(),
handler: async (ctx, args) => {
await fetch("...", { headers: { Authorization: args.apiKey } });
return null;
},
});
// 宿主应用传递环境变量
await ctx.runAction(components.myComponent.functions.callApi, {
apiKey: process.env.API_KEY!,
});Pitfall 3: Expecting ctx.auth in Component
陷阱3:期望组件拥有ctx.auth
❌ WRONG:
typescript
// src/component/functions.ts
export const userAction = mutation({
args: { data: v.string() },
returns: v.null(),
handler: async (ctx, args) => {
// ❌ Components don't have ctx.auth!
const identity = await ctx.auth.getUserIdentity();
return null;
},
});✅ CORRECT:
typescript
// src/component/functions.ts
export const userAction = mutation({
args: {
userId: v.string(), // ✅ Accept user info as argument
data: v.string(),
},
returns: v.null(),
handler: async (ctx, args) => {
await ctx.db.insert("userActions", {
userId: args.userId,
data: args.data,
});
return null;
},
});
// Host app handles auth
export const doUserAction = mutation({
args: { data: v.string() },
returns: v.null(),
handler: async (ctx, args) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) throw new Error("Unauthorized");
await ctx.runMutation(components.myComponent.functions.userAction, {
userId: identity.subject,
data: args.data,
});
return null;
},
});❌ 错误:
typescript
// src/component/functions.ts
export const userAction = mutation({
args: { data: v.string() },
returns: v.null(),
handler: async (ctx, args) => {
// ❌ 组件没有ctx.auth!
const identity = await ctx.auth.getUserIdentity();
return null;
},
});✅ 正确:
typescript
// src/component/functions.ts
export const userAction = mutation({
args: {
userId: v.string(), // ✅ 通过参数接收用户信息
data: v.string(),
},
returns: v.null(),
handler: async (ctx, args) => {
await ctx.db.insert("userActions", {
userId: args.userId,
data: args.data,
});
return null;
},
});
// 宿主应用处理认证
export const doUserAction = mutation({
args: { data: v.string() },
returns: v.null(),
handler: async (ctx, args) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) throw new Error("未授权");
await ctx.runMutation(components.myComponent.functions.userAction, {
userId: identity.subject,
data: args.data,
});
return null;
},
});Quick Reference
快速参考
Component Definition
组件定义
typescript
// convex.config.ts
import { defineComponent } from "convex/server";
export default defineComponent("componentName");typescript
// convex.config.ts
import { defineComponent } from "convex/server";
export default defineComponent("componentName");Host App Installation
宿主应用安装
typescript
// convex/convex.config.ts
import { defineApp } from "convex/server";
import myComponent from "@org/my-component/convex.config";
const app = defineApp();
app.use(myComponent, { name: "mountName" });
export default app;typescript
// convex/convex.config.ts
import { defineApp } from "convex/server";
import myComponent from "@org/my-component/convex.config";
const app = defineApp();
app.use(myComponent, { name: "mountName" });
export default app;Calling Component Functions
调用组件函数
typescript
import { components } from "./_generated/api";
// In mutations/queries/actions
await ctx.runMutation(components.mountName.module.function, args);
await ctx.runQuery(components.mountName.module.function, args);
await ctx.runAction(components.mountName.module.function, args);typescript
import { components } from "./_generated/api";
// 在mutation/query/action中调用
await ctx.runMutation(components.mountName.module.function, args);
await ctx.runQuery(components.mountName.module.function, args);
await ctx.runAction(components.mountName.module.function, args);Component Limitations
组件限制
| Feature | Available in Components? |
|---|---|
| Yes (own schema only) |
| No |
| No |
| Yes |
| No (use strings) |
| Pagination cursors | String only |
| 功能特性 | 组件中是否可用? |
|---|---|
| 是(仅能访问自身Schema) |
| 否 |
| 否 |
| 是 |
| 否(使用字符串) |
| 分页游标 | 仅支持字符串类型 |