关注【索引目录】服务号,更多精彩内容等你来探索!
实时评论和 @提及等互动功能已成为现代 Web 应用中不可或缺的一部分。用户如今期望实时更新和无缝协作,因此这些工具对于保持用户参与度并让您的应用脱颖而出至关重要。
但实时评论和提及功能不仅能创造更具互动性的体验,还能在用户群中建立真正的社区感。当人们能够在你的平台上讨论内容、分享反馈并互相帮助时,他们就再也不想离开了。
不相信?去 Reddit 问问!
简而言之,如果您想提高用户参与度和忠诚度,添加实时评论是明智之举。
在本文中,我们将向您展示如何使用Next.js、 Prisma、Radix UI、 Clerk Auth和Velt构建实时评论系统,以实现实时协作功能。
您将了解:
-
如何简化身份验证流程 -
如何处理数据流 -
如何实时提供更新。
最后,您将拥有一个现代协作平台的坚实框架,该平台可以轻松扩展并为您的用户提供引人入胜的体验。
最终的应用程序登陆页面将如下所示:
但首先,让我们看看从头开始构建这些功能时可能面临的挑战。
从头开始使用 @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、 Prisma、Radix UI、 Clerk Auth和Velt构建了一个将安全身份验证与实时协作相结合的评论系统。虽然这是一个简单的演示,但您可以使用这些工具在其基础上构建您的项目。
在坚实的基础之上,我们可以根据需要进一步增强评论系统。例如,我们可以添加对更深层次的评论线索(回复回复)的支持,或编辑和删除评论等审核功能。我们或许可以实现高级通知选项(例如,提及时的电子邮件通知,或将通知标记为已读的功能)。
在性能方面,随着评论数量的增长,我们应该考虑缓存和分页功能来加载评论。我们已经为通知添加了分页参数;同样,如果一个页面积累了数千条评论,我们应该分块加载,并缓存最近的评论以便快速检索。
关注【索引目录】服务号,更多精彩内容等你来探索!

