nextjs学习第2期 - 集成convex

Convex 数据库是一个开源的响应式后端即服务(Backend-as-a-Service)平台,专为现代全栈应用开发设计。它集成了数据库、服务器函数、文件存储、实时同步、身份验证等功能,开发者只需编写 TypeScript 代码即可构建高性能、实时更新的应用。

集成convex

安装依赖

指南:docs.convex.dev/quickstart/...

pnpm install convex

运行convex

pnpm dlx convex dev

结束后项目根目录下会生成配置文件:.env.local ,并且看板会生成新的convex项目:

添加测试数据

这是一个测试

sampleData.jsonl

typescript 复制代码
{"text": "Buy groceries", "isCompleted": true}
{"text": "Go for a swim", "isCompleted": true}
{"text": "Integrate Convex", "isCompleted": false}

将数据导入数据库:pnpm dlx convex import --table tasks sampleData.jsonl

创建查询请求:convex/tasks.ts

typescript 复制代码
import { query } from "./_generated/server";

export const get = query({
  args: {},
  handler: async (ctx) => {
    return await ctx.db.query("tasks").collect();
  },
});

创建**ConvexClientProvider**

创建 app/ConvexClientProvider.tsx

typescript 复制代码
"use client";

import { ConvexProvider, ConvexReactClient } from "convex/react";
import { ReactNode } from "react";

const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!);

export function ConvexClientProvider({ children }: { children: ReactNode }) {
  return <ConvexProvider client={convex}>{children}</ConvexProvider>;
}

在 app/layout.tsx 中使用:

tsx 复制代码
<ConvexClientProvider>
  {children}
</ConvexClientProvider>

展示数据

app/page.tsx

tsx 复制代码
"use client";

import Image from "next/image";
import { useQuery } from "convex/react";
import { api } from "../convex/_generated/api";

export default function Home() {
  const tasks = useQuery(api.tasks.get);
  return (
    <main className="flex min-h-screen flex-col items-center justify-between p-24">
      {tasks?.map(({ _id, text }) => <div key={_id}>{text}</div>)}
    </main>
  );
}

启动服务,发现数据显示在页面上,到此convex集成完毕,可以把测试数据删掉。

集成clerk进行认证

官方指南:docs.convex.dev/auth/clerk#...

clerk创建应用

clerk创建新应用,并选择认证方式:

Clerk创建 JWT 模板

选择convex然后保存

配置

配置1:

复制上面的 Issuer url 添加到配置中:.env.local

NEXT_PUBLIC_CLERK_FRONTEND_API_URL=

配置2:

创建 convex/auth.config.ts

typescript 复制代码
export default {
  providers: [
    {
      // Replace with your own Clerk Issuer URL from your "convex" JWT template
      // or with `process.env.CLERK_JWT_ISSUER_DOMAIN`
      // and configure CLERK_JWT_ISSUER_DOMAIN on the Convex Dashboard
      // See https://docs.convex.dev/auth/clerk#configuring-dev-and-prod-instances
      domain: process.env.CLERK_JWT_ISSUER_DOMAIN,
      applicationID: "convex",
    },
  ]
};

并在 convex 中配置 CLERK_JWT_ISSUER_DOMAIN,值还是之前复制的 issuer url:

然后尝试重新启动convex:pnpm dlx convex dev,没有异常就可以。

安装clerk依赖

pnpm install @clerk/nextjs

配置 clerk keys

复制 clerk keys 到 .env.local文件中

plain 复制代码
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=xxx
CLERK_SECRET_KEY=xxx

添加 Clerk middleware

创建 src/middleware.ts

typescript 复制代码
import { clerkMiddleware } from '@clerk/nextjs/server'

export default clerkMiddleware()

export const config = {
  matcher: [
    // Skip Next.js internals and all static files, unless found in search params
    '/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
    // Always run for API routes
    '/(api|trpc)(.*)',
  ],
}

配置 ConvexProviderWithClerk

修改文件 app/ConvexClientProvider.tsx

tsx 复制代码
"use client";

import { ConvexReactClient } from "convex/react";
import { ConvexProviderWithClerk } from "convex/react-clerk";
import { ReactNode } from "react";
import { useAuth } from '@clerk/nextjs'

if (!process.env.NEXT_PUBLIC_CONVEX_URL) {  // 新增
  throw new Error('Missing NEXT_PUBLIC_CONVEX_URL in your .env file')
}

const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!);

export function ConvexClientProvider({ children }: { children: ReactNode }) {
  // ConvexProvider 替换成 ConvexProviderWithClerk,并添加 ClerkProvider 包裹
  return (
    <ClerkProvider>
      <ConvexProviderWithClerk client={convex} useAuth={useAuth}>
        {children}
      </ConvexProviderWithClerk>
    </ClerkProvider>
  )
}

认证

ConvexClientProvider.tsx,区分认证和未认证:

tsx 复制代码
export function ConvexClientProvider({ children }: { children: ReactNode }) {
  return (
    <ClerkProvider>
      <ConvexProviderWithClerk client={convex} useAuth={useAuth}>
        {/* 认证通过 */}
        <Authenticated>{children}</Authenticated>

        {/* 认证中 */}
        <AuthLoading> 认证中... </AuthLoading>

        {/* 未认证 */}
        <Unauthenticated>
          未认证
          <SignInButton />
        </Unauthenticated>
      </ConvexProviderWithClerk>
    </ClerkProvider>
  )
}

app/page.tsx

tsx 复制代码
export default function Home() {
  return (
    <>
      认证通过
      <UserButton />
    </>
  );
}

未认证的状态:

当点击 Sign in 按钮,跳转到登录表单:

登录中:

登录后:

到此就完成了登录认证能力。

用户数据同步到convex

当前用户信息只会体现在clerk中,可以在clerk创建webhook,同步数据到convex中。

创建webhook

先在clerk中创建webhook,endpoint url 前部分填写 convex http actions url(convex看板 -> settings -> URL & Deploy Key -> Http Actions URL),后部分自定义 clerk-users-webhook

点击创建,复制 Signing Secret,配置到 .env.local 文件中(convex 看板 变量中也同步添加):

CLERK_WEBHOOK_SECRET=xxx

创建用户schema

项目中声明用户schema:

typescript 复制代码
// convex/schema.ts
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";

export default defineSchema({
  users: defineTable({
    clerkId: v.string(),      // Clerk 的 user.id
    email: v.string(),
    username: v.string(),
    imageUrl:  v.string(),
  })
    .index("by_clerkId", ["clerkId"])
    .index("by_email", ["email"]),
});

保存之后,convex 看板会自动创建 users 这个表格。

创建用户 CRUD

typescript 复制代码
// convex/users.ts
import { v } from 'convex/values';
import { internalMutation, internalQuery } from './_generated/server';

export const create = internalMutation({
  args: {
    username: v.string(),
    imageUrl: v.string(),
    clerkId: v.string(),
    email: v.string()
  },
  handler: async (ctx, args) => {
    await ctx.db.insert("users", args);
  }
})

export const get = internalQuery({
  args: {
    clerkId: v.string(),
  },
  handler: async (ctx, args) => {
    await ctx.db.query("users")
      .withIndex("by_clerkId", q => q.eq("clerkId", args.clerkId))
      .unique();
  }
})

webhook接收器

typescript 复制代码
// convex/http.ts
import { httpRouter } from "convex/server";
import { httpAction } from "./_generated/server";
import { internal } from "./_generated/api";
import { WebhookEvent } from "@clerk/nextjs/server";
import { Webhook } from "svix";


async function validatePayload(req: Request): Promise<WebhookEvent | undefined> {
  const payload = await req.text();

  // 1. 安全校验
  const svixHeaders = {
    "svix-id": req.headers.get("svix-id")!,
    "svix-signature": req.headers.get("svix-signature")!,
    "svix-timestamp": req.headers.get("svix-timestamp")!
  }

  const webhook = new Webhook(process.env.CLERK_WEBHOOK_SECRET!);
  try {
    return webhook.verify(payload, svixHeaders) as WebhookEvent;
  } catch (err) {
    console.error("Webhook signature verification failed", err);
    return ;
  }
} 


const handleClerkWebhook = httpAction(async (ctx, req) => {
  const event = await validatePayload(req);

  if (!event) {
    return new Response("Could not validate Clerk payload", {
      status: 400
    });
  }

  switch (event.type) {
    case "user.created":
      const user = await ctx.runQuery(internal.users.get, {clerkId: event.data.id});

      if (user) {
        console.log(`Updating user ${event.data.id} with: ${event.data}`);
      }

    case "user.updated":
      console.log("Creating/Updating User:", event.data.id);
      await ctx.runMutation(internal.users.create, {
        username: `${event.data.first_name} ${event.data.last_name}`,
        imageUrl: event.data.image_url,
        clerkId: event.data.id,
        email: event.data.email_addresses[0].email_address
      })
      break;

    default:
      console.log("Clerk webhook event not supported:", event.type);
  }
  return new Response("OK", { status: 200 });
})

const http = httpRouter();

http.route({
  path: "/clerk-users-webhook",
  method: "POST",
  handler: handleClerkWebhook
})

export default http;

然后先清空clerk所有的用户,重新登录,观察clerk数据是否同步到convex中:

说明数据正常同步。

相关推荐
Mintopia7 小时前
🚀 Next 数据库集成:建模篇
前端·javascript·next.js
EndingCoder1 天前
测试 Next.js 应用:工具与策略
开发语言·前端·javascript·log4j·测试·全栈·next.js
Mintopia1 天前
Next 全栈开发:Prisma 的安装与配置指南 —— 从数据库魔法到代码现实
前端·javascript·next.js
玲小珑1 天前
Next.js 教程系列(二十八)Next.js 的安全最佳实践
前端·next.js
掘金安东尼1 天前
Next.js 原生实现 PWA 离线能力
前端·javascript·next.js
Mintopia3 天前
Next.js 全栈:接收和处理请求
前端·javascript·next.js
玲小珑3 天前
Next.js 教程系列(二十七)React Server Components (RSC) 与未来趋势
前端·next.js
Mintopia4 天前
Next.js 全栈开发基础:在 pages/api/*.ts 中创建接口的艺术
前端·javascript·next.js
遂心_5 天前
Next.js 入门实战:从零构建你的第一个 SSR 应用
前端·javascript·next.js