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

สร้าง Scalable To-Do App ด้วย Blitz.js และ Upstash Redis

Blitz.js เป็น React Framework ที่ Fork มาจาก Next.js วันนี้เราจะสร้าง Blitz.js To-Do Application ที่เก็บ Tasks ใน Upstash เริ่มเลย!

Setup

ต้องติดตั้ง Blitz.js บนคอมพิวเตอร์ก่อน

NPM:

npm install -g blitz --legacy-peer-deps

Yarn:

yarn global add blitz

สร้าง Blitz.js App ใหม่ด้วย blitz new และ cd เข้า Directory

blitz new blitzjs-todo && cd blitzjs-todo

ติดตั้ง TailwindCSS สำหรับ Styling

blitz install tailwind

ติดตั้ง Upstash JS SDK เพื่อเรียก Upstash API ได้ง่ายขึ้น

NPM:

npm i @upstash/redis

Yarn:

yarn i @upstash/redis

รัน blitz dev ตรวจสอบว่าทุกอย่างทำงานถูกต้อง ลองสร้าง Account และ Sign In ควรจะเห็นแบบนี้:

สร้าง Scalable To-Do App ด้วย Blitz.js และ Upstash Redis

File Structure ควรเป็นแบบนี้:

สร้าง Scalable To-Do App ด้วย Blitz.js และ Upstash Redis

คัดลอก UPSTASH_REDIS_REST_URL และ UPSTASH_REDIS_REST_TOKEN จาก Upstash Console ใส่ไฟล์ .env:

# This env file should be checked into source control
# This is the place for default values for all environments
# Values in `.env.local` and `.env.production` will override these values
UPSTASH_REDIS_REST_URL=YOUR_URL_HERE
UPSTASH_REDIS_REST_TOKEN=YOUR_TOKEN_HERE

Setup Blitz.js Application เสร็จแล้ว เริ่ม Implement To-Do List!

Implementation

Blitz.js มาพร้อม User Authentication Built-in! ใช้สำหรับ Private To-Do Lists ต่อ User

เริ่มต้น Upstash JS SDK ใน /lib/redis.ts

import { Redis } from "@upstash/redis";
const redis = Redis.fromEnv();
export default redis;

ต้องทำ 3 API Routes เข้าถึง To-Do Lists

ไปที่ app/api สร้างไฟล์ getall.ts วางโค้ดนี้:

import { BlitzApiRequest, BlitzApiResponse, getSession } from "blitz";
import redis from "../../lib/redis";

export const handler = async (req: BlitzApiRequest, res: BlitzApiResponse) => {
    const session = await getSession(req, res);

    if (!session.userId) {
        res.status(401).json({ error: `Do not tamper with this route!` });
    } else {
        await redis
            .lrange(String(session.userId), 0, 100)
            .then((data) => res.status(200).json({ data: data, success: true }))
            .catch((error) => res.status(500).json({ error: error }));
    }
};
export default handler;

API Route นี้ทำงานยังไง: Request เข้า Route Validate User Logged In หากไม่มี User Return "Not Authorized" หากมี User Fetch Upstash Redis หา To-Dos ทั้งหมดใน List (ประมาณ 100 รายการ)

Q: แล้วเราจะ Add To-Dos ยังไง? A: คำถามดี! มาทำต่อ!

วางโค้ดนี้ในไฟล์ใหม่ add.ts ใน app/api:

import { BlitzApiRequest, BlitzApiResponse, getSession } from "blitz";
import redis from "../../lib/redis";

const handler = async (req: BlitzApiRequest, res: BlitzApiResponse) => {
    const session = await getSession(req, res);

    if (req.method !== "POST" || !req.body.data || !session.userId) {
        res.status(401).json({ error: `Do not tamper with this route!` });
    } else {
        let todo = encodeURI(req.body.data);
        await redis
            .lpush(String(session.userId), todo)
            .then(() => res.status(200).json({ success: true }))
            .catch(() => res.status(500).json({ error: "Error adding data." }));
    }
};
export default handler;

API Route นี้คล้ายก่อนหน้า แต่เพิ่ม Checks ในบรรทัดที่ 5 เพราะเป็น POST Request ตรวจสอบ 3 อย่าง: 1) เป็น POST Request จริง 2) มี JSON/Text ใน req.body.data 3) User Logged In หากผ่านทั้ง 3 Push To-Do ไป Redis List บน Upstash Error ใดๆ Return 500 ด้วย .catch

Route สุดท้าย: Remove To-Dos สร้าง app/api/remove.ts วางโค้ดนี้:

import { BlitzApiRequest, BlitzApiResponse, getSession } from "blitz";
import redis from "../../lib/redis";

const handler = async (req: BlitzApiRequest, res: BlitzApiResponse) => {
    const session = await getSession(req, res);

    if (req.method !== "POST" || !req.body.data || !session.userId) {
        res.status(401).json({ error: `Do not tamper with this route!` });
    } else {
        let todo = encodeURI(req.body.data);
        await redis
            .lrem(String(session.userId), 1, todo)
            .then(() => res.status(200).json({ success: true }))
            .catch(() => res.status(500).json({ error: "Error removing data." }));
    }
};
export default handler;

เหมือน add API Route เกือบทุกอย่าง ความต่างคือใช้ LREM แทน LPUSH เพื่อ Remove Item จาก Redis

Building The Frontend

ลบทุกอย่างใน app/pages/index.js เขียน To-Do List ใหม่ทีละขั้นตอน

Imports ด้านบน:

import { Link, BlitzPage, useMutation, Routes, getAntiCSRFToken } from "blitz";
import { useRef, useEffect, useState, Suspense } from "react";
import Layout from "app/core/layouts/Layout";
import { useCurrentUser } from "app/core/hooks/useCurrentUser";
import logout from "app/auth/mutations/logout";

ใช้ React Hooks สร้าง Core Functionality

const Main = () => {
    const todoRef = useRef<HTMLInputElement>(null);
    const [todos, setTodos] = useState([]);
    const currentUser = useCurrentUser();
    const [logoutMutation] = useMutation(logout);

    const handleAddTodo = async (e) => {
        e.preventDefault();
        const antiCSRFToken = await getAntiCSRFToken();
        const response = await fetch("/api/add", {
            method: "POST",
            headers: {
                "Content-Type": "application/json",
                "anti-csrf": antiCSRFToken,
            },
            body: JSON.stringify({ data: todoRef.current?.value }),
        });
        const data = await response.json();
        if (data.success) {
            todoRef.current!.value = "";
            fetchTodos();
        }
    };

    const handleRemoveTodo = async (id) => {
        const antiCSRFToken = await getAntiCSRFToken();
        const response = await fetch("/api/remove", {
            method: "POST",
            headers: {
                "Content-Type": "application/json",
                "anti-csrf": antiCSRFToken,
            },
            body: JSON.stringify({ data: id }),
        });
        const data = await response.json();
        if (data.success) {
            fetchTodos();
        }
    };

    const fetchTodos = async () => {
        const antiCSRFToken = await getAntiCSRFToken();
        const response = await fetch("/api/getall", {
            method: "GET",
            headers: { "anti-csrf": antiCSRFToken },
        });
        const res = await response.json();
        setTodos(res.data);
    };

    useEffect(() => { fetchTodos(); }, []);

    if (currentUser) {
        return (
            <>
                <button className="mt-4 px-2 py-1 border-2 border-black hover:bg-gray-400 mb-3"
                    onClick={async () => { await logoutMutation(); }}>
                    Logout
                </button>
                <div> User id: <code>{currentUser.id}</code> <br />
                    User email: <code>{currentUser.email}</code> </div>
                <form className="mt-2" onSubmit={handleAddTodo}>
                    <p>add a todo:</p>
                    <input ref={todoRef} className="w-full border-black border-2 focus:outline-none text-center" />
                </form>
                <div className="flex flex-col gap-2 mt-4 bg-gray-300 rounded-md">
                    {(todos as string[]).map((todo: string, index: number) => (
                        <div className="flex items-center p-3 rounded-md bg-gray-300" key={index}>
                            <button onClick={() => handleRemoveTodo(todo)}
                                className="flex items-center mr-4 justify-center w-5 h-5 rounded-[0.25rem] border border-solid border-gray-500 shadow-sm hover:bg-gray-700">
                            </button>
                            <span>{todo}</span>
                        </div>
                    ))}
                </div>
            </>
        );
    } else {
        return (
            <div className="flex flex-col gap-4 text-center">
                <Link href={Routes.SignupPage()}>
                    <a className="mt-4 px-2 py-1 border-2 border-black hover:bg-gray-400">
                        <strong>Sign Up</strong> </a> </Link>
                <Link href={Routes.LoginPage()}>
                    <a className="mt-4 px-2 py-1 border-2 border-black hover:bg-gray-400">
                        <strong>Login</strong> </a> </Link>
            </div>
        );
    }
};

Component <Main/> เป็นหัวใจของ Application ใช้ State และ Ref สำหรับ Input "New To-DO" ใช้ antiCSRFToken Blitz.js ต้องการ Tokens นี้เมื่อ Fetch API Routes ป้องกัน Malicious Actors

3 Functions หลักจัดการ Data:

  • handleAddTodo
  • handleRemoveTodo
  • fetchTodos

เรียก fetchTodos ตอนโหลดหน้า เพื่อ Load To-Dos ที่ User ยังต้องทำ เมื่อ User Add/Remove To-Do เรียก fetchTodos อีกครั้งเพื่อ Reflect Changes

หาก User ไม่ได้ Login จะถูก Prompt ให้ Login ก่อนเห็นหน้านี้

Sign Up หรือ Log In หากยังไม่มี Session จำ To-DOs ไม่ได้โดยไม่มี Account และทุก API Routes ต้อง Authentication ด้วย AntiCSRFToken!

Export Page:

const Home: BlitzPage = () => {
    return (
        <div className="flex flex-col min-h-screen items-center justify-center">
            <main>
                <div className="my-4">
                    <Suspense fallback="Loading..."> <Main /> </Suspense>
                </div>
            </main>
        </div>
    );
};
Home.suppressFirstRenderFlicker = true;
Home.getLayout = (page) => <Layout title="Home">{page}</Layout>;
export default Home;

Blitz.js ใช้ Approach ต่าง Next.js เล็กน้อย แต่ Core เหมือนกัน ใช้ Suspense แสดง Loading แล้วค่อยแสดง <Main/>

รัน blitz dev อีกครั้ง เปิด Browser ดู App

ถ้าทำตามคำแนะนำ App ควรประมาณนี้หลัง Login และ Add To-DOs:

สร้าง Scalable To-Do App ด้วย Blitz.js และ Upstash Redis

Remove To-Do ได้โดยคลิก Box ข้างๆ นั่นคือ removeTodo Function

ยินดีด้วย!

หวังว่าเรียนรู้อะไรใหม่จากบล็อกนี้ หากไม่ก็ถือว่า Brush Up Skills แล้ว Blitz.js กำลัง Pivot Focus ออกจาก Next.js อาจเป็น Framework ต่างหากในอนาคต ติดตามเว็บไซต์ ของพวกเขา!

Project Source: GitHub Link

Working Demo: Demo Link

มี Feedback? Follow @upstash on Twitter และ Join Discord Server!