首页
开发技巧正文内容

完美的Next.js点赞按钮

2024年03月17日
阅读时长 2 分钟
阅读量 36
完美的Next.js点赞按钮

在Next.js中实现一个完美的点赞按钮并不容易。

但是通过结合tRPC内置的乐观更新功能和lodash的防抖能力,我解决了这些问题,打造了_完美的Next.js点赞按钮_,今天我将向您展示如何做到这一点。

所以,不多说了... 让我们开始吧!


认证

认证是区分不同用户点赞的必要条件。

没有认证,无法统计点赞数量并将点赞与个别用户关联起来。

如果您不知道如何在Next.js中设置认证,我在下面的文章中提供了一些帮助,可以帮助您使用非常少的代码集成Auth.js

两行代码实现认证:Auth.js + Next.js 14

tRPC

tRPC是一个API包装器,利用React Query为您提供强大的数据获取机制,例如:

  • 乐观更新 - 提供onMutate函数,让您在触发API请求之前更新UI。

  • 数据验证 - 通过tRPC端点提供经过验证的输入并接收经过验证的输出,无需额外开销。

这只是使用tRPC的众多原因之一。但是,您也可以选择使用**Server Actions或基本的Route Handlers**,但这将需要您更多地“重新发明轮子”。

请参考下面的文章,了解如何在Next.js 14中配置tRPC:

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过程来使我们的点赞按钮功能正常工作:

  1. 一个过程用于获取当前点赞数量

  2. 一个过程用于更新当前点赞数量

我将逐步解释每个过程,以便易于理解。

注意:这是我为博客实现的代码。您可能需要替换一些变量,使其适用于“社交媒体应用”等。

获取点赞数量

// 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 }
    }),
  1. 使用zod模式验证input参数。

  2. 接受博客文章的slug以及Auth.js的session

  3. 查询articleLikes表,获取此文章的currentLikes

  4. 执行另一个查询,确定此用户是否已点赞该文章,然后将其存储在articleLiked中(为01)。

  5. 返回currentLikes和布尔值isLiked(使用!!运算符从0=false1=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
      }
    }),
  1. 再次以slugsession作为输入,但这次还传入与[getArticleLikeData](#b01c)相关的isLiked状态。

  2. 如果点赞按钮被“点赞”,则删除与当前用户当前文章对应的点赞条目。

  3. 如果点赞按钮未被“点赞”,则为当前文章当前用户插入一个新条目。

前端

最后,我们需要与我们的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在按下点赞按钮时发送电子邮件,这将提供有用的分析数据。

免责声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。

相关文章

探索多种软件架构模式及其实用应用
2024年11月22日19:06
本文深入探讨了多种软件架构模式,包括有界上下文、边车模式、发布-订阅模式、应用网关、微服务、命令职责分离(CQRS)等,介绍了它们的优点、使用场景以及具体应用实例。文章强调根据具体项目需求和团队能力选择最合适的架构,以构建高效和可维护的解决方案,同时展示了各架构模式间的综合应用,提供了丰富的案例和技术细节。
15个高级Python快捷键助您更快编程
2024年11月21日07:02
本文分享了 15 个高级的 Python 编程快捷键,包括上下文管理器、行内字典合并、函数参数解包、链式比较、dataclasses、海象运算符、反转列表、备忘录缓存、splitlines、enumerate、字典推导、zip 用于并行迭代、itertools.chain 扁平化列表、functools.partial 部分函数和 os.path 文件路径管理等,帮助开发者提高编程效率和代码简洁性。
揭示网页开发的 11 个迷思:停止相信这些误区
2024年11月19日22:05
网页开发充满误解,这篇博文针对11个常见迷思进行揭秘。包括网站开发后不需更新、需掌握所有技术、AI会取代开发者等。强调持续学习、专业化、用户体验的重要性,澄清误区如多任务处理的必要性和最新技术的必需性。文章提醒开发者注重实用而非追求完美代码,以务实态度面对开发工作。
你知道 CSS 的四种 Focus 样式吗?
2024年11月18日21:41
本文介绍了四种 CSS focus 样式::focus、:focus-visible、:focus-within 以及自定义的 :focus-visible-within,帮助提升网站用户体验。:focus 样式应用于被选中元素;:focus-visible 仅在键盘导航时显示;:focus-within 用于父元素;自定义 :focus-visible-within 结合两者效果。合理运用这些样式能使网站更方便键盘用户导航。
利用 Python 实现自动化图像裁剪:简单高效的工作流程
2024年11月11日20:49
使用 Python 和 OpenCV 自动裁剪图像,轻松实现 16:9 的完美构图。这个指南介绍了如何通过代码进行灰度化、模糊处理和边缘检测,最终识别出最重要的部分进行裁剪。特别适合需要批量处理图像的情况,节省大量时间。
每位资深前端开发人员都应了解的 TypeScript 高级概念
2024年11月11日02:07
资深前端开发者应了解 TypeScript 的高级概念,如联合类型、交叉类型、类型保护、条件类型、映射类型、模板字面量类型和递归类型。这些特性可提升代码的可维护性和可扩展性,确保在开发复杂应用时实现更高的类型安全性和效率。