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

สร้าง Session Store ที่ปลอดภัยและ Scalable ด้วย Clerk และ Upstash Redis

Use Case หลักของ Redis คือการเก็บและจัดการ User Sessions เพื่อ Maintain State ระหว่าง Requests ใน Web Apps สามารถทำได้หลายวิธี และเครื่องมือ Serverless ใหม่ๆ มี Option ง่ายๆ สำหรับ Deploy

การจัดการ User Session Data สำคัญต่อ Business Apps หลายอย่าง เช่น Personalization Platforms ใช้ Redis เก็บ Interactions และ Preferences เพื่อเสนอ Content/Products แบบ Customized ใน Gaming ใช้ Redis จัดการ User Data ให้ Multiplayer Smooth ด้วย Real-time Tracking Advertising Platforms ใช้ Redis เก็บ Session Data เพื่อ Optimize Ad Delivery และ Personalize Campaigns ต่อไป ตัวอย่างนี้จะเน้น E-commerce Application สำรวจการใช้ Redis จัดการ Shopping Carts อย่างมีประสิทธิภาพ

Project Description - รายละเอียดโปรเจกต์

ใน Blog Post นี้เราจะใช้ Clerk, Next.js และ Upstash Redis สร้าง Session Store สำหรับ Shopping Application Features ที่จะ Implement:

  • Users Sign-up, Sign-in, Sign-out
  • แต่ละ User Add/Remove Items ใน Shopping Cart เฉพาะตัว
  • Users Update Item Quantities ใน Cart

Features อื่นๆ เกี่ยวกับ QStash และ Upstash Ratelimit:

  • Actions บางอย่าง Trigger Events Schedule Emails ผ่าน QStash ส่งโดย Resend เช่น Checkout → Schedule Shipping Confirmation Email 24 ชม. ซื้อ Item → Prompt ให้ Rate หลัง Delay
  • Users Rate Items เก็บ Rating Data ใน Data Structures Upstash Redis ใช้ Upstash Ratelimit ควบคุม Flow ป้องกัน Misuse

Demo - สาธิต

Demo ที่ Deploy ไว้ที่นี่

GitHub Repository ที่นี่

Creating the Next.js Application - สร้าง Next.js App

เปิด Terminal สร้าง App:

npx create-next-app@latest

เลือก Options ได้ Template พร้อม:

npx create-next-app@latest
Need to install the following packages: create-next-app@13.4.18
Ok to proceed? (y) y
✔ What is your project named? shopstash
✔ Would you like to use TypeScript? No / -> Yes
✔ Would you like to use ESLint? No / -> Yes
✔ Would you like to use Tailwind CSS? No /> Yes
✔ Would you like to use `src/` directory? -> No / Yes
✔ Would you like to use App Router? (recommended) No / -> Yes
✔ Would you like to customize the default import alias? -> No / Yes
Creating a new Next.js app in /Users/***/shopstash.

Integrating Clerk - ผสาน Clerk

เพิ่ม Clerk ง่ายด้วย Next.js SDK Prebuilt Components/Hooks ติดตั้ง:

npm install clerk@nextjs

สร้าง App บน Clerk Dashboard Config Sign-up Info ตามต้องการ ได้ Credentials ใส่ .env.local:

NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_********
CLERK_SECRET_KEY=sk_test_********

Config Paths Clerk ใน .env.local:

NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/
NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/

Wrap Root Layout ด้วย <ClerkProvider> ใช้ Active Session/User Contexts จะ Implement Header Component ต่อไป:

import { ClerkProvider } from "@clerk/nextjs";
import Header from "./components/Header";
export default function RootLayout({ children, }: { children: React.ReactNode; }) {
  return (
    <ClerkProvider>
      <html lang="en">
        <body className="bg-white">
          <Header />
          <main className="container bg-white">
            <div className="flex min-h-screen items-start justify-center ">
              <div className="mt-5">{children}</div>
            </div>
          </main>
        </body>
      </html>
    </ClerkProvider>
  );
}

ตอนนี้ Clerk ติดตั้งแล้ว ตัดสินใจซ่อน Page ไหนไว้หลัง Authentication ใน middleware.tsx Root Folder:

import { authMiddleware } from "@clerk/nextjs";
export default authMiddleware({});
export const config = {
  matcher: ["/((?!.*\\..*|_next).*)", "/", "/(api|trpc)(.*)"],
};

App ทั้งหมด Protected เข้าหน้าไหนไม่ Sign In Redirect ไป Index Authenticate

ตอนนี้ต้องการ Sign-up/Sign-in Pages Navigation จาก Header Render ตาม Active User ถ้ามี User → Sign-out + Profile ไม่มี → Sign-in/Sign-up Routes

Header State ไม่มี Active User:

สร้าง Session Store ที่ปลอดภัยและ Scalable ด้วย Clerk และ Upstash Redis

สร้าง Routes สำหรับ User Actions ใช้ Built-in Clerk Components (Customize ได้) Sign-up สร้าง app/sign-in/[[...sign-up]]/page.tsx:

import { SignUp } from "@clerk/nextjs";
const SignUpPage = () => {
  return ( <> <SignUp /> </> );
};
export default SignUpPage;

Sign-in คล้ายกัน สร้าง app/sign-in/[[...sign-in]]/page.tsx:

import { SignIn } from "@clerk/nextjs";
const SignInPage = () => {
  return ( <> <SignIn /> </> );
};
export default SignInPage;

Clerk Integrated แล้ว พร้อมสร้าง Unique Session Store บน Redis ต่อ User ดึง User ID จาก Clerk เก็บ Session Data บน Upstash Redis ตัวอย่าง Shopping Cart แต่ละ Session เก็บ Cart Items

Concept Cart Item ก่อน Blueprint สร้าง App ต่อ หากต้องการ Items หลากหลาย ChatGPT ช่วยได้ หรือดูจาก GitHub Repo ต้องมี Images เหมาะสมต่อ Item

export const items = [
  { id: 1, title: "Elegant Leather Watch", image: "/images/1.png", description: "A sophisticated leather watch for all occasions.", company: "Timepiece Creations", price: 99.99 },
];

Implementing Shopping Cart - Implement Cart

Cart ควร Accessible จากหลาย Section ให้ Components Render ตาม Cart Contents เช่น Item Detail Page หรือ List Page เห็นว่า Product อยู่ใน Cart แล้วหรือยัง

สร้าง Session Store ที่ปลอดภัยและ Scalable ด้วย Clerk และ Upstash Redis

ใช้ React Context API ให้ Access Cart Operations (Add, Remove, Reset) แบบ Semi-global

Setup Upstash Redis Copy UPSTASH_REDIS_REST_URL และ UPSTASH_REDIS_REST_TOKEN จาก Console ใส่ .env:

UPSTASH_REDIS_REST_URL=<YOUR_URL>
UPSTASH_REDIS_REST_TOKEN=<YOUR_TOKEN>

Cart Context ใน app/context/CartContext.tsx Wrap Context รอบ App หลัก ใช้ Methods ได้Capabilities:

  • Add Items และ Adjust Quantities
  • Remove Items
  • Reset Cart
  • Checkout Feature

Algorithm Overview:

  • Cart เป็น Session Store เก็บใน Hash บน Upstash Redis Key Unique จาก User ID Format cart:<USER_ID>
  • Save Cart Data ใช้ Item IDs เป็น Keys Quantities เป็น Values Redis Commands ทำให้ Modify Cart ง่าย
  • Client-side Cart State เป็น Array ของ Item Objects Load ข้อมูลจาก Upstash Redis ใน useEffect ระหว่าง Load Page Changes Trigger Re-render
  • Redis Data Structures ทำ addItem removeItem ง่าย ใช้ redis.hincrby() จัดการ Add/Remove/Adjust Quantities
  • resetCart ลบ Hash Key ด้วย redis.del()

Adding Item to Cart - เพิ่ม Item เข้า Cart

จากมุมมอง Redis Hash Add Item หรือ Change Quantity ใช้ Command เดียวกัน hincrby สร้าง Key Set Value 1 หรือ Increase ตาม increment Parameter

Client-side Mirror Actions เพิ่ม Item ใหม่ หรือ Adjust Quantity ใน Cart State

const addItem = async (id: number) => {
  const item = items.find((i) => i.id === id);
  if (!item) return;
  const doesItemExist = cart.some((i) => { return id === i.id; });
  let newCart: Item[];
  if (!doesItemExist) {
    newCart = [...(cart || []), item];
    redis.hincrby(`user:${userId}`, id.toString(), 1);
    const newCartItemIDs = { ...cartItems, [id]: 1 };
    setCartItemIDs(newCartItemIDs);
    setCart(newCart);
  } else {
    const item = items.find((i) => i.id === id);
    const updatedItemQuantities = { ...cartItems, [id]: cartItems[id] + 1 };
    setCartItems(updatedItemQuantities);
    redis.hincrby(`user:${userId}`, id.toString(), 1);
  }
};

Removing Item from the Cart - ลบ Item ออกจาก Cart

Remove คล้าย Add ใช้ hincrby ให้ increment เป็น -1

const removeItem = async (id: number, force: boolean = false) => {
  const doesItemExist = cart.some((i) => { return id === i.id; });
  if (!doesItemExist) return;
  if (cartItems[id] === 1 || force) {
    const newCart: Item[] = cart.filter((item: { id: number }) => { return item.id !== id; });
    const newCartItems = { ...cartItems };
    delete newCartItems[id];
    redis.hdel(`user:${userId}`, id.toString());
    setCart(newCart);
    setCartItems(newCartItems);
  } else if (cartItems[id] > 1) {
    const updatedItemQuantities = { ...cartItems, [id]: cartItems[id] - 1 };
    setCartItems(updatedItemQuantities);
    redis.hincrby(`user:${userId}`, id.toString(), -1);
  }
};

Cart Functionality พร้อม Methods ทั้งหมด Integrated ผ่าน Project Global Access ตัวอย่าง Use Cases:

  • Index Page: แสดง Items ทั้งหมด พร้อม Buttons Add/Remove หากอยู่ใน Cart แล้ว
  • ใช้ shadcnui React UI Library สร้าง Modal/Sheet รวม Cart Items หน้าเดียว Adjust Quantities, Reset, Checkout

Index Page - หน้าหลัก

Items แสดงเฉพาะ Active Signed-in Users Fetch User Data จาก Clerk Render Components ตาม Response

Card Component ดึง Functions/Objects จาก CartContext:

export default function CardComponent(props: { item: cardProps }) {
  const { item } = props;
  const { id, title, image, company } = item;
  const { addItem, removeItem, cartItems } = useContext(CartContext);
  return (
    <>
      <Card className="transition duration-200 hover:shadow-lg">
        <Link href={`/products/${id}`}>
          <CardHeader>
            <CardTitle>{title}</CardTitle>
          </CardHeader>
          <CardContent>
            <Image src={image} alt={title} width={300} height={300}></Image>
            <CardDescription>{company}</CardDescription>
          </CardContent>
        </Link>
        <CardFooter>
          <div className="grid grid-rows-2">
            <CartButton id={id} cartItems={cartItems} addItem={addItem} removeItem={removeItem} />
          </div>
        </CardFooter>
      </Card>
    </>
  );
}

Cart Button สำคัญ Render ตาม Cart State เพิ่ม/ลบ Item:

const CartButton = ({ id, cartItems, addItem, removeItem, }: { id: number; cartItems: cartContent; addItem: (id: number) => Promise<void>; removeItem: (id: number, force: boolean) => Promise<void>; }) => {
  const itemExists: boolean = cartItems?.hasOwnProperty(id);
  const { triggerEvent } = useContext(UserStateContext);
  return (
    <button className={`${ itemExists ? "bg-red-400 text-black" : "bg-cyan-500 text-black" } flex items-center justify-center gap-3 rounded-full px-4 py-2 transition-all duration-300`} onClick={() => { if (itemExists) { removeItem(id, true); } else { addItem(id); } }} >
      <p className="text-sm font-bold"> {itemExists ? "Remove from Cart" : "Add to Cart"} </p>
      <FaCartShopping size="25" />
    </button>
  );
};

Cart State Dynamic Prompt Component Render ตาม Status สร้าง Dedicated Cart Component ดู Items ทั้งหมด Adjust Quantities Reset

ใช้ shadcn/ui Sheet Component Base Customize Interior

Cart ว่าง:

สร้าง Session Store ที่ปลอดภัยและ Scalable ด้วย Clerk และ Upstash Redis

Cart มี Items เห็น Selections Adjust Quantities ดู Total Value:

สร้าง Session Store ที่ปลอดภัยและ Scalable ด้วย Clerk และ Upstash Redis

Full Walkthrough Component Creation ดูด้านล่าง รายละเอียด Button Configs ดู GitHub Repo

โปรเจกต์เสร็จสิ้น สร้าง Cart Feature ด้วย Upstash Redis และ shadcn/ui

Conclusion - สรุป

Clerk จัดการ User + Upstash Redis เก็บข้อมูลประสิทธิภาพ = Backbone ของ Dynamic Cart System ทั้ง Security และ Performance ตัวอย่างเครื่องมือ Upstash Redis, Clerk ร่วมกันแก้ปัญหาซับซ้อนได้ลื่นไหล

แนะนำปรับปรุงเพิ่มเติม:

  • User Experience: Animations, Feedback Loops, Detailed Product Previews
  • Performance: Advanced Caching Strategies, Service Workers สำหรับ Load Times และ Offline Experience
  • Features: Wishlists, Recommendations ตาม Cart Contents, Promo Codes
  • Integration: Payment Gateways Checkout Smooth, Third-party Inventory/CRM Systems

ขอบคุณที่ติดตาม 歡迎 Feedback และ Innovations จากคุณ! คำถาม/ปัญหา ติดต่อ fahreddin@upstash.com

GitHub Repository ที่นี่

ส่วน Schedule Emails ด้วย QStash และ Resend จะครอบคลุมใน Post ถัดไป ตอนนี้ดู Example Repo ได้