Computer >> บทช่วยสอนคอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Redis

เพิ่ม View Counter ให้บล็อก Next.js ด้วย Upstash Redis

ได้แรงบันดาลใจจากบล็อกของ Lee ที่แสดงจำนวน views ของแต่ละบทความ ผมจึงอยากทำแบบเดียวกันสำหรับเพจของตัวเอง ผมใช้ Next.js 13 พร้อม app router ใหม่ แต่แทนที่จะเก็บ page views ใน MySQL จะใช้ Upstash Redis แทน

นี่คือตัวอย่างสิ่งที่เราจะสร้าง แต่ละการ์ดบนหน้าแรกจะแสดงจำนวน views

เพิ่ม View Counter ให้บล็อก Next.js ด้วย Upstash Redis

ทำไมถึงเลือก Redis?

Redis มาพร้อมกับ 2 คำสั่งยอดเยี่ยมที่ทำให้การ deduplicate และการเพิ่มตัวนับเป็นเรื่องง่าย

เพื่อให้ตัวนับแม่นยำขึ้น ผมอยาก debounce การเพิ่มตัวนับ หากผู้ใช้ refresh หน้า ตัวนับควรเพิ่มแค่ครั้งเดียว ทำได้ง่ายด้วยคำสั่ง SET ของ Redis ที่มีตัวเลือก NX (set key เฉพาะเมื่อยังไม่มี) และ EX (หมดอายุหลังจากวินาทีที่กำหนด) รวมทั้งสองตัวเลือกนี้ เราสามารถรับรองได้ว่าผู้ใช้คนเดียวไม่จะเพิ่มตัวนับซ้ำภายในเวลาที่กำหนด (เช่น 24 ชั่วโมง)

คำสั่งที่สองคือ INCR ซึ่งเพิ่มค่าของ key ที่กำหนดทีละ 1 แบบ atomic

ตั้งค่า Redis

การตั้งค่าฐานข้อมูลบน Upstash ง่ายและมี free tier 10,000 requests ต่อวัน! สร้างฐานข้อมูลใหม่ ที่นี่ ใช้เวลาเพียงไม่กี่วินาที หลังจากนั้นเลื่อนลงและคัดลอก REST connection secrets ไปไว้ในไฟล์ .env.local:

# .env.local
UPSTASH_REDIS_REST_URL=
UPSTASH_REDIS_REST_TOKEN=

Next.js

ตอนนี้มี Redis database แล้ว เริ่ม implement counter ได้ ติดตั้ง @upstash/redis ก่อน:

pnpm add @upstash/redis

เพื่อเก็บ page views เราต้องการ 2 คอมโพเนนต์: API route และ client component เริ่มจาก API route ก่อน

/api/incr.ts

Upstash และ @upstash/redis เข้ากันได้กับ Vercel edge functions ก่อนอื่น import ทุกอย่างที่ต้องการ setup redis และตั้งค่า runtime เป็น edge

สร้างไฟล์ /api/incr.ts ใส่โค้ดดังนี้:

// /api/incr.ts
import { NextRequest, NextResponse } from "next/server";
import { Redis } from "@upstash/redis";

const redis = Redis.fromEnv();

export const config = {
  runtime: "edge",
};

ต่อไป ต้องการ slug หรือ identifier ใน request body หากไม่มี ส่ง status 400 กลับไป

// /api/incr.ts
export default async function incr(req: NextRequest): Promise<NextResponse> {
  const body = await req.json();
  const slug = body.slug as string | undefined;
  if (!slug) {
    return new NextResponse("Slug not found", { status: 400 });
  }
  // more to come here
}

จากนั้นต้องการ IP address ของผู้ใช้ ทำได้ผ่าน req.ip จะ hash IP ด้วยอัลกอริทึม SHA-256 และเก็บในฐานข้อมูล วิธีนี้ไม่ต้องเก็บ IP โดยตรง ซึ่งอาจเป็นประเด็นความปลอดภัย

// /api/incr.ts
const ip = req.ip;
// Hash the IP and turn it into a hex string
const buf = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(ip));
const hash = Array.from(new Uint8Array(buf))
  .map((b) => b.toString(16).padStart(2, "0"))
  .join("");

ตอนนี้ใช้คำสั่ง Redis แรก รวม SET กับ NX และ EX เพื่อเช็คว่า IP เฉพาะได้ดูหน้านี้ภายใน 24 ชั่วโมงที่ผ่านมาหรือยัง:

// /api/incr.ts
const isNew = await redis.set(["deduplicate", hash, slug].join(":"), true, {
  nx: true,
  ex: 24 * 60 * 60,
});
if (!isNew) {
  new NextResponse(null, { status: 202 });
}

สิ่งสุดท้ายคือเพิ่มตัวนับสำหรับ slug ที่กำหนด ใช้คำสั่ง INCR:

// /api/incr.ts
await redis.incr(["pageviews", "projects", slug].join(":"));
return new NextResponse(null, { status: 202 });

สำหรับอ้างอิง โค้ดเต็มดูได้ ที่นี่

/app/[slug]/view.tsx

ต่อไป สร้าง client component เล็กๆ ที่ส่ง request ไปยัง api route ที่สร้างไว้ทุกครั้งที่ mount คอมโพเนนต์นี้สามารถ embed ในหน้าไหนก็ได้ที่ต้องการติดตาม

// /app/[slug]/view.tsx
"use client";
import { useEffect } from "react";

export const ReportView: React.FC<{ slug: string }> = ({ slug }) => {
  useEffect(() => {
    fetch("/api/incr", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ slug }),
    });
  }, [slug]);
  
  return null;
};

/app/[slug]/page.tsx

สิ่งสุดท้ายคือเพิ่มคอมโพเนนต์ ReportView ลงในหน้าที่ต้องการติดตาม:

// /app/[slug]/page.tsx
import { ReportView } from "./view";

type Props = {
  params: { slug: string; };
};

export default function Page({ params }: Props) {
  return (
    <div>
      <ReportView slug={params.slug} />
      {/* Add your page content here */}
    </div>
  );
}

ตอนนี้ การเข้าชม /app/[slug] ทุกครั้งจะถูกติดตามและตัวนับจะเพิ่มสำหรับแต่ละผู้เยี่ยมชมที่ไม่ซ้ำกันใน 24 ชั่วโมงล่าสุด

แสดงจำนวน Views

ติดตาม views แล้วแต่เราก็อยากแสดงออกมาด้วย มาดูวิธีทำกัน

เพื่อแสดงจำนวน views ต้องดึงข้อมูลจากฐานข้อมูล ใช้คำสั่ง GET ควรเพิ่ม config revalidate ให้ page component ด้วย เพื่อให้หน้า revalidate ทุก 60 วินาที ไม่ใช่ทุก request

// /app/[slug]/page.tsx
type Props = {
  params: { slug: string; };
};

export const revalidate = 60

export default function Page({ params }: Props) {
  const views = await redis.get<number>(["pageviews", "projects", params.slug].join(":")) ?? 0
  return ...
}

สรุป

ดูตัวอย่างเต็มได้ที่ chronark.com โค้ดอยู่บน GitHub โดยเฉพาะส่วนต่อไปนี้:

  • api route
  • tracking component
  • loading the number of views

สนใจ page view analytics เพิ่มเติม? แจ้งเราทราบทาง Twitter หรือ Discord นะครับ