Computer >> Hướng Dẫn Máy Tính >  >> Lập Trình >> Redis

Xây dựng Waiting Room có khả năng mở rộng cho Next.js App với Edge Functions & Upstash Redis

Trong bài này, chúng ta tạo waiting room cho ứng dụng Next.js dùng Vercel Edge functions và Upstash Redis.

Xem mã nguồn và demo app.

Waiting Room?

Waiting room hữu ích khi muốn giới hạn số lượng active visitors đến website để tránh quá tải tài nguyên.

Trong triển khai này, bạn set maximum active visitors. Có 2 tham số kiểm soát traffic:

  • Max website capacity: Số visitor tối đa trên website cùng lúc?
  • Max session timeout: Số giây tối đa visitor có thể ở idle

Bước 1: Thiết lập dự án

Tạo Next.js app:

examples git:(master) ✗ npx create-next-app@latest --typescript
✔ What is your project named? … nextjs-waiting-room
Creating a new Next.js app in /Users/enes/dev/examples/nextjs-waiting-room.

Cài upstash-redis:

npm install @upstash/redis

Bước 2: Triển khai

Vercel hỗ trợ Edge functions qua Next.js middleware. Thêm _middleware.ts dưới pages/api/. Middleware code intercept tất cả requests đến /api. Xem cấu hình khác tại đây.

Cập nhật pages/api/_middleware.ts:

import { Redis } from "@upstash/redis";
import { NextFetchEvent, NextRequest, NextResponse } from "next/server";

const COOKIE_NAME_ID = "__waiting_room_id";
const COOKIE_NAME_TIME = "__waiting_room_last_update_time";
const UPSTASH_REDIS_REST_TOKEN = "REPLACE_HERE";
const UPSTASH_REDIS_REST_URL = "REPLACE_HERE";
const TOTAL_ACTIVE_USERS = 10;
const SESSION_DURATION_SECONDS = 30;

const redis = new Redis({
  url: UPSTASH_REDIS_REST_URL,
  token: UPSTASH_REDIS_REST_TOKEN,
});

export async function middleware(req: NextRequest, ev: NextFetchEvent) {
  const cookies = req.cookies;
  let userId;
  if (cookies[COOKIE_NAME_ID] != null) {
    userId = cookies[COOKIE_NAME_ID];
  } else {
    userId = makeid(8);
  }

  const size = await redis.dbsize();
  console.log("current capacity:" + size);

  // đủ capacity
  if (size < TOTAL_ACTIVE_USERS) {
    return getDefaultResponse(req, userId);
  } else {
    // site capacity full
    const user = await redis.get(userId);
    if (user === "1") {
      // user đã có active session
      return getDefaultResponse(req, userId);
    } else {
      // capacity full, forward user đến waiting room
      return getWaitingRoomResponse();
    }
  }
}

function makeid(length: number) {
  let result = "";
  const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
  const charactersLength = characters.length;
  for (let i = 0; i < length; i++) {
    result += characters.charAt(Math.floor(Math.random() * charactersLength));
  }
  return result;
}

async function getDefaultResponse(request: NextRequest, userId: string) {
  // uncomment dưới để test với static html content
  let newResponse = new NextResponse(default_html);
  newResponse.headers.set("content-type", "text/html;charset=UTF-8");
  // const response = await fetch(request)
  // const newResponse = new Response(response.body, response)

  const cookies = request.cookies;
  const now = Date.now();
  let lastUpdate = cookies[COOKIE_NAME_TIME];
  let lastUpdateTime = 0;
  if (lastUpdate) lastUpdateTime = parseInt(lastUpdate);

  const diff = now - lastUpdateTime;
  const updateInterval = (SESSION_DURATION_SECONDS * 1000) / 2;

  if (diff > updateInterval) {
    await redis.setex(userId, SESSION_DURATION_SECONDS, "1");
    newResponse.cookies.set(COOKIE_NAME_TIME, now.toString());
  }
  newResponse.cookies.set(COOKIE_NAME_ID, userId);
  return newResponse;
}

async function getWaitingRoomResponse() {
  const newResponse = new NextResponse(waiting_room_html);
  newResponse.headers.set("content-type", "text/html;charset=UTF-8");
  return newResponse;
}

const waiting_room_html = `
<title>Waiting Room</title>
<meta http-equiv='refresh' content='30' />
<style>*{box-sizing:border-box;margin:0;padding:0}body{line-height:1.4;font-size:1rem;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif;padding:2rem;display:grid;place-items:center;min-height:100vh}.container{width:100%;max-width:800px}p{margin-top:.5rem}</style>
<div class='container'>
  <h1>
    <div>You are now in line.</div>
    <div>Thanks for your patience.</div>
  </h1>
  <p>We are experiencing a high volume of traffic. Please sit tight and we will let you in soon. </p>
  <p><b>This page will automatically refresh, please do not close your browser.</b></p>
</div>
`;

const default_html = `
<title>Waiting Room Demo</title>
<style>*{box-sizing:border-box;margin:0;padding:0}body{line-height:1.4;font-size:1rem;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif;padding:2rem;display:grid;place-items:center;min-height:100vh}.container{width:100%;max-width:800px}p{margin-top:.5rem}</style>
<div class="container">
  <h1>
    <div>Waiting Room Demo</div>
  </h1>
  <p> Visit this site from a different browser, you will be forwarded to the waiting room when the capacity is full. </p>
  <p> Check <a href='//github.com/upstash/redis-examples/tree/master/nextjs-waiting-room' style={{"color": "blue"}}>this project </a> to set up a waiting room for your website.</p>
</div>
`;

Dùng Upstash Redis làm state store giữ active user sessions. Nhờ REST API, upstash-redis tương thích Vercel Edge functions.

Cần tạo Global database từ Upstash console. Sao chép/dán REST token và REST URL từ console. Redis database nên trống và chỉ dùng cho app này.

Cũng set TOTAL_ACTIVE_USERS và SESSION_DURATION_SECONDS theo yêu cầu riêng.

App tạo unique id cho visitor mới, set làm cookie và push vào Redis. Lần sau, app check nếu visitor đã có session check Redis. Khi insert vào Redis, set expiration time làm session idle timeout. Nếu số sessions vượt max-capacity, user mới forward đến waiting room page.

Cập nhật waiting_room_html để customize waiting room page.

Cập nhật method getDefaultResponse() để forward đến trang riêng dùng NextResponse.

Bước 3: Chạy và Deploy

Chạy local bằng npm run dev. Muốn test dễ, set 1 cho TOTAL_ACTIVE_USERS và mở trang (https://localhost:3000/api/hello) trong trình duyệt khác nhau để test waiting room.

Deploy lên Vercel:

vercel deploy --prod

Vercel chạy _middleware.ts ở edge locations để minimize latency globally.

Kết luận

Tutorial này showcase việc xây dựng dynamic application at edge dễ dàng nhờ Vercel và Upstash. Xem examples cho thêm ví dụ.

Chờ feedback của bạn trên Twitter hoặc Discord.