Blitz.js เป็นเฟรมเวิร์ก React ที่เดิมแยกจาก Next.js วันนี้เราจะสร้างแอปพลิเคชั่น To-Do ของ Blitz.js ที่จัดเก็บงานใน Upstash เพื่อไม่ให้เป็นการเสียเวลา มาเริ่มกันเลย!
ตั้งค่า
คุณจะต้องติดตั้ง Blitz.js บนคอมพิวเตอร์ของคุณเพื่อเริ่มต้น
NPM:
npm install -g blitz --legacy-peer-deps
เส้นด้าย:
yarn global add blitz
หากต้องการสร้างแอป Blitz.js ใหม่ ให้ใช้ blitz new
และ cd ลงในไดเร็กทอรี
blitz new blitzjs-todo && cd blitzjs-todo
เยี่ยมมาก ตอนนี้มาติดตั้ง TailwindCSS เพื่อจัดรูปแบบเว็บไซต์ของเรา
blitz install tailwind
สุดท้าย มาติดตั้ง Upstash JS SDK เพื่อให้การเรียก Upstash API ง่ายขึ้น
NPM:
npm i @upstash/redis
เส้นด้าย:
yarn i @upstash/redis
ณ จุดนี้ คุณควรเรียกใช้ blitz dev
เพื่อให้แน่ใจว่าทุกอย่างทำงานอย่างถูกต้อง ลองสร้างบัญชีและลงชื่อเข้าใช้ด้วย ควรมีลักษณะเช่นนี้หากคุณทำทุกอย่างถูกต้องแล้ว
นอกจากนี้ โครงสร้างไฟล์ของคุณควรมีลักษณะดังนี้:
คัดลอก 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
เราได้ตั้งค่าแอปพลิเคชัน Blitz.js ของเราเรียบร้อยแล้ว! มาเริ่มใช้งานรายการสิ่งที่ต้องทำของเรากันเถอะ
การนำไปใช้
Blitz.js มาพร้อมกับ User Authentication ในตัว! มาใช้ประโยชน์จากสิ่งนี้เพื่อสร้างรายการสิ่งที่ต้องทำส่วนตัวสำหรับผู้ใช้แต่ละคน
ขั้นแรก ให้เริ่มต้น Upstash JS SDK ใน /lib/redis.ts
import { Redis } from "@upstash/redis";
const redis = Redis.fromEnv();
export default redis;
เราจะต้องสร้าง 3 เส้นทาง API ต่างๆ เพื่อเข้าถึงรายการสิ่งที่ต้องทำของเรา
ไปที่ 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 นี้ทำงานอย่างไรทีละขั้นตอน ขั้นแรก เราขอเส้นทาง บนเส้นทางนั้น เราตรวจสอบว่าผู้ใช้เข้าสู่ระบบแล้ว หากไม่มีผู้ใช้ เราจะตอบกลับว่า "ไม่ได้รับอนุญาต" หากมี คือ ผู้ใช้ จากนั้นเราดึงฐานข้อมูล Upstash Redis เพื่อค้นหาสิ่งที่ต้องทำทั้งหมดที่อยู่ในรายการในปัจจุบัน สิ่งนี้จะดึงสิ่งที่ต้องทำประมาณร้อยรายการ
ถาม:เดี๋ยวก่อน เราจะเพิ่ม 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 นี้ค่อนข้างคล้ายกับเส้นทางสุดท้าย แต่สังเกตว่าเราได้เพิ่มการตรวจสอบเพิ่มเติมในบรรทัดที่ห้า นั่นเป็นเพราะคำขอนี้ไม่ใช่ GET
ขอค่อนข้างเป็น POST
ขอ. สังเกตวิธีที่เราตรวจสอบสามสิ่ง อันดับแรก เราตรวจสอบให้แน่ใจว่าคำขอนั้นเป็น POST
ขอ. ต่อไป เราตรวจสอบให้แน่ใจว่ามี JSON หรือข้อความใน req.body.data
. สุดท้าย เราตรวจสอบให้แน่ใจว่าผู้ใช้เข้าสู่ระบบแล้ว หากผ่านการตรวจสอบเพียงเล็กน้อย เราสามารถพุชสิ่งที่ต้องทำไปยังรายการ Redis ของเราบน Upstash หากมีข้อผิดพลาดขณะดึงข้อมูล เราสามารถคืนค่า 500 โดยใช้ .catch
.
เส้นทางสุดท้ายที่เราต้องเพิ่มคือเส้นทางหนึ่งเพื่อลบสิ่งที่ต้องทำของเรา เมื่อคุณทำบางสิ่งเสร็จแล้ว คุณต้องข้ามมันออกไปแน่นอน! มาเพิ่มเส้นทาง API สุดท้ายของเราใน 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 ความแตกต่างที่สำคัญที่นี่คือ เรากำลังใช้ LREM
ไม่ใช่ LPUSH
เพื่อลบรายการออกจาก Redis
การสร้างส่วนหน้า
เริ่มจากลบทุกอย่างใน app/pages/index.js
และเขียนรายการสิ่งที่ต้องทำของเราทีละขั้นตอน
วางการนำเข้าเหล่านี้ที่ด้านบนของไฟล์
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 เพื่อสร้างฟังก์ชันหลักของรายการสิ่งที่ต้องทำ มาปรับใช้คุณสมบัติหลักบางอย่างของรายการกัน
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>
)
}
}
<Main/>
องค์ประกอบที่เป็นแกนหลักของแอปพลิเคชันของเรา การดูโค้ดอย่างละเอียดถี่ถ้วนจะบอกเราว่าเราใช้งานอย่างไร ที่ด้านบนขององค์ประกอบ เราเริ่มต้นสถานะสำหรับแอปพลิเคชันของเรา นอกจากนี้เรายังประกาศ ref
เพื่อใช้ในภายหลังในการป้อนข้อมูล "สิ่งที่ต้องทำใหม่" ของเรา คุณอาจสังเกตเห็นการใช้ antiCSRFToken
! Blitz.js ต้องใช้โทเค็นเหล่านี้เมื่อดึงเส้นทาง API ใด ๆ เพื่อป้องกันไม่ให้ผู้ประสงค์ร้ายทำอันตรายไซต์ของคุณ เป็นเรื่องที่ดีในความคิดของฉัน!
เราใช้สามฟังก์ชันหลักในการจัดการข้อมูลของเราบนเว็บไซต์ สามสิ่งนี้คือ:
handleAddTodo
handleRemoveTodo
fetchTodos
เราเรียก fetchTodos
ทันทีที่หน้าโหลดขึ้นมา ให้โหลด To-Dos ทั้งหมดที่ผู้ใช้ยังต้องทำให้เสร็จ เมื่อผู้ใช้ลบหรือเพิ่มสิ่งที่ต้องทำ เราจะเรียก fetchTodos
อีกครั้งเพื่อสะท้อนถึงการเปลี่ยนแปลงนั้นบนเว็บไซต์!
หากผู้ใช้ไม่ได้เข้าสู่ระบบ ผู้ใช้จะได้รับแจ้งให้ลงชื่อเข้าใช้เว็บไซต์ก่อนที่จะเห็นหน้านี้
คุณสามารถลงทะเบียนหรือเข้าสู่ระบบหากคุณยังไม่มีเซสชั่นบนเว็บไซต์ โปรดจำไว้ว่า คุณไม่สามารถจัดเก็บสิ่งที่ต้องทำโดยไม่มีบัญชี และเส้นทาง API ทั้งหมดกำหนดให้คุณต้องตรวจสอบสิทธิ์ด้วย AntiCSRFToken
!
แต่เดี๋ยวก่อน อีกหนึ่งขั้นตอนที่สำคัญ! เราต้องส่งออกหน้า!
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 ใช้แนวทางที่แตกต่างจาก Next เล็กน้อย แต่ในตอนนี้แนวทางดังกล่าวยังคงเหมือนเดิม เราใช้ Suspense
ที่เรานำเข้ามาก่อนหน้านี้เพื่อแสดงให้ผู้ใช้เห็นว่าแอปกำลังโหลด จากนั้นเราจะแสดง </Main>
. ของเรา คอมโพเนนท์หลังจากโหลดเสร็จ!
หากต้องการดูผลการเปลี่ยนแปลงของคุณ ให้เรียกใช้ในคอนโซลอีกครั้ง และไปที่แอปของคุณในเบราว์เซอร์
blitz dev
หากคุณปฏิบัติตามคำแนะนำ ใบสมัครของคุณควรมีลักษณะเป็นแนวนี้เมื่อคุณเข้าสู่ระบบและเพิ่มสิ่งที่ต้องทำ:
คุณสามารถลบ To-Do ได้โดยคลิกที่ช่องข้างๆ นั่นคือสิ่งที่ removeTodo
ฟังก์ชันสำหรับ 😉.
ยินดีด้วย!
ฉันหวังว่าคุณจะได้เรียนรู้สิ่งใหม่จากการอ่านโพสต์ในบล็อกนี้ และถ้าคุณไม่ จำไว้ว่าการฝึกฝนทักษะของคุณก็ไม่เสียหาย! Blitz.js กำลังเปลี่ยนโฟกัสออกจาก Next.js ดังนั้นมันอาจจะเป็นเฟรมเวิร์กที่แตกต่างไปจากเดิมอย่างสิ้นเชิงในอนาคต แต่โปรดติดตามเว็บไซต์ของพวกเขาที่นี่!
ที่มาของโครงการ :ลิงค์ GitHub
การสาธิตการทำงาน: ลิงค์สาธิต
มีข้อเสนอแนะ? อย่าลืมติดตาม @upstash บน Twitter และเข้าร่วมเซิร์ฟเวอร์ Discord!