譯者:朱先忠
在本文中,我們將學習如何使用Next.js、Prisma、Postgres和Fastify來聯合開發一個完整的全棧Web應用程序。具體地說,我們將構建一個考勤管理演示應用程序,用於管理員工的考勤信息。該應用程序的流程比較簡單:一個管理用戶登錄頁面,創建當天的考勤表界面,還有每個員工可以在考勤表上登錄和註銷的界面等。
何謂Next.js?
Next.js是一個靈活的基於React框架的工具,它能夠為您提供創建快速Web應用程序的組件。它通常被稱為全棧式React框架,因為它可以使前端和後端應用程序位於同一個代碼基上;並且,這種實現使用的是無服務器端(Serverless)功能。
何謂Prisma?
Prisma是一個開源的ORM框架,同樣基於Node.js框架和Typescript腳本實現。Prisma大大簡化了SQL數據庫的數據建模、遷移和數據訪問過程。截止撰寫本文時,Prisma支持以下數據庫管理系統:PostgreSQL、MySQL、MariaDB、SQLite、AWS Aurora、Microsoft SQL Server、Azure SQL和MongoDB。
何謂Postgres?
Postgres也稱為PostgreSQL,是一個免費開源的關係數據庫管理系統。它是SQL語言的超集,具有許多優秀特性,允許開發人員安全地存儲和擴展複雜的數據工作負載。
示例項目開發先決條件
本文是一個實踐演示教程。因此,為了順利調試通過這個項目,最好確保先在您的計算機上安裝以下軟件:
Node.js已經成功地安裝在您的計算機上
PostgreSQL數據庫服務器正運行在您的計算機上
注意:本教程的代碼可以在Github網站上找到;所以,您可以隨意克隆下所有源碼並繼續學習。
項目設置
讓我們從設置Next.js應用程序開始。首先,請運行下面的命令。
複製npx create-next-app@latest1.
等待安裝完成,然後運行下面的命令來安裝依賴項。
複製yarn add fastify fastify-nextjs iron-session @prisma/clientyarn add prisma nodemon --dev1.2.
等待安裝完成即可。
設置Next.js和Fastify
默認情況下,Next.js不使用Fastify作為其服務器。為了使用Fastfy作為我們的Next.js應用程序的服務器,需要在你的package.json配置文件中添加以下代碼段:
複製"scripts": { "dev": "nodemon server.js", "build": "next build", "start": "next start", "lint": "next lint"}1.2.3.4.5.6.
創建我們的Fastify服務器
接下來,我們創建一個名字為server.js的文件。這個文件是我們應用程序的入口點。然後,我們添加命令require("fastfy-nextjs"),以便包括一個特定的插件,此插件能夠暴露Fastify中的Next.js API來處理頁面的渲染任務。
接下來,打開server.js文件,並添加以下代碼段:
複製const fastify = require("fastify")()async function noOpParser(req, payload) {return payload;}fastify.register(require("fastify-nextjs")).after(() => {fastify.addContentTypeParser("text/plain", noOpParser);fastify.addContentTypeParser("application/json", noOpParser);fastify.next("/*")fastify.next("/api/*", { method: "ALL" });})fastify.listen(3000, err => {if (err) throw errconsole.log("Server listening on <http://localhost:3000>")})1.2.3.4.5.6.7.8.9.10.11.12.13.14.
在上面代碼片斷中,我們使用插件fastify-nextjs來暴露Fastify中的Next.js API,以便幫助我們完成渲染任務。然後,我們使用noOpParser函數分析發來的請求。具體地說,此函數負責在我們的Next.js API路由處理器中可以使用請求體中的內容。注意到,這裡我們通過命令[fastify.next](<http://fastify.next>定義了程序中的兩個路由。然後我們創建了Fastify服務器,並讓它監聽端口3000。
接下來,我們使用“yarn dev”命令運行上面的應用程序。於是,程序會在地址localhost:3000上運行起來。
Prisma設置
首先,運行以下命令以獲得基本的Prisma設置:
複製npx prisma init1.
上面的命令將創建一個名字為Prisma的目錄,其下還有一個相應的配置文件名是schema.prisma。此文件是您的主Prisma配置文件,其中將包含您的數據庫模式。此外,一個.env文件也將添加到項目的根目錄中。注意,您需要打開這個.env文件,並將虛擬連接URL替換為PostgreSQL數據庫的真實連接URL。
現在,把prisma/schema.prisma文件中的內容替換成如下代碼:
複製datasource db { url = env("DATABASE_URL") provider="postgresql"}generator client { provider = "prisma-client-js"}model User { id Int @id @default(autoincrement()) createdAt DateTime @default(now()) email String @unique name String password String role Role @default(EMPLOYEE) attendance Attendance[] AttendanceSheet AttendanceSheet[]}model AttendanceSheet { id Int @id @default(autoincrement()) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt createdBy User? @relation(fields: [userId], references: [id]) userId Int?}model Attendance { id Int @id @default(autoincrement()) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt signIn Boolean @default(true) signOut Boolean signInTime DateTime @default(now()) signOutTime DateTime user User? @relation(fields: [userId], references: [id]) userId Int?}enum Role { EMPLOYEE ADMIN}1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.
在上面的代碼片段中,我們創建了一個用戶,一個考勤表AttendanceSheet和Attention模型,並定義了每個模型之間的關係。
接下來,需要在數據庫中創建表格。請運行以下命令:
複製npx prisma db push1.
運行上述命令後,您應該會在終端中看到如下屏幕截圖所示的輸出:
創建實用工具函數
Prisma設置完成後,讓我們創建三個實用函數,它們將不時在我們的應用程序中使用。
為此,打開文件lib/parseBody.js,並添加以下代碼段。此函數的任務是將請求正文解析為JSON:
複製export const parseBody = (body) => { if (typeof body === "string") return JSON.parse(body) return body}1.2.3.4.
然後,打開/lib/request.js文件,添加以下代碼段。此函數負責返回iron-session的會話屬性對象。
複製export const sessionCookie = () => { return ({ cookieName: "auth", password: process.env.SESSION_PASSWORD, // 安全提示:在生產環境(使用HTTPS協議)中應當把secure設置為true,但是不能在開發環境(HTTP)下使用true cookieOptions: { secure: process.env.NODE_ENV === "production", }, })}1.2.3.4.5.6.7.8.9.10.
接下來,將SESSION_PASSWORD添加到.env文件:它應該是至少32個字符的字符串。
設計應用程序的樣式
完成上面的實用函數開發後,讓我們為應用程序添加一些樣式。我們將為這個應用程序定義幾個CSS模塊。為此,打開styles/Home.modules.css文件,並添加以下代碼段:
複製.container { padding: 0 2rem;}.man { min-height: 100vh; padding: 4rem 0; flex: 1; display: flex; flex-direction: column; justify-content: center; align-items: center;1.2.3.4.5.6.7.8.9.10.11.
創建邊欄組件
造型完成後,讓我們創建邊欄組件,以便幫助我們導航到應用程序控制面板上的不同頁面。為此,打開components/SideBar.js文件,並粘貼下面的代碼段。
該路由處理通過localhost:3000/api/login登錄後的POST請求,並在用戶經過身份驗證後生成身份驗證Cookie。
複製import { PrismaClient } from "@prisma/client"import { withIronSessionApiRoute } from "iron-session/next";import { parseBody } from "../../lib/parseBody";import { sessionCookie } from "../../lib/session";export default withIronSessionApiRoute( async function loginRoute(req, res) { const { email, password } = parseBody(req.body) const prisma = new PrismaClient() //按唯一標識符 const user = await prisma.user.findUnique({ where: { email },}) if(user.password === password) { //從數據庫中獲取用戶,然後: user.password = undefined req.session.user = user await req.session.save(); return res.send({ status: "success", data: user }); }; res.send({ status: "error", message: "incorrect email or password" }); }, sessionCookie(),);1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.
設置註銷API路由
打開/page/api/logout文件並添加下面的代碼段。此路由負責處理對localhost:3000/api/logout的GET請求,該請求通過銷燬會話Cookie註銷用戶。
複製import { withIronSessionApiRoute } from "iron-session/next";import { sessionCookie } from "../../lib/session";export default withIronSessionApiRoute( function logoutRoute(req, res, session) { req.session.destroy(); res.send({ status: "success" }); }, sessionCookie());1.2.3.4.5.6.7.8.9.10.
創建控制面板頁面
此頁面為用戶提供了登錄和註銷考勤表的界面。當然,管理員還可以通過此界面創建考勤表。現在,打開page/dashboard/index.js文件,並添加下面代碼段。
複製import { withIronSessionSsr } from "iron-session/next";import Head from "next/head"import { useState, useCallback } from "react";import { PrismaClient } from "@prisma/client"import SideBar from "../../components/SideBar"import styles from "../../styles/Home.module.css"import dashboard from "../../styles/Dashboard.module.css"import { sessionCookie } from "../../lib/session";import { postData } from "../../lib/request";export default function Page(props) { const [attendanceSheet, setState] = useState(JSON.parse(props.attendanceSheet)); const sign = useCallback((action="") => { const body = { attendanceSheetId: attendanceSheet[0]?.id, action } postData("/api/sign-attendance", body).then(data => { if (data.status === "success") { setState(prevState => { const newState = [...prevState] newState[0].attendance[0] = data.data return newState }) } }) }, [attendanceSheet]) const createAttendance = useCallback(() => { postData("/api/create-attendance").then(data => { if (data.status === "success") { alert("New Attendance Sheet Created") setState([{...data.data, attendance:[]}]) } }) }, []) return ( <div> <Head> <title>Attendance Management Dashboard</title> <meta name="description" content="dashboard" /> </Head> <div className={styles.navbar}></div> <main className={styles.dashboard}> <SideBar /> <div className={dashboard.users}> { props.isAdmin && <button className={dashboard.create} onClick={createAttendance}>Create Attendance Sheet</button> } { attendanceSheet.length > 0 && <table className={dashboard.table}> <thead> <tr> <th>Id</th> <th>Created At</th> <th>Sign In</th> <th>Sign Out</th> </tr> </thead> <tbody> <tr> <td>{attendanceSheet[0]?.id}</td> <td>{attendanceSheet[0]?.createdAt}</td> { attendanceSheet[0]?.attendance.length != 0 ? <> <td>{attendanceSheet[0]?.attendance[0]?.signInTime}</td> <td>{ attendanceSheet[0]?.attendance[0]?.signOut ? attendanceSheet[0]?.attendance[0]?.signOutTime: <button onClick={() => sign("sign-out")}> Sign Out </button> }</td> </> : <> <td> <button onClick={() => sign()}> Sign In </button> </td> <td>{""}</td> </> } </tr> </tbody> </table> } </div> </main> </div> )}1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.45.46.47.48.49.50.51.52.53.54.55.56.57.58.59.60.61.62.63.64.65.66.67.68.69.70.71.72.73.74.75.76.77.78.79.80.81.82.83.84.85.86.87.88.89.90.91.92.93.94.95.96.97.98.99.100.101.102.103.104.105.106.107.108.109.110.111.112.113.114.115.116.
我們使用getServerSideProps函數來生成頁面數據,而withIronSessionSsr是一個用於處理服務器端呈現頁面功能的iron-session函數。在下面的代碼段中,我們使用數據庫考勤表中的一行查詢考勤表的最後一行。其中,userId等於存儲在用戶會話中的用戶id。我們還檢查用戶是否是管理員(ADMIN)角色。
複製export const getServerSideProps = withIronSessionSsr( async ({req}) => { const user = req.session.user const prisma = new PrismaClient() const attendanceSheet = await prisma.attendanceSheet.findMany({ take: 1, orderBy: { id: "desc", }, include: { attendance: { where: { userId: user.id }, } } }) return { props: { attendanceSheet: JSON.stringify(attendanceSheet), isAdmin: user.role === "ADMIN" } }}, sessionCookie())1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.
設置創建考勤API路由
打開頁面/api/create Attention.js文件,並添加下面代碼段。
複製import { PrismaClient } from "@prisma/client"import { withIronSessionApiRoute } from "iron-session/next";import { sessionCookie } from "../../lib/session"; export default withIronSessionApiRoute( async function handler(req, res) { const prisma = new PrismaClient() const user = req.session.user const attendanceSheet = await prisma.attendanceSheet.create({ data: { userId: user.id, }, }) res.json({status: "success", data: attendanceSheet}); }, sessionCookie())1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.
設置簽名考勤API路由
此路由負責處理我們對localhost:3000/api/sign-attendance的API POST請求。路由接受POST請求,而attendanceSheetId和action用於登錄和註銷attendanceSheet。
打開/page/api/sign-attendance.js文件,並添加下面的代碼段。
複製import { PrismaClient } from "@prisma/client"import { withIronSessionApiRoute } from "iron-session/next";import { parseBody } from "../../lib/parseBody";import { sessionCookie } from "../../lib/session"; export default withIronSessionApiRoute( async function handler(req, res) { const prisma = new PrismaClient() const {attendanceSheetId, action} = parseBody(req.body) const user = req.session.user const attendance = await prisma.attendance.findMany({ where: { userId: user.id, attendanceSheetId: attendanceSheetId } }) //check if atendance have been created if (attendance.length === 0) { const attendance = await prisma.attendance.create({ data: { userId: user.id, attendanceSheetId: attendanceSheetId, signIn: true, signOut: false, signOutTime: new Date() }, }) return res.json({status: "success", data: attendance}); } else if (action === "sign-out") { await prisma.attendance.updateMany({ where: { userId: user.id, attendanceSheetId: attendanceSheetId }, data: { signOut: true, signOutTime: new Date() }, }) return res.json({status: "success", data: { ...attendance[0], signOut: true, signOutTime: new Date()}}); } res.json({status: "success", data:attendance}); }, sessionCookie())1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.45.46.47.48.49.50.51.52.53.
創建考勤頁面
這個服務器端呈現的頁面將顯示登錄用戶的所有考勤表信息。打開/page/dashboard/attendance.js文件,並添加下面的代碼段。
複製import { withIronSessionSsr } from "iron-session/next";import Head from "next/head"import { PrismaClient } from "@prisma/client"import SideBar from "../../components/SideBar"import styles from "../../styles/Home.module.css"import dashboard from "../../styles/Dashboard.module.css"import { sessionCookie } from "../../lib/session";export default function Page(props) { const data = JSON.parse(props.attendanceSheet) return ( <div> <Head> <title>Attendance Management Dashboard</title> <meta name="description" content="dashboard" /> </Head> <div className={styles.navbar}></div> <main className={styles.dashboard}> <SideBar /> <div className={dashboard.users}> <table className={dashboard.table}> <thead> <tr> <th> Attendance Id</th> <th>Date</th> <th>Sign In Time</th> <th>Sign Out Time</th> </tr> </thead> <tbody> { data.map(data => { const {id, createdAt, attendance } = data return ( <tr key={id}> <td>{id}</td> <td>{createdAt}</td> { attendance.length === 0 ? ( <> <td>You did not Sign In</td> <td>You did not Sign Out</td> </> ) : ( <> <td>{attendance[0]?.signInTime}</td> <td>{attendance[0]?.signOut ? attendance[0]?.signOutTime : "You did not Sign Out"}</td> </> ) } </tr> ) }) } </tbody> </table> </div> </main> </div> )}1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.45.46.47.48.49.50.51.52.53.54.55.56.57.58.59.60.61.62.63.64.65.66.67.68.69.70.71.72.73.74.75.76.77.78.79.80.81.82.83.84.85.86.87.88.
在下面的代碼片段中,我們從attendanceSheet表中查詢所有行,並獲取用戶id等於存儲在用戶會話中的用戶id的考勤信息。
複製export const getServerSideProps = withIronSessionSsr( async ({req}) => { const user = req.session.user const prisma = new PrismaClient() const attendanceSheet = await prisma.attendanceSheet.findMany({ orderBy: { id: "desc", }, include: { attendance: { where: { userId: user.id }, } } }) return { props: { attendanceSheet: JSON.stringify(attendanceSheet), } }}, sessionCookie())1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.
創建考勤表頁面
這個服務器端呈現的頁面負責顯示所有考勤表以及登錄到該考勤表的員工信息。為此,打開/page/dashboard/attendance.js文件,並添加下面的代碼段。
複製import { withIronSessionSsr } from "iron-session/next";import Head from "next/head"import { PrismaClient } from "@prisma/client"import SideBar from "../../components/SideBar"import styles from "../../styles/Home.module.css"import dashboard from "../../styles/Dashboard.module.css"import { sessionCookie } from "../../lib/session";export default function Page(props) { const data = JSON.parse(props.attendanceSheet) return ( <div> <Head> <title>Attendance Management Dashboard</title> <meta name="description" content="dashboard" /> </Head> <div className={styles.navbar}></div> <main className={styles.dashboard}> <SideBar /> <div className={dashboard.users}> { data?.map(data => { const {id, createdAt, attendance } = data return ( <> <table key={data.id} className={dashboard.table}> <thead> <tr> <th> Attendance Id</th> <th>Date</th> <th> Name </th> <th> Email </th> <th> Role </th> <th>Sign In Time</th> <th>Sign Out Time</th> </tr> </thead> <tbody> { (attendance.length === 0) && ( <> <tr><td> {id} </td> <td>{createdAt}</td> <td colSpan={5}> No User signed this sheet</td></tr> </> ) } { attendance.map(data => { const {name, email, role} = data.user return ( <tr key={id}> <td>{id}</td> <td>{createdAt}</td> <td>{name}</td> <td>{email}</td> <td>{role}</td> <td>{data.signInTime}</td> <td>{data.signOut ? attendance[0]?.signOutTime: "User did not Sign Out"}</td> </tr> ) }) } </tbody> </table> </> ) }) } </div> </main> </div> )}1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.45.46.47.48.49.50.51.52.53.54.55.56.57.58.59.60.61.62.63.64.65.66.67.68.69.70.71.72.73.74.75.76.77.78.79.80.81.82.83.84.85.86.87.88.89.90.91.92.93.94.95.96.97.98.99.100.101.
在下面的代碼片段中,我們從attendanceSheet表中查詢所有行,並通過選擇名稱、電子郵件和角色來獲取考勤信息。
複製export const getServerSideProps = withIronSessionSsr(async () => { const prisma = new PrismaClient() const attendanceSheet = await prisma.attendanceSheet.findMany({ orderBy: { id: "desc", }, include: { attendance: { include: { user: { select: { name: true, email: true, role: true } } } }, }, }) return { props: { attendanceSheet: JSON.stringify(attendanceSheet), } }}, sessionCookie())1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.
測試應用程序
最後,我們來測試一下上面完整的示例應用程序。首先,我們必須向數據庫中添加用戶數據。我們是使用Prisma Studio來實現此任務的。要啟動Prisma Studio,請運行以下命令:
複製npx prisma studio1.
最終,Prisma索引頁面如下所示:
要創建數據庫用戶,要求使用管理員(ADMIN)角色;而創建普通類型的多個用戶,只需要使用員工(EMPLOYEE)角色即可。為此,請切換到以下頁面:
然後,使用“yarn dev”命令啟動服務器。通過此命令將在本地地址[localhost:3000]上啟動服務器,並啟動應用程序登錄頁面如下所示:
現在,請使用具有管理員(ADMIN)角色的用戶登錄,因為只有管理員用戶才能創建考勤表。登錄成功後,應用程序會將您重定向到系統的控制面板界面。
接下來,您可以單擊側欄中的考勤鏈接以查看用戶的考勤信息。結果應符合以下顯示的內容:
接下來,單擊側欄上的考勤表(Attendance Sheet)鏈接,可以查看所有用戶的考勤情況。結果如下:
結論
在本文中,我們探討了如何配合Next.js使用自定義Fastify服務器。然後,還介紹了Prisma和Prisma Studio有關知識,還介紹瞭如何將Prisma連接到Postgres數據庫,以及如何使用Prisma客戶端和Prisma Studio來創建、讀取和更新數據庫的問題。此外,您還學習瞭如何使用iron-session對用戶進行身份驗證。
最後,在本文的主體示例項目中,我們聯合Next.js、Prisma、Postgres和Fastfy構建了一款完整的員工考勤管理應用程序。