在Next.js中实现一个完美的点赞按钮并不容易。
但是通过结合tRPC内置的乐观更新功能和lodash的防抖能力,我解决了这些问题,打造了_完美的Next.js点赞按钮_,今天我将向您展示如何做到这一点。
所以,不多说了... 让我们开始吧!
认证是区分不同用户点赞的必要条件。
没有认证,无法统计点赞数量并将点赞与个别用户关联起来。
如果您不知道如何在Next.js中设置认证,我在下面的文章中提供了一些帮助,可以帮助您使用非常少的代码集成Auth.js:
tRPC是一个API包装器,利用React Query为您提供强大的数据获取机制,例如:
-
乐观更新 - 提供onMutate函数,让您在触发API请求之前更新UI。
-
数据验证 - 通过tRPC端点提供经过验证的输入并接收经过验证的输出,无需额外开销。
这只是使用tRPC的众多原因之一。但是,您也可以选择使用**Server Actions或基本的Route Handlers**,但这将需要您更多地“重新发明轮子”。
请参考下面的文章,了解如何在Next.js 14中配置tRPC:
让我们首先创建一个数据库模式,用于在服务器上存储点赞数据。
使用您选择的数据库和ORM,尝试复制下面示例中模式的语义:
// schema.ts
import { integer, primaryKey, sqliteTable, text } from "drizzle-orm/sqlite-core"
import { randomUUID } from "crypto"
export const articleLikes = sqliteTable("articleLikes", {
id: text("id", { length: 255 })
.notNull()
.primaryKey()
.$defaultFn(() => randomUUID()),
userId: text("userId", { length: 255 }).notNull(),
articleSlug: text("articleSlug", { length: 255 }).notNull(),
createdAt: text("createdAt").default(sql`CURRENT_TIMESTAMP`),
})
我使用的是**Turso,一个使用Drizzle ORM封装的sqlite**数据库,用于定义上述表。
请记住,此示例设计用于我的个人博客,这意味着您可以省略articleSlug
属性。
您可以将其替换为任何相关属性,例如社交媒体帖子的
postId
。
现在,我们需要两个tRPC过程来使我们的点赞按钮功能正常工作:
-
一个过程用于获取当前点赞数量。
-
一个过程用于更新当前点赞数量。
我将逐步解释每个过程,以便易于理解。
注意:这是我为博客实现的代码。您可能需要替换一些变量,使其适用于“社交媒体应用”等。
// src/routers/blogRouter.ts
getArticleLikeData: publicProcedure
.input(z.object({ slug: z.string(), session: z.custom<Session | null>() }))
.query(async ({ input }): Promise<LikeData> => {
const currentLikes = (
await db
.select()
.from(articleLikes)
.where(eq(articleLikes.articleSlug, input.slug))
).length
const articleLiked = (
await db
.select()
.from(articleLikes)
.where(
and(
eq(articleLikes.articleSlug, input.slug),
eq(articleLikes.userId, input.session?.user.id ?? "")
)
)
).length
return { likes: currentLikes, isLiked: !!articleLiked }
}),
-
使用zod模式验证
input
参数。 -
接受博客文章的
slug
以及Auth.js的session
。 -
查询
articleLikes
表,获取此文章的currentLikes
。 -
执行另一个查询,确定此用户是否已点赞该文章,然后将其存储在
articleLiked
中(为0或1)。 -
返回
currentLikes
和布尔值isLiked
(使用!!
运算符从0=false或1=true获取真值)。
// src/routers/blogRouter.ts
updateArticleLikes: publicProcedure
.input(
z.object({
slug: z.string(),
session: z.custom<Session | null>(),
isLiked: z.boolean(),
})
)
.mutation(async ({ input }) => {
if (input.isLiked) {
await db
.delete(articleLikes)
.where(
and(
eq(articleLikes.articleSlug, input.slug),
eq(articleLikes.userId, input.session?.user.id!)
)
)
return 0
} else {
await db.insert(articleLikes).values({
articleSlug: input.slug,
userId: input.session?.user.id!,
})
return 1
}
}),
-
再次以
slug
和session
作为输入,但这次还传入与[getArticleLikeData](#b01c)
相关的isLiked
状态。 -
如果点赞按钮被“点赞”,则删除与当前用户和当前文章对应的点赞条目。
-
如果点赞按钮未被“点赞”,则为当前文章和当前用户插入一个新条目。
最后,我们需要与我们的tRPC过程交互,并提取渲染在LikeButton
组件中所需的数据。
我的实现非常长,所以我将抽象并保留最重要的部分:
import { useEffect, useState } from "react"
import { debounce } from "lodash"
import { Heart } from "lucide-react"
import { Session } from "next-auth"
// import ...
// 从服务器组件获取数据以作为初始props
type LikeData = { likes: number; isLiked: boolean }
interface LikeButtonProps {
currentSlug: string
session: Session | null
initialLikesData: LikeData
}
export const LikeButton = ({
currentSlug,
session,
initialLikesData,
}: LikeButtonProps) => {
// tRPC过程,用于获取当前文章和用户的点赞数据。
const { data } = trpc.blogRouter.getArticleLikeData.useQuery(
{ slug: currentSlug, session },
{
refetchOnMount: false,
refetchOnReconnect: false,
refetchOnWindowFocus: false,
initialData: initialLikesData,
}
)
// 我们管理本地状态,以允许对UI进行乐观更新。
const [likesData, setLikesData] = useState(data)
// 将客户端状态与服务器数据同步
useEffect(() => {
setLikesData(data)
}, [data])
// tRPC过程,用于更新当前文章和用户的点赞数据。
const { mutateAsync: updateLikeCount } =
trpc.blogRouter.updateArticleLikes.useMutation({
onMutate: () => {
// 根据状态乐观更新UI上的点赞数量。
if (likesData!.isLiked) {
setLikesData((prev) => ({ isLiked: false, likes: prev!.likes - 1 }))
} else {
setLikesData((prev) => ({ isLiked: true, likes: prev!.likes + 1 }))
}
},
})
return (
<div className="flex items-center">
<Popover>
{session ? (
<Button
variant="ghost"
size="icon"
onClick={debounce(async () => {
// 防抖250ms,以防止API过载
await updateLikeCount({
session,
slug: currentSlug,
isLiked: likesData!.isLiked,
})
}, 250)}
>
<Heart fill={likesData?.isLiked ? "red" : "none"} />
</Button>
) : (
{/* 此按钮打开`Popover` */}
<PopoverTrigger asChild>
<Button variant="ghost" size="icon">
<Heart fill={likesData?.isLiked ? "red" : "none"} />
</Button>
</PopoverTrigger>
)}
{/* 告诉用户登录 */}
<PopoverContent className="flex flex-col text-center space-y-4 border-muted-foreground/50">
<Label>登录以❤️</Label>
<AuthButton session={session} />
</PopoverContent>
</Popover>
<div>{likesData?.likes ?? "0"}</div>
</div>
)
}
-
我使用shadcn-ui中的
Popover
来有条件地渲染AuthButton
,如果用户尚未登录,则告诉用户“登录以❤️”。 -
我使用**lodash中的
debounce
函数,在进行API调用时提供250ms
的防抖延迟**。了解更多关于防抖的信息。
这是最终结果:
您可以放心,后端工作得很出色:
哇!这真是一大堆内容,我希望有足够的信息让您适应我的代码并将其重新用于您的Next.js应用程序。此外,您可能希望通过nodemailer在按下点赞按钮时发送电子邮件,这将提供有用的分析数据。