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

สร้างแอปจดหมายข่าวที่ปรับขนาดได้โดยใช้ Upstash Redis และเวิร์กโฟลว์

ในบล็อกนี้ เราจะสร้างแอปจดหมายข่าวที่ผู้ใช้สามารถสมัครรับข้อมูลและเลือกความถี่ที่ต้องการรับจดหมายข่าวได้ เราจะใช้ Upstash Redis เพื่อจัดเก็บข้อมูลการสมัครสมาชิกและ Upstash Workflow เพื่อจัดการการดำเนินการจัดเก็บข้อมูล การส่งอีเมลต้อนรับ และกำหนดเวลาจดหมายข่าวตามความต้องการของผู้ใช้

แรงจูงใจ

ก่อนอื่นเลย สภาพแวดล้อมแบบไร้เซิร์ฟเวอร์นั้นยอดเยี่ยมมาก! สามารถปรับขนาดได้สูงและประหยัดงบประมาณ อย่างไรก็ตาม มีข้อจำกัดบางประการ เช่น การจำกัดเวลาดำเนินการ นี่อาจเป็นปัญหาได้โดยเฉพาะเมื่อคุณต้องการรันงานที่ต้องใช้เวลานาน

นั่นคือที่ขั้นตอนการทำงานขั้นสูง เข้ามาเล่น ด้วย Upstash Workflow คุณสามารถสร้างเวิร์กโฟลว์ถาวรที่สามารถทำงานได้นานเท่าที่ต้องการ ดังนั้นคุณจึงไม่ต้องกังวลกับการหมดเวลาของฟังก์ชันไร้เซิร์ฟเวอร์อีกต่อไป

นี่คือรายการคุณสมบัติที่คุณได้รับเมื่อใช้ Upstash Workflow:

  • ไม่มีการหมดเวลาของฟังก์ชันไร้เซิร์ฟเวอร์อีกต่อไป :เวิร์กโฟลว์ของคุณสามารถทำงานได้นานเท่าที่จำเป็น
  • การกู้คืนอัตโนมัติ :หากมีสิ่งผิดปกติเกิดขึ้นและเวิร์กโฟลว์ล้มเหลวกลางคัน ระบบจะกู้คืนโดยอัตโนมัติ
  • ลองใหม่อัตโนมัติ :หากขั้นตอนใดๆ ในเวิร์กโฟลว์ล้มเหลว ขั้นตอนนั้นจะถูกลองใหม่โดยอัตโนมัติ
  • การตรวจสอบแบบเรียลไทม์ :คุณสามารถตรวจสอบขั้นตอนการทำงานของคุณแบบเรียลไทม์ได้จาก Upstash Console

ข้อกำหนดเบื้องต้น

  • ความเข้าใจพื้นฐานเกี่ยวกับแอปพลิเคชัน Next.js
  • บัญชี Upstash สำหรับโทเค็น Redis และ QStash
  • บัญชี Vercel สำหรับการปรับใช้
  • งโกรก (แนะนำ) เพื่อการพัฒนาท้องถิ่น

การตั้งค่าโครงการ

มาเริ่มต้นด้วยการบูตโปรเจ็กต์ Next.js ใหม่โดยใช้ 08 :

npx create-next-app@latest --typescript newsletter-app
cd newsletter-app

ตอนนี้ เรามาเพิ่มการขึ้นต่อกันที่จำเป็นเพื่อโต้ตอบกับบริการ Upstash QStash และ Redis:

npm install @upstash/qstash @upstash/redis

โครงสร้างไดเรกทอรี

ก่อนที่จะเจาะลึกโค้ด เรามาดูกันว่าเราจะจัดระเบียบโครงการของเราอย่างไร:

  • 11 :นี่คือที่ที่ส่วนประกอบและหน้าแอปพลิเคชันหลักของเราจะใช้งานอยู่
  • 23 :เราจะใส่เส้นทาง API ของเราไว้ที่นี่—สำหรับการสมัครสมาชิก การยกเลิกการสมัคร และการจัดการเวิร์กโฟลว์
  • 30 :โฟลเดอร์นี้จะมีส่วนประกอบของแบบฟอร์มการสมัครสมาชิกและการยกเลิกการสมัครสมาชิกของเรา
  • 43 :ฟังก์ชันยูทิลิตี้สำหรับ Redis และการส่งอีเมลจะอยู่ที่นี่
  • 51 :เราจะเก็บคำจำกัดความประเภท TypeScript ของเราไว้ในไดเร็กทอรีนี้

ตัวแปรสภาพแวดล้อม

เราจำเป็นต้องสร้าง 61 ไฟล์ที่รากของโครงการของเราและเพิ่มสิ่งต่อไปนี้:

QSTASH_TOKEN=
UPSTASH_REDIS_REST_URL=
UPSTASH_REDIS_REST_TOKEN=
EMAIL_SERVICE_URL=
NEXT_PUBLIC_BASE_URL=
  • QSTASH_TOKEN :โทเค็น Upstash QStash ของเราเข้าถึงได้จากคอนโซล Upstash
  • UPSTASH_REDIS_REST_URL และ UPSTASH_REDIS_REST_TOKEN :ข้อมูลประจำตัว Upstash Redis ของเราเข้าถึงได้จากคอนโซล Upstash
  • EMAIL_SERVICE_URL :จุดสิ้นสุดของ API การส่งอีเมลของเรา
  • NEXT_PUBLIC_BASE_URL :URL พื้นฐานของแอปพลิเคชันที่เราปรับใช้ (เช่น 76 ).

นอกจากนี้เรายังสามารถตั้งค่า 84 ได้อีกด้วย ตัวแปรใน 93 ของเรา ไฟล์เพื่อการพัฒนาท้องถิ่นด้วย ngrok URL ของเรา หากต้องการเรียนรู้เพิ่มเติมเกี่ยวกับวิธีพัฒนาเวิร์กโฟลว์ในเครื่องด้วย ngrok โปรดดูเอกสาร Upstash

102 ตัวแปรสภาพแวดล้อมมีความจำเป็นสำหรับการพัฒนาท้องถิ่นเท่านั้น ในการผลิต 116 พารามิเตอร์จะถูกตั้งค่าโดยอัตโนมัติและสามารถละเว้นได้

การดำเนินโครงการ

ส่วนประกอบแบบฟอร์มสมัครสมาชิก

125 ส่วนประกอบอนุญาตให้ผู้ใช้ป้อนอีเมลและเลือกความถี่ที่ต้องการรับจดหมายข่าว เมื่อส่งแบบฟอร์มแล้ว เราจะส่งคำขอ POST ไปที่ 130 ด้วยข้อมูลแบบฟอร์ม

src/components/SubscriptionForm.tsx
"use client";
 
import React, { useState } from "react";
 
export default function SubscriptionForm() {
 const [frequency, setFrequency] = useState("daily");
 const [showCustomFrequency, setShowCustomFrequency] = useState(false);
 const [message, setMessage] = useState("");
 const [isError, setIsError] = useState(false);
 
 // Handle frequency selection
 const handleFrequencyChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
 const value = e.target.value;
 setFrequency(value);
 setShowCustomFrequency(value === "custom");
 };
 
 // Handle form submission
 const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
 e.preventDefault();
 setMessage("");
 setIsError(false);
 
 const formData = new FormData(e.currentTarget);
 
 try {
 const response = await fetch("/api/subscribe", {
 method: "POST",
 headers: {
 "Content-Type": "application/json",
 },
 body: JSON.stringify(Object.fromEntries(formData.entries())),
 });
 
 const result = await response.json();
 
 if (!response.ok) {
 setIsError(true);
 setMessage(result.error || "An error occurred during subscription.");
 } else {
 setIsError(false);
 setMessage(result.message || "Subscription successful!");
 }
 } catch (error) {
 console.error("An unexpected error occurred:", error);
 setIsError(true);
 setMessage("An unexpected error occurred.");
 }
 };
 
 // Render the form
 return (
 <form className="flex flex-col gap-4 text-gray-700" onSubmit={handleSubmit}>
 <input
 type="email"
 name="email"
 placeholder="Your Email"
 required
 className="border p-2 rounded"
 />
 <select
 name="frequency"
 value={frequency}
 onChange={handleFrequencyChange}
 required
 className="border p-2 rounded text-gray-700"
 >
 <option value="daily">Daily</option>
 <option value="weekly">Weekly</option>
 <option value="monthly">Monthly</option>
 <option value="custom">Custom Amount of Days</option>
 </select>
 {showCustomFrequency && (
 <input
 type="number"
 name="customFrequency"
 placeholder="Enter number of days"
 min="1"
 className="border p-2 rounded text-gray-700"
 required
 />
 )}
 <button type="submit" className="bg-blue-500 text-white p-2 rounded">
 Subscribe
 </button>
 
 {message && (
 <p className={`mt-2 ${isError ? "text-red-500" : "text-green-500"}`}>
 {message}
 </p>
 )}
 </form>
 );
}

ยกเลิกการสมัครส่วนประกอบแบบฟอร์ม

144 องค์ประกอบอนุญาตให้ผู้ใช้ป้อนอีเมลเพื่อยกเลิกการสมัครรับจดหมายข่าว เมื่อส่งแบบฟอร์มแล้ว เราจะส่งคำขอ POST ไปที่ 159 ด้วยข้อมูลอีเมล นอกจากนี้ยังกรอกข้อมูลในช่องอีเมลล่วงหน้าหากผู้ใช้คลิกลิงก์ยกเลิกการสมัครในอีเมลฉบับใดฉบับหนึ่ง

src/components/UnsubscribeForm.tsx
"use client";
 
import { useState, useEffect, Suspense } from "react";
import { useSearchParams } from "next/navigation";
 
const UnsubscribeForm = () => {
 const searchParams = useSearchParams();
 const [email, setEmail] = useState("");
 const [message, setMessage] = useState("");
 const [isError, setIsError] = useState(false);
 
 // Pre-fill email from query parameter
 useEffect(() => {
 const emailParam = searchParams.get("email");
 if (emailParam) {
 setEmail(emailParam);
 }
 }, [searchParams]);
 
 // Handle form submission
 const handleSubmit = async (e: React.FormEvent) => {
 e.preventDefault();
 setMessage("");
 setIsError(false);
 
 try {
 const response = await fetch("/api/unsubscribe", {
 method: "POST",
 headers: {
 "Content-Type": "application/json",
 },
 body: JSON.stringify({ email }),
 });
 
 const data = await response.json();
 
 if (response.ok) {
 setIsError(false);
 setMessage("You have been unsubscribed successfully.");
 } else {
 setIsError(true);
 setMessage(data.error || "Something went wrong. Please try again.");
 }
 } catch (error) {
 console.error("Error unsubscribing:", error);
 setIsError(true);
 setMessage("An unexpected error occurred. Please try again.");
 }
 };
 
 // Render the form
 return (
 <form className="flex flex-col gap-4 text-gray-700" onSubmit={handleSubmit}>
 <input
 type="email"
 name="email"
 value={email}
 onChange={(e) => setEmail(e.target.value)}
 placeholder="Your Email"
 required
 className="border p-2 rounded"
 />
 <button
 type="submit"
 className="bg-red-500 hover:bg-red-700 text-white p-2 rounded"
 >
 Unsubscribe
 </button>
 
 {message && (
 <p className={`mt-2 ${isError ? "text-red-500" : "text-green-500"}`}>
 {message}
 </p>
 )}
 </form>
 );
};
 
export default function UnsubscribePage() {
 return (
 <Suspense fallback={<div>Loading...</div>}>
 <UnsubscribeForm />
 </Suspense>
 );
}

จัดเก็บข้อมูลใน Redis

เราจะใช้ Upstash Redis เพื่อจัดเก็บข้อมูลการสมัครสมาชิกของผู้ใช้

ในการใช้ Upsatsh Redis ก่อนอื่นเราต้องตั้งค่าฐานข้อมูล Redis บน Upstash Console และรับ REST URL และโทเค็นของเรา สำหรับข้อมูลเพิ่มเติมเกี่ยวกับเรื่องนี้ คุณสามารถดูเอกสาร Upstash ได้

160 จะมีไคลเอนต์ Redis และฟังก์ชันตัวช่วยสำหรับการโต้ตอบกับ Redis:

src/lib/redis.ts
import { Redis } from "@upstash/redis";
 
export const redis = new Redis({
 url: process.env.UPSTASH_REDIS_REST_URL!,
 token: process.env.UPSTASH_REDIS_REST_TOKEN!,
});
 
export async function getUserFrequency(email: string): Promise<number | null> {
 const data = await redis.get(`user:${email}`);
 console.log("User data:", data);
 if (!data) return null;
 
 const parsed = JSON.parse(JSON.stringify(data));
 return parsed.frequency;
}
 
export async function removeUser(email: string): Promise<void> {
 await redis.del(`user:${email}`);
}
 
export async function checkSubscription(email: string): Promise<boolean> {
 return (await getUserFrequency(email)) !== null;
}

ฟังก์ชันการส่งอีเมล

ในการส่งอีเมล เราจะใช้ API อีเมลของเราเองที่เราพัฒนาขึ้นในบล็อกโพสต์ก่อนหน้าเกี่ยวกับการสร้าง Email Scheduler ด้วย QStash Python SDK

src/lib/email.ts
export async function sendEmail(message: string, email: string) {
 console.log(`Sending email to ${email}`);
 const url = process.env.EMAIL_SERVICE_URL;
 const payload = {
 to_email: email,
 subject: "Upstash Newsletter",
 content: message,
 };
 
 if (!url) {
 console.error("EMAIL_SERVICE_URL is not defined.");
 return;
 }
 
 const response = await fetch(url, {
 method: "POST",
 headers: {
 "Content-Type": "application/json",
 },
 body: JSON.stringify(payload),
 });
 
 if (!response.ok) {
 console.error("Failed to send email:", await response.text());
 }
}

คำจำกัดความประเภท

เรายังต้องการคำจำกัดความประเภทสำหรับข้อมูลการสมัครสมาชิก:

src/types/index.ts
export type SubscriptionData = {
 email: string;
 frequency: string;
 customFrequency?: string;
};

สมัครเส้นทาง API

เราจะสร้างเส้นทาง API ที่จัดการคำขอสมัครสมาชิก เมื่อผู้ใช้ส่งแบบฟอร์มการสมัคร จุดสิ้นสุดนี้จะตรวจสอบว่าผู้ใช้ได้สมัครแล้วหรือไม่ และจัดคิวเวิร์กโฟลว์เพื่อจัดการการส่งอีเมลตามความถี่ที่ผู้ใช้เลือก

src/app/api/subscribe/route.ts
import { NextRequest, NextResponse } from "next/server";
import { checkSubscription } from "@/lib/redis";
 
export const POST = async (request: NextRequest) => {
 try {
 const { email, frequency: freq, customFrequency } = await request.json();
 
 console.log("Email:", email);
 console.log("Frequency:", freq);
 console.log("Custom Frequency:", customFrequency);
 
 if (!email || !freq) {
 console.error("Email and frequency are required.");
 return NextResponse.json(
 { error: "Email and frequency are required." },
 { status: 400 }
 );
 }
 
 let frequency = freq;
 if (frequency === "custom") {
 if (!customFrequency) {
 console.error("Custom frequency days are required.");
 return NextResponse.json(
 { error: "Custom frequency days are required." },
 { status: 400 }
 );
 }
 frequency = customFrequency;
 }
 
 if (frequency === "daily") {
 frequency = "1";
 } else if (frequency === "weekly") {
 frequency = "7";
 } else if (frequency === "monthly") {
 frequency = "30";
 }
 
 const frequencyNumber = Number(frequency);
 
 if (isNaN(frequencyNumber) || frequencyNumber <= 0) {
 console.error("Invalid frequency value.");
 return NextResponse.json(
 { error: "Invalid frequency value." },
 { status: 400 }
 );
 }
 
 const exists = await checkSubscription(email);
 
 if (exists) {
 console.error("Email is already subscribed.");
 return NextResponse.json(
 { error: "Email is already subscribed." },
 { status: 400 }
 );
 }
 
 console.log("Subscription successful!");
 
 console.log("Enqueue the workflow");
 // Enqueue the workflow
 await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/api/workflow`, {
 method: "POST",
 headers: {
 Authorization: `Bearer ${process.env.QSTASH_TOKEN}`,
 "Content-Type": "application/json",
 },
 body: JSON.stringify({
 email: email,
 frequency: frequencyNumber,
 }),
 })
 .then((response) => {
 if (!response.ok) {
 console.error("Failed to enqueue workflow:", response.statusText);
 return NextResponse.json(
 { error: "Failed to enqueue workflow." },
 { status: 500 }
 );
 } else {
 console.log("Workflow enqueued successfully");
 }
 })
 .catch((error) => {
 console.error("Error enqueuing workflow:", error);
 return NextResponse.json(
 { error: "Error enqueuing workflow." },
 { status: 500 }
 );
 });
 
 return NextResponse.json({ message: "Subscription successful!" });
 } catch (error) {
 console.error("Error occurred:", error);
 return NextResponse.json(
 { error: "An error occurred during subscription." },
 { status: 500 }
 );
 }
};

ยกเลิกการสมัครเส้นทาง API

เนื่องจากเรามีเส้นทางการสมัครสมาชิก เราจึงต้องมีเส้นทางยกเลิกการสมัครสมาชิกด้วย เมื่อมีการร้องขอ เราจะตรวจสอบว่าผู้ใช้สมัครรับข้อมูลหรือไม่ และลบข้อมูลของพวกเขาออกจาก Redis เราจะส่งอีเมลยืนยันด้วย

src/app/api/unsubscribe/route.ts
import { NextRequest, NextResponse } from "next/server";
import { redis } from "@/lib/redis";
import { sendEmail } from "@/lib/email";
 
export const POST = async (request: NextRequest) => {
 try {
 const { email } = await request.json();
 
 if (!email) {
 return NextResponse.json(
 { error: "Email is required." },
 { status: 400 }
 );
 }
 
 const userExists = await redis.exists(`user:${email}`);
 if (!userExists) {
 return NextResponse.json(
 { error: "Email is not subscribed." },
 { status: 400 }
 );
 }
 
 // Remove the user from Redis
 await redis.del(`user:${email}`);
 
 // Send an email to confirm unsubscription
 await sendEmail(
 "You have been unsubscribed from Upstash Newsletter.",
 email
 );
 
 return NextResponse.json({ message: "You have been unsubscribed." });
 } catch (error) {
 console.error("Unsubscribe error:", error);
 return NextResponse.json(
 { error: "An error occurred. Please try again." },
 { status: 500 }
 );
 }
};

เส้นทาง API เวิร์กโฟลว์

ตอนนี้เป็นส่วนที่สนุก! เราจะสร้างเส้นทาง API ที่จัดการขั้นตอนการทำงานสำหรับการส่งจดหมายข่าวตามช่วงความถี่ที่ระบุ

ขั้นตอนการทำงานของเราจะดำเนินการดังต่อไปนี้:

  1. จัดเก็บข้อมูลการสมัครสมาชิกของผู้ใช้ใน Redis
  2. ส่งอีเมลต้อนรับ
  3. ป้อนวงวน:
    • รอตามระยะเวลาความถี่ที่ระบุ
    • ตรวจสอบว่าผู้ใช้ยังคงสมัครรับข้อมูลอยู่หรือไม่
    • ส่งอีเมลจดหมายข่าว
    • ทำซ้ำจนกว่าจะมีการส่งจดหมายข่าวตามจำนวนที่กำหนด เนื่องจากเราไม่ต้องการวนซ้ำไม่สิ้นสุด
src/app/api/workflow/route.ts

นี่คือตัวอย่างขั้นตอนการทำงานที่เสร็จสมบูรณ์สำหรับผู้ใช้ที่สมัครรับ รับจดหมายข่าวฉบับเดียว และยกเลิกการสมัคร:

สร้างแอปจดหมายข่าวที่ปรับขนาดได้โดยใช้ Upstash Redis และเวิร์กโฟลว์

คุณสามารถเข้าถึงและตรวจสอบขั้นตอนการทำงานของคุณได้จาก Upstash Console

ส่วนประกอบของหน้าหลัก

มาตั้งค่าหน้าหลักของแอปพลิเคชันของเรากันดีกว่า หน้านี้จะมีแบบฟอร์มสมัครสมาชิกและลิงก์ไปยังหน้ายกเลิกการสมัคร

src/app/page.tsx
import SubscriptionForm from "@/components/SubscriptionForm";
import Link from "next/link";
 
export default function Home() {
 return (
 <main className="flex flex-col items-center justify-center min-h-screen p-4">
 <h1 className="text-3xl font-bold mb-6">
 Subscribe to Upstash Newsletter
 </h1>
 
 {/* Subscription Form */}
 <SubscriptionForm />
 
 {/* Unsubscribe Link */}
 <div className="mt-8">
 <p className="text-gray-600">
 Already subscribed and want to unsubscribe?
 <Link
 href="/unsubscribe"
 className="text-red-500 hover:text-red-700 font-bold ml-2"
 >
 Click here to unsubscribe
 </Link>
 </p>
 </div>
 </main>
 );
}

ยกเลิกการสมัครส่วนประกอบของหน้า

สุดท้ายนี้ เรามาสร้างหน้าการยกเลิกการสมัครสมาชิกกันดีกว่า

src/app/unsubscribe/page.tsx
import UnsubscribePage from "@/components/UnsubscribeForm";
 
export default function UnsubscribeHome() {
 return (
 <main className="flex flex-col items-center justify-center min-h-screen p-4">
 <h1 className="text-3xl font-bold mb-6">
 Unsubscribe from Upstash Newsletter
 </h1>
 
 {/* Unsubscribe Form */}
 <UnsubscribePage />
 </main>
 );
}

บทสรุป

และคุณก็ได้แล้ว! เราได้สร้างแอปจดหมายข่าวที่เรียบง่ายโดยไม่ต้องกังวลกับการหมดเวลาของฟังก์ชันไร้เซิร์ฟเวอร์

คุณสามารถค้นหาซอร์สโค้ดที่สมบูรณ์สำหรับโปรเจ็กต์นี้ได้บน GitHub และดูการสาธิตสดได้ที่นี่

สำหรับข้อมูลเพิ่มเติมเกี่ยวกับเวิร์กโฟลว์ Upstash โปรดดูที่เอกสารประกอบ Upstash

หากคุณมีคำถามใดๆ โปรดติดต่อเราได้ที่ Discord นอกจากนี้ อย่าลืมสำรวจ Upstash Blog เพื่อดูบทแนะนำและกรณีการใช้งานเพิ่มเติม