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

Tạo một ứng dụng bản tin có thể mở rộng bằng cách sử dụng Upstash Redis và Workflow

Trong blog này, chúng tôi sẽ xây dựng một ứng dụng bản tin nơi người dùng có thể đăng ký và chọn tần suất họ muốn nhận bản tin của mình. Chúng tôi sẽ sử dụng Upstash Redis để lưu trữ dữ liệu đăng ký và Quy trình công việc Upstash để quản lý các hoạt động lưu trữ dữ liệu, gửi email chào mừng và lên lịch nhận bản tin dựa trên tùy chọn của người dùng.

Động lực

Trước hết, môi trường không có máy chủ thật tuyệt vời! Chúng có khả năng mở rộng cao và dễ dàng về ngân sách. Tuy nhiên, chúng có những hạn chế nhất định, chẳng hạn như giới hạn thời gian thực hiện. Điều này đặc biệt có thể gây ra vấn đề khi bạn cần chạy các tác vụ dài hạn.

Đó là nơi Quy trình làm việc Upstash phát huy tác dụng. Với Upstash Workflow, bạn có thể tạo các quy trình làm việc liên tục có thể chạy bao lâu tùy thích. Vì vậy, bạn không phải lo lắng về việc hết thời gian chờ của chức năng serverless nữa.

Đây là danh sách các tính năng bạn nhận được khi sử dụng Upstash Workflow:

  • Không còn thời gian chờ của chức năng không có máy chủ :Quy trình làm việc của bạn có thể chạy trong thời gian cần thiết.
  • Tự động phục hồi :Nếu có sự cố xảy ra và quy trình làm việc bị lỗi giữa chừng, quy trình sẽ tự động khôi phục.
  • Tự động thử lại :Nếu bất kỳ bước nào trong quy trình làm việc không thành công, bước đó sẽ tự động được thử lại.
  • Giám sát thời gian thực :Bạn có thể theo dõi quy trình công việc của mình trong thời gian thực từ Bảng điều khiển Upstash.

Điều kiện tiên quyết

  • Hiểu biết cơ bản về ứng dụng Next.js.
  • Tài khoản Upstash dành cho mã thông báo Redis và QStash.
  • Tài khoản Vercel để triển khai.
  • ngrok (được khuyến nghị) để phát triển địa phương.

Thiết lập dự án

Hãy bắt đầu bằng cách khởi động một dự án Next.js mới bằng cách sử dụng create-next-app :

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

Bây giờ, hãy thêm các phần phụ thuộc cần thiết để tương tác với các dịch vụ Upstash QStash và Redis:

npm install @upstash/qstash @upstash/redis

Cấu trúc thư mục

Trước khi đi sâu vào mã, hãy xem nhanh cách chúng tôi tổ chức dự án của mình:

  • src/app/ :Đây là nơi các thành phần và trang ứng dụng chính của chúng tôi sẽ hoạt động.
  • src/app/api/ :Chúng tôi sẽ đặt các tuyến API của mình tại đây—để đăng ký, hủy đăng ký và xử lý quy trình làm việc.
  • src/components/ :Thư mục này sẽ chứa các thành phần biểu mẫu đăng ký và hủy đăng ký của chúng tôi.
  • src/lib/ :Các chức năng tiện ích cho Redis và gửi email sẽ có ở đây.
  • src/types/ :Chúng tôi sẽ giữ các định nghĩa loại TypeScript trong thư mục này.

Biến môi trường

Chúng ta cần tạo một .env vào thư mục gốc của dự án của chúng tôi và thêm vào phần sau:

QSTASH_TOKEN=
UPSTASH_REDIS_REST_URL=
UPSTASH_REDIS_REST_TOKEN=
EMAIL_SERVICE_URL=
NEXT_PUBLIC_BASE_URL=
  • QSTASH_TOKEN :Mã thông báo QStash Upstash của chúng tôi được truy cập từ Bảng điều khiển Upstash.
  • UPSTASH_REDIS_REST_URLUPSTASH_REDIS_REST_TOKEN :Thông tin xác thực Upstash Redis của chúng tôi được truy cập từ Bảng điều khiển Upstash.
  • EMAIL_SERVICE_URL :Điểm cuối của API gửi email của chúng tôi.
  • NEXT_PUBLIC_BASE_URL :URL cơ sở của ứng dụng đã triển khai của chúng tôi (ví dụ:https://your-app.vercel.app ).

Chúng ta cũng có thể đặt UPSTASH_WORKFLOW_URL biến trong .env của chúng tôi tệp để phát triển cục bộ bằng URL ngrok của chúng tôi. Để tìm hiểu thêm về cách phát triển quy trình làm việc cục bộ bằng ngrok, hãy tham khảo Tài liệu Upstash.

UPSTASH_WORKFLOW_URL Biến môi trường chỉ cần thiết cho sự phát triển cục bộ. Trong quá trình sản xuất, baseUrl tham số được đặt tự động và có thể bỏ qua.

Triển khai dự án

Thành phần biểu mẫu đăng ký

SubscriptionForm thành phần cho phép người dùng nhập email của họ và chọn tần suất họ muốn nhận bản tin. Khi biểu mẫu được gửi, chúng tôi sẽ gửi yêu cầu POST tới /api/subscribe với dữ liệu biểu mẫu.

src/thành phần/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>
 );
}

Hủy đăng ký thành phần biểu mẫu

UnsubscribeForm thành phần cho phép người dùng nhập email của họ để hủy đăng ký nhận bản tin. Khi biểu mẫu được gửi, chúng tôi sẽ gửi yêu cầu POST tới /api/unsubscribe với dữ liệu email. Nó cũng điền trước trường email nếu người dùng nhấp vào liên kết hủy đăng ký trong một trong các email.

src/thành phần/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>
 );
}

Lưu trữ dữ liệu trong Redis

Chúng tôi sẽ sử dụng Upstash Redis để lưu trữ dữ liệu đăng ký của người dùng.

Để sử dụng Upsatsh Redis, trước tiên chúng ta cần thiết lập cơ sở dữ liệu Redis trên Bảng điều khiển Upstash và nhận URL REST cũng như mã thông báo. Để biết thêm thông tin về vấn đề này, bạn có thể xem Tài liệu Upstash.

redis.ts sẽ chứa các chức năng trợ giúp và ứng dụng khách Redis của chúng tôi để tương tác với 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;
}

Chức năng gửi email

Để gửi email, chúng tôi sẽ sử dụng API email của riêng mình mà chúng tôi đã phát triển trong bài đăng blog trước đây về cách tạo Trình lập lịch email bằng 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());
 }
}

Định nghĩa loại

Chúng tôi cũng cần định nghĩa loại cho dữ liệu đăng ký:

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

Đăng ký lộ trình API

Chúng tôi sẽ tạo một tuyến API xử lý các yêu cầu đăng ký. Khi người dùng gửi biểu mẫu đăng ký, điểm cuối này sẽ kiểm tra xem người dùng đã đăng ký chưa và sắp xếp một quy trình công việc để xử lý việc gửi email dựa trên tần suất người dùng đã chọn.

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 }
 );
 }
};

Hủy đăng ký lộ trình API

Vì chúng tôi có lộ trình đăng ký nên chúng tôi cũng cần lộ trình hủy đăng ký. Khi có yêu cầu, chúng tôi sẽ kiểm tra xem người dùng đã đăng ký chưa và xóa dữ liệu của họ khỏi Redis. Chúng tôi cũng sẽ gửi email xác nhận.

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 }
 );
 }
};

Lộ trình API quy trình công việc

Bây giờ, đây là phần thú vị! Chúng tôi sẽ tạo một tuyến API xử lý quy trình gửi bản tin theo các khoảng tần suất được chỉ định.

Quy trình làm việc của chúng tôi sẽ thực hiện như sau:

  1. Lưu trữ dữ liệu đăng ký của người dùng trong Redis.
  2. Gửi email chào mừng.
  3. Nhập vòng lặp:
    • Đợi trong khoảng thời gian tần suất được chỉ định.
    • Kiểm tra xem người dùng có còn đăng ký hay không.
    • Gửi email bản tin.
    • Lặp lại cho đến khi một số lượng bản tin nhất định đã được gửi vì chúng ta không muốn vòng lặp vô hạn.
src/app/api/workflow/route.ts

Dưới đây là ví dụ về quy trình làm việc đã hoàn thành dành cho người dùng đã đăng ký, nhận một bản tin và hủy đăng ký:

Tạo một ứng dụng bản tin có thể mở rộng bằng cách sử dụng Upstash Redis và Workflow

Bạn có thể truy cập và giám sát quy trình làm việc của mình từ Bảng điều khiển Upstash.

Thành phần trang chính

Hãy thiết lập trang chính của ứng dụng của chúng tôi. Trang này sẽ bao gồm biểu mẫu đăng ký và liên kết đến trang hủy đăng ký.

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>
 );
}

Hủy đăng ký thành phần trang

Cuối cùng, hãy tạo trang hủy đăng ký.

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>
 );
}

Kết luận

Và bạn có nó! Chúng tôi đã xây dựng một ứng dụng bản tin đơn giản mà không phải lo lắng về thời gian chờ của chức năng serverless.

Bạn có thể tìm thấy mã nguồn hoàn chỉnh cho dự án này trên GitHub và xem bản demo trực tiếp tại đây.

Để biết thêm thông tin về Upstash Workflow, bạn có thể tham khảo Tài liệu về Upstash.

Nếu bạn có bất kỳ câu hỏi nào, vui lòng liên hệ với chúng tôi trên Discord. Ngoài ra, đừng quên khám phá Blog Upstash để biết thêm hướng dẫn và trường hợp sử dụng.