大数跨境

使用 Velt、Clerk Auth、Prisma 和 Radix UI 构建具有实时评论和 @Mentions 的协作应用程序

使用 Velt、Clerk Auth、Prisma 和 Radix UI 构建具有实时评论和 @Mentions 的协作应用程序 索引目录
2025-06-10
2
导读:关注【索引目录】服务号,更多精彩内容等你来探索!实时评论和 @提及等互动功能已成为现代 Web 应用中不可或缺的一部分。

关注【索引目录】服务号,更多精彩内容等你来探索!

实时评论和 @提及等互动功能已成为现代 Web 应用中不可或缺的一部分。用户如今期望实时更新和无缝协作,因此这些工具对于保持用户参与度并让您的应用脱颖而出至关重要。

但实时评论和提及功能不仅能创造更具互动性的体验,还能在用户群中建立真正的社区感。当人们能够在你的平台上讨论内容、分享反馈并互相帮助时,他们就再也不想离开了。

不相信?去 Reddit 问问!

简而言之,如果您想提高用户参与度和忠诚度,添加实时评论是明智之举。

在本文中,我们将向您展示如何使用Next.js、 PrismaRadix UI、 Clerk AuthVelt构建实时评论系统,以实现实时协作功能。

您将了解:

  • 如何简化身份验证流程
  • 如何处理数据流
  • 如何实时提供更新。

最后,您将拥有一个现代协作平台的坚实框架,该平台可以轻松扩展并为您的用户提供引人入胜的体验。

最终的应用程序登陆页面将如下所示:



但首先,让我们看看从头开始构建这些功能时可能面临的挑战。

从头开始使用 @Mentions 构建实时评论的挑战

从零开始构建一个包含提及的实时评论系统可能相当复杂。您需要一个直观的前端和一个能够实时处理评论的可靠后端。因此,这不仅仅是编写代码的问题,还关乎您投入的时间和开发人力。

虽然这仍然可行,但您可能会面临以下主要挑战。

前端:

以下是您将要面对的情况的详细说明:

  • 交互式评论线程:
    您需要一个动态界面,评论(和回复)一经发布就会更新。
  • @Mentions 自动完成:
    标记另一个用户可能听起来很简单,但它需要检测“@”按键并立即显示用户建议的下拉列表。
  • 存在指示器:
    如果您希望用户真正感受到他们参与了实时讨论,则需要显示还有谁在线或正在打字。
  • 应用内通知:
    当有人提及某个用户或回复其评论时,该用户应该立即知道。

头疼了吗?等等,还有更多!

后端:

构建实时评论系统的后端与创建前端一样具有挑战性。你需要:

  • 数据存储和检索:
    可靠的数据库设计,可以存储评论、提及的用户配置文件以及将它们链接在一起的线程或回复结构。
  • 实时数据同步:
    为了实现即时更新,您的后端必须实时向客户端推送新的评论和通知。
  • 评论和提及的 API 端点:
    您需要构建安全的 API 路由来处理评论的创建和获取。例如,“发表评论” API 应该接受评论文本(如果是回复,则可能需要线程 ID)以及提及的用户 ID 列表。
  • 身份验证和授权:
    安全至关重要,只有经过身份验证的用户才可以发表评论或查看特定信息。后端需要在每次请求时验证用户的身份(例如通过令牌或会话)。
  • 输入验证和垃圾信息控制:
    接受用户生成的内容意味着您必须对输入保持警惕。为了防止垃圾信息或恶意数据,您应该验证传入的评论数据。
  • 通知和提及逻辑:
    当评论包含提及时,后端必须向被提及的用户创建通知条目(或发送电子邮件/实时警报)。

如你所见,这已经相当多了。考虑到以上几点,创建一个自定义的实时评论系统需要大量的资源,包括时间、开发、专业知识和持续维护。如果你仍然坚持自己动手,你仍然会遇到系统扩展和增强的问题。

我的建议?不,不要从零开始创建一切。当你可以轻松使用 Velt时,就不要这么做。

Velt SDK 简介



Velt 是一款开发者工具包,它能为您的应用带来实时协作功能,无需繁琐的开发。它就像一个即插即用的解决方案,提供 Figma 或 Notion 等应用中常见的实时评论、在线状态指示器和应用内通知等功能,但其功能却被捆绑到 React 库中。

您无需费力处理自定义 WebSocket、实时协议和复杂的 UI,只需将 Velt 的组件直接嵌入代码即可。无论您添加的是文本评论、提及、回复还是音频笔记,Velt 都会在后台处理整个客户端逻辑和实时同步,无需您手动操作。

即使在后端,Velt 也显著简化了流程。一旦你通过 API 密钥将应用连接到 Velt,Velt 的服务就能确保新评论立即广播给所有关注者,并实时发出提及通知。你仍然可以将数据存储在数据库中以进行记录,或将其与任何现有的用户身份验证绑定,Velt 可以完美地处理所有这些操作。

最棒的是?您还能继承 Velt 内置的安全性和扩展功能,从而降低遇到身份验证漏洞或性能瓶颈的可能性。简而言之,Velt 承担了繁重的工作,让您可以快速启动并运行协作应用,而无需牺牲可靠性或用户体验。

现在,让我们进入有趣的部分,并向您展示如何使用 Velt 构建评论和提及系统

我们为什么选择使用的工具

让我们从 Prisma 开始。在我们的评论系统中,我们需要处理评论、提及和用户之间复杂的关系。我们选择 Prisma 是因为它具有强大的类型安全性、便捷的模式管理和高效的数据处理能力。



其类型安全的数据库客户端可在编译时捕获错误,从而提高开发速度并确保一致的数据处理。

model Comment {
  id        String   @id @default(cuid())
  content   String
  authorId  String   // Clerk user ID
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  // Relations
  parentId     String?   // For nested comments
  parent       Comment?  @relation("CommentToComment", fields: [parentId], references: [id])
  replies      Comment[] @relation("CommentToComment")
  mentions     Mention[] @relation("CommentMentions")
}

我们应用的 UI 采用Radix UI ,因为它提供了无头、无样式的组件,并自动遵循无障碍最佳实践。这确保了诸如 @mentions 的下拉菜单、通知的模态框和工具提示等元素默认可访问,同时通过 CSS 或实用程序类实现了充分的设计灵活性。



Clerk负责处理我们的身份验证需求。它提供了无缝的身份验证体验,并与 Next.js 完美集成。我们使用它来管理用户会话、配置文件,并保护我们的 API 路由。



以下是我们保护评论端点的方法:

import { auth } from "@clerk/nextjs/server";

export async function POST(req: Request) {
  const { userId } = await auth();
  if (!userId) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }
  // Handle comment creation
}

您还可以使用Upstash Redis进行缓存,但这是可选的。

在此实现结束时,.env.example项目根目录中的文件应具有以下变量:

# Clerk Authentication
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=your_clerk_publishable_key
CLERK_SECRET_KEY=your_clerk_secret_key

# Velt Real-time Collaboration
NEXT_PUBLIC_VELT_API_KEY=your_velt_api_key

# Database
DATABASE_URL="postgresql://user:password@localhost:5432/your_database_name"

# Rate Limiting
RATE_LIMIT_MAX=60
RATE_LIMIT_WINDOW_MS=60000

先决条件:

  • 对于本教程,您应该安装 Node.js 并对 Next.js 有基本的了解。
  • 您还需要一个免费的 Clerk 帐户(用于获取用于身份验证的 API 密钥)和 Velt 帐户(用于获取用于协作功能的 API 密钥)。

设置你的项目

首先,让我们设置我们的 Next.js 项目并安装所需的一切。我们将使用 Clerk 进行身份验证,使用 Velt 进行实时协作,并提供一些主题支持,以保持界面简洁。

打开终端并运行以下命令来创建一个基于 TypeScript 的 Next.js 应用程序,其中包含所有必需的包:

`npx create-next-app@latest comments-app --typescript`
`cd comments-app`

现在,您必须安装必要的软件包:

npm install @clerk/nextjs @veltdev/react @prisma/client zod next-themes
npm install prisma --save-dev

接下来,在您的项目中初始化 Prisma:

npx prisma init

现在是时候设置我们的数据库模式了。在prisma/schema.prisma文件中创建以下模型:

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

generator client {
  provider = "prisma-client-js"
}

model Comment {
  id        String   @id @default(cuid())
  content   String
  authorId  String   // Clerk user ID
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  // Relations
  parentId     String?   
  parent       Comment?  @relation("CommentToComment", fields: [parentId], references: [id], onDelete: Cascade)
  replies      Comment[] @relation("CommentToComment")
  mentions     Mention[] @relation("CommentMentions")

  @@index([authorId])
  @@index([parentId])
  @@index([createdAt])
}

model Mention {
  id        String   @id @default(cuid())
  userId    String   // Clerk user ID of the mentioned user
  commentId String
  createdAt DateTime @default(now())

  comment      Comment       @relation("CommentMentions", fields: [commentId], references: [id], onDelete: Cascade)
  notification Notification? @relation("MentionNotification")

  @@index([userId])
  @@index([commentId])
  @@unique([userId, commentId])
}

配置 Clerk Auth

Clerk 将在我们的应用中处理用户帐户和会话。要进行设置,您需要提供您的 Clerk 凭据,并在 Next.js 中初始化 Clerk 的中间件。在项目根目录中创建一个 .env.local 文件并添加您的 Clerk 凭据:

`NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=your_publishable_key`

`CLERK_SECRET_KEY=your_secret_key`

由于 Clerk 负责处理身份验证,我们需要设置一些中间件来妥善管理用户会话。只需前往项目的根目录,创建或更新该middleware.ts文件即可。这将确保身份验证在整个应用中无缝运行,从而保障评论系统的安全性,并为每个用户提供个性化服务。

import { authMiddleware } from "@clerk/nextjs";
export default authMiddleware({

// Public routes that don't require authentication
publicRoutes: ["/", "/api/public"]
});
export const config = {
matcher: ['/((?!.+\\\\.[\\\\w]+$|_next).*)', '/', '/(api|trpc)(.*)'],
};

配置 Velt

现在,我们将连接 Velt SDK。与 Clerk 一样,Velt 的功能也需要 API 密钥。您需要注册一个 Velt 账户,然后前往您的 Velt 账户/控制面板获取 API 密钥,并将其添加到同一个.env.local文件中:

NEXT_PUBLIC_VELT_API_KEY=your_velt_api_key

重要提示:您必须在 velt 控制台仪表板中启用应用内通知



接下来,我们将配置Providers.tsx组件,将 Clerk 的身份验证系统与 Velt 的实时协作功能连接起来。通过使用 Clerk 和 Velt 的提供程序包装我们的应用,我们确保身份验证上下文和协作工具在整个应用程序中均可访问。最后,我们将已验证的用户信息传递给 Velt,以实现特定于用户的实时交互。

"use client";
import { ClerkProvider, useUser } from "@clerk/nextjs";
import { VeltProvider } from "@veltdev/react";

function VeltProviderWithAuth({ children }: { children: React.ReactNode }) {
  const { user, isSignedIn } = useUser();
  const veltUser = isSignedIn && user
    ? { userId: user.id, name: user.fullName || "Anonymous", /* ... */ }
    : { userId: "anonymous", name: "Anonymous", /* ... */ };

  return (
    <VeltProvider 
      apiKey={process.env.NEXT_PUBLIC_VELT_API_KEY}
      user={veltUser}  // Pass user directly to VeltProvider
    >
      {children}
    </VeltProvider>
  );
}

// Then in the child component:
function CommentSection() {
  const { client } = useVeltClient();  // This is safe because it's inside VeltProvider
  const { user } = useUser();

  useEffect(() => {
    if (client && user) {
      client.identify({
        userId: user.id,
        name: user.fullName || "Anonymous",
        // ... other user data
      });
    }
  }, [client, user]);

  return <div>...</div>;
}

在这里,我们设置了一个组合提供程序,以确保 Clerk 和 Velt 能够协同工作。注意我们如何根据 Clerk 的数据创建 Velt 用户并配置实时评论功能。是不是很棒?

保护你的路线

为了保护与评论相关的端点,请创建具有 Clerk 保护的 API 路由:

import { auth } from "@clerk/nextjs";
import { NextResponse } from "next/server";
export async function POST(request: Request) {
const { userId } = auth();
// Check if user is authenticated
    if (!userId) {
        return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
    }
    try {
    // Your comment handling logic here
        const body = await request.json();
        return NextResponse.json({ success: true });
    } catch (error) {
        return NextResponse.json(
    { error: "Failed to process request" },
    { status: 500 }
    );
    }
}

在这里我们确保只有登录的用户才能提交评论,以帮助确保您的应用程序安全。

让我们通过使用 Providers 组件包装您的应用程序来完成设置。这可确保 Clerk 的身份验证和 Velt 的实时功能在整个应用中均可使用。

打开您的layout.tsx文件(或者_app.tsx如果使用 Pages Router)并像这样更新它:

import { Providers } from "@/components/Providers";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}

实时评论页面

现在是时候看看实时评论功能的实际效果了,因此我们将构建一个页面,让用户可以实时查看和发表评论。

在使用 App Router 的 Next.js 应用中,您可以/comments通过在 处添加文件来创建页面app/comments/page.tsx。如果您使用的是旧版 Pages Router,则应将文件放置在 处pages/comments.tsx

在此页面组件中,我们将使用 Velt 的 React 组件来设置评论 UI:

"use client";
import { VeltComments, VeltCommentTool } from "@veltdev/react";
import { useUser } from "@clerk/nextjs";

export default function CommentsPage() { 
const { isLoaded, isSignedIn } = useUser(); 
const router = useRouter(); 
useEffect(() => { 
if (isLoaded && !isSignedIn) { router.push("/sign-in?redirect_url=/comments"); 

}, [isLoaded, isSignedIn, router]); 
useSetDocument("comments-page", { 
documentName: "Comments Page", 
documentDescription: "Page for managing and viewing comments", 
metadata: { 
type: "comments", 
status: "active", 
}, 
}); 
if (!isSignedIn) return null;
  return (
    <main>
      <h1>Comments</h1>
      <VeltComments id="comments-page" />
      <VeltCommentTool id="comments-page" />
    </main>
  );
}

在页面呈现之前,它会检查用户是否已登录,并在需要时将其重定向到注册页面。用户通过身份验证后,各种 Velt 组件会在评论页面上启用实时更新和交互。

至此,前端基本完成。运行 Next.js 应用后,页面/comments将显示评论框和(初始为空的)评论线程。在线状态指示器和通知铃声也会出现,但它们在有人在线或触发通知之前不会有太大作用。如果您现在尝试提交评论,目前不会有任何反应——因为我们尚未实现后端逻辑来处理发布评论或检索任何现有评论/通知。

评论和@Mentions 的后端

我们需要一个 API 路由来接收新的评论(并处理这些评论中的提及)。在使用 App Router 的 Next.js 应用中,您可以/comments通过在 处添加文件来创建页面app/comments/page.tsx。如果您使用的是旧版 Pages Router,则应将文件放置在 处pages/comments.tsx

以下是使用 Next.js API 路由处理程序、用于身份验证的 Clerk 和用于数据库操作的 Prisma(ORM)的示例实现:

import { auth } from "@clerk/nextjs/server";
import { NextResponse } from "next/server";
import { prisma } from "@/lib/db";
import { createCommentSchema } from "@/lib/validations";

export async function POST(req: Request) {
  const { userId } = await auth();
  if (!userId)
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });

  const body = createCommentSchema.parse(await req.json());
  const comment = await prisma.comment.create({
    data: { content: body.content, authorId: userId, parentId: body.parentId },
  });
  // Optionally handle @mentions...
  return NextResponse.json(comment);
}

这就是我们的 API 在确保安全高效的同时处理新评论创建的方式。它包含速率限制以防止垃圾邮件,使用 Zod 验证用户输入,并确保提及触发正确的通知。此外,它还能顺畅地管理提及记录的创建,以便用户在对话中被标记时收到通知。

实现@mentions

得益于 Velt,许多提及功能都可在前端自动处理。我们userSuggestions在 Velt 提供程序中配置了可提及用户列表。当用户输入 时@,Velt 会自动显示一个可提及用户的下拉列表。

为了实现这一点,我们首先需要将正确的用户数据从 Clerk 传递给 Velt。当用户通过 Clerk 登录时,身份验证状态由 Clerk 管理。然后,VeltProviderWithAuth 组件会检测登录状态并创建相应的 Velt 用户。Velt 中的 useIdentify 钩子用于告知 Velt 当前用户是谁,而 LayoutContent 组件则向 Velt 提供额外的用户上下文,包括组织信息。

然后,Velt 使用这些信息来支持其实时功能,如存在指示器、提及和通知,从而使 @mention 功能对用户来说无缝且直观。

function VeltProviderWithAuth({ children }: { children: ReactNode }) {
  const { user, isLoaded, isSignedIn } = useUser();
  const apiKey = process.env.NEXT_PUBLIC_VELT_API_KEY;

  // Create a Velt user from Clerk's user data
  const veltUser =
    isSignedIn && user
      ? {
          userId: user.id,
          name: user.fullName || user.username || "Anonymous",
          email: user.emailAddresses[0]?.emailAddress || "",
          photoUrl: user.imageUrl || "",
          organizationId: "default-org",
        }
      : {
          userId: "anonymous",
          name: "Anonymous",
          email: "",
          photoUrl: "",
          organizationId: "default-org",
        };

  const veltConfig = {
    apiKey,
    debug: true,
    user: veltUser,
    organizationId: "default-org",
    // ... other configuration options
  };

  return <VeltProvider {...veltConfig}>{children}</VeltProvider>;
}

用户数据直接来自您的 Clerk 身份验证,确保只能提及真实的、经过身份验证的用户。

接下来,我们可以通过向 Velt 提供可提及的用户列表,使用 @mentions 实现联系人列表管理。这涉及两个关键组件:

  • 以 Velt 预期格式返回用户的 API 端点:
// src/app/api/users/route.ts
export async function GET() {
  try {
    const { userId } = await auth();
    if (!userId) return NextResponse.json({ users: [], groups: [] });

    const client = await clerkClient();
    const users = await client.users.getUserList({
      limit: 100,
      orderBy: '-created_at'
    });

    const veltUsers = users.data.map(user => ({
      userId: user.id,
      name: `${user.firstName || ""} ${user.lastName || ""}`.trim() || "Anonymous",
      email: user.emailAddresses[0]?.emailAddress || "",
      photoUrl: user.imageUrl || "",
      groups: ["organization"]
    }));

    return NextResponse.json({
      users: veltUsers,
      groups: [{
        id: "organization",
        name: "Organization",
        description: "All users in the organization"
      }]
    });
  } catch (error) {
    return NextResponse.json({ users: [], groups: [] }, { status: 500 });
  }
}
  • 使用 Velt 实用程序管理联系人列表的组件:
function VeltUserSetup({ children }: { children: ReactNode }) {
  const { user, isSignedIn } = useUser();
  // Get access to Velt's contact utilities
  const contactElement = useContactUtils();
  const [allUsers, setAllUsers] = useState<Array<{
    userId: string;
    name: string;
    email: string;
    photoUrl: string;
  }>>([]);

  // Fetch all users from our API
  useEffect(() => {
    async function fetchUsers() {
      if (!isSignedIn) return;
      const response = await fetch("/api/users");
      const users = await response.json();
      setAllUsers(users);
    }
    fetchUsers();
  }, [isSignedIn]);

  // Update Velt's contact list with our users
  useEffect(() => {
    if (contactElement && allUsers.length > 0) {
      contactElement.updateContactList(allUsers, { merge: false });
    }
  }, [contactElement, allUsers]);

  return <>{children}</>;
}

此@mention 系统通过我们的自定义逻辑和 Velt 的内置功能组合来工作。

此设置的核心是useContactUtils钩子,它提供对联系人相关实用程序的访问,我们需要通过该实用程序来管理可提及的用户,主要通过updateContactList,我们使用它来指定在给定上下文中可以提及的用户。该updateContactList方法接受两个参数:一个根据 Velt 要求格式化的用户列表,以及一个允许我们配置更新行为方式的选项对象。

{
         merge: boolean,  // Whether to merge with existing contacts
         scope?: string   // Optional: Limit contacts to specific document/location
       }

因此,当有人在评论中输入“@”时,Velt 会立即查找我们共享过的用户列表updateContactList,并弹出一个包含这些名称的下拉菜单。用户只需从列表中选择某个用户,Velt 就会自动将其姓名添加到评论中作为提及——无需额外步骤。整个过程自然流畅,毫不费力。此外,由于列表与您的 Clerk 用户群保持同步,任何新用户都会自动添加,因此每个人都能随时了解最新情况,并随时准备被提及。

当某人在评论中被提及时的评论固定示例:



实时状态和通知


实时交互让您的应用充满活力。本指南将向您展示 Velt 如何提供实时状态指示器和通知铃声。

例如,通知面板只需添加:

<div className="fixed top-4 right-24 z-50">
<VeltNotificationsTool />
</div>

该组件会自动显示任何新通知(例如提及),确保用户随时了解最近的活动。

我们已经在前端添加了<VeltPresence><VeltNotificationsTool>组件。状态组件通过 Velt 的实时服务自动运行,每当用户访问页面时,状态组件就会useSetDocument("comments-page")运行,Velt 的服务会知道该用户在该文档中,并通过状态组件通知其他用户。因此,除了我们所做的识别之外,无需自定义后端代码来实现状态。

通知面板应显示如下@mention 通知:



通知系统由处理获取通知的专用 API 路由支持:

export async function GET(req: Request) {
try {
const { userId } = await auth();
if (!userId) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const { searchParams } = new URL(req.url);
const parsed = paginationSchema.parse({
page: searchParams.get("page"),
limit: searchParams.get("limit"),
});
const skip = (parsed.page - 1) * parsed.limit;

// Fetch notifications with mention details
const [notifications, total] = await Promise.all([
prisma.notification.findMany({
where: {
userId,
},
include: {
mention: {
include: {
comment: true,
},
},
},
orderBy: {
createdAt: "desc",
},
skip,
take: parsed.limit,
}),
prisma.notification.count({
where: {
userId,
},
}),
]);
return NextResponse.json({
notifications,
total,
pages: Math.ceil(total / parsed.limit),
});
} catch (error) {
// Error handling
}
}

至此,我们有:

  • 实时评论用户界面
  • 能够发表评论并提及
  • 实时呈现(由 Velt 处理)
  • 提及通知(创建和获取)

安全最佳实践

让我们总结一下迄今为止所应用的安全最佳实践,以便更清楚:

身份验证保护

我们使用的所有敏感路由(评论页面及相关 API)都会检查用户是否为有效的已验证用户。未经验证的请求会返回 401 错误或重定向至登录页面,以确保只有合法用户才能发布或查看评论。以下是我们的检查方法:

import { clerkMiddleware } from "@clerk/nextjs/server";
import type { NextRequest } from "next/server";
export default clerkMiddleware((auth, req: NextRequest) => {
*// Allow Velt API routes to bypass authentication*
const url = req.nextUrl.pathname;
if (url.startsWith("/api/velt")) {
return NextResponse.next();
}
});

此中间件函数默认使用 Clerk 对所有请求强制执行身份验证。但是,如果请求的 URL 路径以 开头/api/velt,则会跳过身份验证检查,允许请求继续进行,而无需用户登录。本质上,它使用 Clerk 的身份验证锁定了整个应用程序,同时仍然为 下的任何端点提供了一个公共“漏洞” /api/velt,如果您希望某些 Velt 特定的 API 路由无需登录即可访问,这将非常方便。

输入验证和垃圾邮件预防

我们对传入数据使用了架构验证 ( Zod),以避免恶意输入。我们还对评论提交实施了速率限制,以防止垃圾评论。该速率限制应用于评论 API 端点,通过限制单个用户发表评论的频率,有助于防止垃圾评论和滥用。

const rateLimitResult = await limiter.check(userId);
if (rateLimitResult.status === 429) {
return rateLimitResult;
}
const json = await req.json();
const body = createCommentSchema.parse(json);
const comment = await prisma.comment.create({
data: {
content: body.content,
authorId: userId,
parentId: body.parentId,
},
include: {
mentions: true,
},
});

这有助于维护系统的完整性和用户体验,并且每 60 秒设置为 60 个请求,有效地防止垃圾邮件和暴力破解压垮应用程序。

安全用户识别

我们始终通过 Clerk 识别用户,并将该身份传递给 Velt ( useIdentify)。这意味着 Velt 的实时事件和状态与实际用户账户绑定。用户无法在实时系统中冒充他人,因为 Clerk 提供了userId我们提供给 Velt 的验证信息。后端的所有评论操作也依赖于userIdClerk 的 JWT——例如,确保用户无法通过修改客户端以其他用户的身份发表评论,因为我们auth()会在服务器上捕获真实身份。

处理安全响应

我们的 API 会捕获错误,并针对意外问题返回通用消息。我们专门捕获验证错误,以告知客户端出了什么问题,但对于其他错误,我们会避免泄露详细信息(仅返回“内部服务器错误”)。我们还会在服务器上记录错误以便调试。这种方法可以防止向客户端泄露堆栈跟踪或敏感信息,是一种良好的安全做法。

export async function GET(req: Request) {
try {
const { userId } = await auth();
if (!userId) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}

*// ... handle the request ...*
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json({ error: error.errors }, { status: 400 });
}
return NextResponse.json(
{ error: "Internal Server Error" },
{ status: 500 }
);
}
}

这里,我们使用 Next.js 无服务器路由,通过检查 来验证请求是否来自已登录用户userId。如果没有用户,它会立即返回 401(未授权)响应。如果用户有效,代码将继续处理请求。如果任何验证失败(例如,通过 Zod 验证),它会返回 400(错误请求)并附带具体的错误详情。对于任何其他未处理的问题,该函数将响应 500(内部服务器错误)。

错误处理和测试

最后,让我们实现适当的错误处理并为我们的评论系统设置测试。

错误管理

首先,让我们在 Velt 初始化中添加错误处理:

export function LayoutContent({ children }: { children: React.ReactNode }) {
const { userId, isLoaded, isSignedIn } = useAuth();
const { user } = useUser();
const { client } = useVeltClient();
const [veltError, setVeltError] = useState<string | null>(null);
useEffect(() => {
async function initializeVelt() {
if (!client || !isLoaded || !isSignedIn || !userId || !user) return;
try {
await client.identify({
userId,
name: user.fullName || "",
email: user.primaryEmailAddress?.emailAddress || "",
organizationId: organization?.id || "default-org",
});
} catch (error) {
console.error("Error initializing Velt:", error);
setVeltError(
"Failed to initialize real-time features. Please check if you have any content blockers enabled."
);
}
}
initializeVelt();
}, [client, isLoaded, isSignedIn, userId, user, organization]);
if (veltError) {
return <div className="text-red-500">{veltError}</div>;
}
}

该组件仅检查用户是否已通过身份验证且 Velt 客户端已准备就绪,然后调用client.identifyVelt 实时系统注册用户。如果发生任何错误,它会记录错误并向用户显示一条简短的警告消息。

对于我们的 API 端点,我们实现了全面的错误处理和速率限制:

try {
// Rate limiting
const rateLimitResult = await limiter.check(userId);
if (rateLimitResult.status === 429) {
return rateLimitResult;
}
  // Main logic...
} catch (error) {
  if (error instanceof z.ZodError)
    return NextResponse.json({ error: error.errors }, { status: 400 });
  return NextResponse.json({ error: "Internal Server Error" }, { status: 500 });
}

首先,我们应用速率限制来阻止垃圾邮件发送者和过多的请求,一旦明确,我们就会使用 Zod 验证请求数据,以确保所有内容都采用正确的格式。

如果出现问题(例如数据库错误或无效数据),我们会捕获它并返回错误响应,以便用户清楚地了解发生了什么。

测试

让我们为评论组件和 API 端点设置测试。首先,为评论组件创建一个测试:

import "@testing-library/jest-dom";
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { useAuth } from "@clerk/nextjs";
import { Comment } from "@/components/Comment";
// Mock Clerk's useAuth hook
jest.mock("@clerk/nextjs", () => ({
useAuth: jest.fn(),
}));
describe("Comment Component", () => {
const mockComment = {
id: "1",
content: "Test comment",
authorId: "user_123",
createdAt: new Date().toISOString(),
mentions: [],
};
beforeEach(() => {
(useAuth as jest.Mock).mockReturnValue({
userId: "user_123",
isLoaded: true,
isSignedIn: true,
});
});
it("renders comment content", () => {
render(<Comment {...mockComment} />);
expect(screen.getByText("Test comment")).toBeInTheDocument();
});
it("shows edit options for comment author", () => {
render(<Comment {...mockComment} />);
expect(screen.getByRole("button", { name: /edit/i })).toBeInTheDocument();
});
it("handles edit submission", async () => {
const onEdit = jest.fn();
render(<Comment {...mockComment} onEdit={onEdit} />);
const editButton = screen.getByRole("button", { name: /edit/i });
await userEvent.click(editButton);
const input = screen.getByRole("textbox");
await userEvent.type(input, " updated");
const submitButton = screen.getByRole("button", { name: /save/i });
await userEvent.click(submitButton);
expect(onEdit).toHaveBeenCalledWith("1", "Test comment updated");
});
});

接下来,我们模拟 Clerk 的useAuth钩子,以模拟一个与评论作者具有相同 ID 的已登录用户。然后,我们验证组件是否在屏幕上显示评论文本,并显示作者的编辑按钮。当我们点击“编辑”时,测试会输入更新后的文本并点击“保存”,同时我们检查它是否使用onEdit正确的参数调用了回调。这样,您就可以确信,对于发布评论的用户来说,编辑工作流程是端到端的。

接下来,让我们测试我们的 API 端点:

// Mock Clerk auth
jest.mock("@clerk/nextjs/server", () => ({
auth: jest.fn(),
}));
// Mock Prisma
jest.mock("@/lib/db", () => ({
prisma: {
comment: {
create: jest.fn(),
},
mention: {
createMany: jest.fn(),
},
notification: {
createMany: jest.fn(),
},
},
}));
describe('Comments API', () => {
beforeEach(() => {
jest.clearAllMocks();
(auth as jest.Mock).mockReturnValue({ userId: 'test_user' });
});
it('creates a comment successfully', async () => {
const { req, res } = createMocks({
method: 'POST',
body: {
content: 'Test comment',
parentId: null,
mentionedUserIds: [],
},
});
(prisma.comment.create as jest.Mock).mockResolvedValue({
id: '1',
content: 'Test comment',
authorId: 'test_user',
mentions: [],
});
await POST(req);
expect(res._getStatusCode()).toBe(200);
expect(JSON.parse(res._getData())).toEqual(
expect.objectContaining({
content: 'Test comment',
})
);
});
it('handles unauthorized requests', async () => {
(auth as jest.Mock).mockReturnValue({ userId: null });
const { req, res } = createMocks({
method: 'POST',
body: {
content: 'Test comment',
},
});
await POST(req);
expect(res._getStatusCode()).toBe(401);
});
it('validates comment data', async () => {
const { req, res } = createMocks({
method: 'POST',
body: {
content: '', // Empty content should fail validation
},
});
await POST(req);
expect(res._getStatusCode()).toBe(400);
});
});

要运行测试,请将这些脚本添加到您的 package.json 中:

{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
}
}

有了额外的错误处理和测试,我们的评论/通知系统现在更加可靠且用户友好。如果出现问题(例如无效请求或有人未登录尝试发帖),系统会立即发现并返回清晰的错误消息。

自动化测试还能让您安心,评论工作流程的每个部分(包括创建新评论和提及)都会在您进行更改时持续运行。最终,这意味着生产过程中的意外更少,并为每个人带来更好的体验。



结论和后续步骤

如果您到目前为止一直在阅读本文,那么您已经完成了一项很棒的工作:使用Next.js、 PrismaRadix UI、 Clerk AuthVelt构建了一个将安全身份验证与实时协作相结合的评论系统。虽然这是一个简单的演示,但您可以使用这些工具在其基础上构建您的项目。

在坚实的基础之上,我们可以根据需要进一步增强评论系统。例如,我们可以添加对更深层次的评论线索(回复回复)的支持,或编辑和删除评论等审核功能。我们或许可以实现高级通知选项(例如,提及时的电子邮件通知,或将通知标记为已读的功能)。

在性能方面,随着评论数量的增长,我们应该考虑缓存和分页功能来加载评论。我们已经为通知添加了分页参数;同样,如果一个页面积累了数千条评论,我们应该分块加载,并缓存最近的评论以便快速检索。

关注【索引目录】服务号,更多精彩内容等你来探索!


【声明】内容源于网络
0
0
索引目录
索引目录是一家专注于医疗、技术开发、物联网应用等领域的创新型公司。我们致力于为客户提供高质量的服务和解决方案,推动技术与行业发展。
内容 444
粉丝 0
索引目录 索引目录是一家专注于医疗、技术开发、物联网应用等领域的创新型公司。我们致力于为客户提供高质量的服务和解决方案,推动技术与行业发展。
总阅读12
粉丝0
内容444