发布于: -/最后更新: -/7 分钟/

Convex - 全栈开发的新一代后端平台

摘要

Convex是一款开源的BaaS(Backend-as-a-Service)平台,专为现代Web应用开发者设计。它提供了一个高性能的实时数据库、文件存储、定时任务调度等核心服务。Convex的生态系统非常丰富,支持通过插件扩展功能,包括Cloudflare R2、Rate Limiter、AI Agent等。Convex的主要特点包括端到端类型安全、默认实时、强一致性等。开发者可以使用Convex开发高性能、实时的Web应用,并且可以通过自定义函数、组件等方式扩展Convex的功能。Convex还支持私有化部署,开发者可以使用Docker将Convex部署到自己的服务器上。

什么是 Convex?

Convex 是一款开源的 BaaS (Backend-as-a-Service) 平台,专为现代 Web 应用开发者设计。它不仅提供了一个高性能的实时数据库,还集成了文件存储、定时任务调度(Cron Jobs)等核心服务。

Convex 的生态系统非常丰富,支持通过插件扩展功能,包括:

  • Cloudflare R2:用于大文件存储。

  • Rate Limiter:提供应用级的速率限制功能。

  • AI Agent:支持构建智能体和集成 AI 工作流。

为什么选择 Convex?

  1. 端到端类型安全 (End-to-End Type Safety) 告别繁琐的原生 SQL 查询和复杂的 ORM 配置。Convex 的所有查询和变更函数均使用 TypeScript 编写,实现了从数据库 schema 到前端组件的完整类型推导,极大降低了运行时错误。

  2. 默认实时 (Real-time by Default) Convex 客户端通过 WebSocket 与服务端保持连接。这意味着数据流是实时的——当数据库中的数据发生变化时,客户端会自动收到更新并重新渲染,无需手动配置轮询或订阅逻辑。

  3. 强一致性 (ACID Transactions) Convex 的所有写入操作(Mutations)都是原子化的。它们在事务中执行,确保了数据的完整性和一致性,解决了分布式系统中常见的并发问题。

基础用法

关于 Convex 的从零开始配置和基础 CRUD 操作,请参考官方文档: Convex Overview | Convex Developer Hub

开发进阶技巧

服务端组件集成 (Server Components)

在 Next.js App Router 中,如果需要实现服务端渲染 (SSR) 以优化 SEO 或首屏加载速度,可以使用 fetchQuery

注意fetchQuery 获取的数据是静态的,不支持实时订阅。

TSX
import { fetchQuery } from "convex/nextjs";
import { api } from "@/convex/_generated/api";

export async function StaticTasks() {
  // 如果该 query 需要验证,则需要传入 token
  const tasks = await fetchQuery(
    api.tasks.list,
    { list: "default" },
    { token: "your-jwt-token" },
  );

  return (
    <div>
      {tasks.map((task) => (
        <div key={task._id}>{task.text}</div>
      ))}
    </div>
  );
}

HTTP 端点与 Webhooks

Convex 支持通过 HTTP Actions 暴露标准的 API 端点,这在处理第三方 Webhooks(如 Clerk 身份验证回调)时非常有用。

TSX
import { httpRouter } from "convex/server";
import { httpAction } from "./_generated/server";
import { internal } from "./_generated/api";
import type { WebhookEvent } from "@clerk/backend";
import { Webhook } from "svix";

const http = httpRouter();

// 设置一个 API endpoint
http.route({
  path: "/clerk-users-webhook",
  method: "POST",
  // 注意:httpAction 中不能直接操作数据库,需调用 runQuery 或 runMutation
  handler: httpAction(async (ctx, request) => {
    const event = await validateRequest(request);
    if (!event) {
      return new Response("Error occured", { status: 400 });
    }

    switch (event.type) {
      case "user.created":
      case "user.updated":
        await ctx.runMutation(internal.users.upsertFromClerk, {
          data: event.data,
        });
        break;
      case "user.deleted": {
        const clerkUserId = event.data.id!;
        await ctx.runMutation(internal.users.deleteFromClerk, { clerkUserId });
        break;
      }
      default:
        console.log("Ignored Clerk webhook event", event.type);
    }
    return new Response(null, { status: 200 });
  }),
});

async function validateRequest(req: Request): Promise<WebhookEvent | null> {
  const payloadString = await req.text();
  const svixHeaders = {
    "svix-id": req.headers.get("svix-id")!,
    "svix-timestamp": req.headers.get("svix-timestamp")!,
    "svix-signature": req.headers.get("svix-signature")!,
  };
  const wh = new Webhook(process.env.CLERK_WEBHOOK_SECRET!);
  try {
    return wh.verify(payloadString, svixHeaders) as unknown as WebhookEvent;
  } catch (error) {
    console.error("Error verifying webhook event", error);
    return null;
  }
}

export default http;

在 Mutation 中处理副作用

Convex 的 Mutation 必须是纯函数,不允许由外部副作用(如直接调用第三方 API)。解决方案是使用 ctx.scheduler 调度一个 Action 来处理副作用。

TSX
import { mutation } from "./_generated/server";
import { v } from "convex/values";
import { internal } from "./_generated/api";

export const mutationThatSchedulesAction = mutation({
  args: { text: v.string() },
  handler: async (ctx, { text }) => {
    // 1. 执行数据库写入
    const taskId = await ctx.db.insert("tasks", { text });

    // 2. 调度 Action (0 表示立即执行)
    await ctx.scheduler.runAfter(0, internal.myFunctions.actionThatCallsAPI, {
      taskId,
      text,
    });
  },
});

Schema 管理与最佳实践

修改表结构

Convex 不允许直接删除包含数据的字段。如果要删除字段,推荐流程如下:

  1. 将字段修改为可选:v.optional(v.string())

  2. 编写脚本将该字段的历史数据更新为 undefined

  3. 确认数据清理完毕后,在 Schema 中移除该字段。

索引优化

索引能极大提升查询速度,但会占用存储空间。应避免创建冗余索引。

TSX
// ❌ 冗余配置:查询 team 已经可以通过 by_team_and_user 的前缀匹配完成
// const allTeamMembers = await ctx.db
//   .query("teamMembers")
//   .withIndex("by_team", (q) => q.eq("team", teamId))
//   .collect();

// ✅ 推荐配置:复用复合索引
// 假设创建了索引 .index("by_team_and_user", ["team", "user"])
const allTeamMembers = await ctx.db
  .query("teamMembers")
  // 使用复合索引的第一部分进行查询
  .withIndex("by_team_and_user", (q) => q.eq("team", teamId))
  .collect();

const currentTeamMember = await ctx.db
  .query("teamMembers")
  .withIndex("by_team_and_user", (q) =>
    q.eq("team", teamId).eq("user", currentUserId),
  )
  .unique();

利用 Convex-Helpers 简化开发

convex-helpers 是官方提供的实用工具库,能显著减少样板代码。

自定义 React Hooks

封装鉴权逻辑,避免未登录时的无效请求。

TSX
import { FunctionReference } from "convex/server";
import {
  OptionalRestArgsOrSkip,
  useConvexAuth,
  useQueries,
} from "convex/react";
import { makeUseQueryWithStatus } from "convex-helpers/react";

// 创建带状态(pending/success/error)的 hook
export const useQueryWithStatus = makeUseQueryWithStatus(useQueries);

/**
 * 自动检查鉴权状态的 Query 包装器
 * 如果用户未登录,该 Query 会自动跳过 ("skip")
 */
export function useAuthenticatedQueryWithStatus<
  Query extends FunctionReference<"query">,
>(query: Query, args: OptionalRestArgsOrSkip<Query>[0] | "skip") {
  const { isAuthenticated } = useConvexAuth();
  return useQueryWithStatus(query, isAuthenticated ? args : "skip");
}

或者结合 <Authenticated> 组件保护路由,再直接使用 useQueryWithStatus

高级自定义函数 (Custom Functions)

通过扩展 ctx,可以直接在 Server Function 中获取当前用户对象,或者自动应用行级安全 (RLS) 规则。

TSX
import { customQuery, customCtx } from "convex-helpers/server/customFunctions";
import { ConvexError } from "convex/values";

const userQuery = customQuery(
  query, // 基础查询函数
  customCtx(async (ctx) => {
    // 统一鉴权逻辑
    const identity = await ctx.auth.getUserIdentity();
    if (!identity) {
      throw new ConvexError("Not authenticated!");
    }

    const user = await getUser(ctx, identity.subject); // 假设的获取用户逻辑
    const db = wrapDatabaseReader({ user }, ctx.db, rules); // 应用 RLS 规则

    return { user, db }; // 扩展 ctx
  }),
);

组件生态 (Components)

数据聚合 (Aggregate)

Convex 提供了 TableAggregate 组件,用于高效处理计数、求和等聚合操作,无需遍历全表。

初始化组件

TSX
const aggregate = new TableAggregate<{
  Key: number;
  DataModel: DataModel;
  TableName: "mytable";
}>(components.aggregate, {
  sortKey: (doc) => doc._creationTime, // 允许按时间范围查询
  sumValue: (doc) => doc.value, // 指定用于 .sum 计算的字段
});

保持数据同步 需要在数据变更操作 (Mutation) 中手动触发更新:

TSX
// 插入数据
const id = await ctx.db.insert("mytable", { foo, bar });
const doc = await ctx.db.get(id);
await aggregate.insert(ctx, doc!);

// 更新数据
const oldDoc = await ctx.db.get(id);
await ctx.db.patch(id, { foo: newFoo });
const newDoc = await ctx.db.get(id);
await aggregate.replace(ctx, oldDoc!, newDoc!);

// 删除数据
const oldDoc = await ctx.db.get(id);
await ctx.db.delete(id);
await aggregate.delete(ctx, oldDoc!);

使用聚合查询

TSX
// 在 Query 或 Mutation 中直接调用
const tableCount = await aggregate.count(ctx);

自部署

Convex 支持基于 Docker 的私有化部署。以下是适配 Coolifydocker-compose.yml 配置示例:

YAML
services:
  backend:
    image: "ghcr.io/get-convex/convex-backend:latest"
    volumes:
      - "data:/convex/data"
    environment:
      - SERVICE_FQDN_BACKEND_3210
      - "INSTANCE_NAME=${INSTANCE_NAME:-self-hosted-convex}"
      - "INSTANCE_SECRET=${SERVICE_HEX_32_SECRET}"
      - "CONVEX_RELEASE_VERSION_DEV=${CONVEX_RELEASE_VERSION_DEV:-}"
      - "ACTIONS_USER_TIMEOUT_SECS=${ACTIONS_USER_TIMEOUT_SECS:-}"
      - "CONVEX_CLOUD_ORIGIN=${SERVICE_FQDN_BACKEND_3210}"
      - "CONVEX_SITE_ORIGIN=${SERVICE_FQDN_BACKEND_3210}/http"
      - "DATABASE_URL=${DATABASE_URL:-}"
      - "DISABLE_BEACON=${DISABLE_BEACON:-}"
      - "REDACT_LOGS_TO_CLIENT=${REDACT_LOGS_TO_CLIENT:-}"
      - "CONVEX_SELF_HOSTED_URL=${SERVICE_FQDN_CONVEX_6791}"
    healthcheck:
      test: "curl -f http://127.0.0.1:3210/version"
      interval: 5s
      start_period: 5s
  dashboard:
    image: "ghcr.io/get-convex/convex-dashboard:latest"
    environment:
      - SERVICE_FQDN_CONVEX_6791
      - NEXT_PUBLIC_DEPLOYMENT_URL=$SERVICE_FQDN_BACKEND_3210
    depends_on:
      backend:
        condition: service_healthy
    healthcheck:
      test: "wget -qO- http://127.0.0.1:6791/"
      interval: 5s
      start_period: 5s

参考资源

正文结束