--- title: Nuxt + Prisma + PostgrelSQL 全端專案 --- # Nuxt + Prisma + PostgrelSQL 全端專案 本專案是一個以 Nuxt 3 為前端、Prisma + PostgreSQL 為後端的電商網站,並整合 TailwindCSS、Pinia 狀態管理與 JWT 身份驗證,實現完整的會員註冊、商品購物、訂單流程與後台管理功能。 --- ## 🔧 專案架構 - **MyShop/** - frontend/ # 使用 Vite + Vue3 建構的購物網站前台 - admin/ # 使用 Nuxt3 開發的後台管理系統 - prisma/ # Prisma schema 與 migration 設定 - .env # 資料庫連線設定 --- ## 🖥️ 前端(Frontend) - **框架**:Vue 3 + Vite - **狀態管理**:Pinia(含購物車狀態持久化) - **路由**:Vue Router - **樣式**:Tailwind CSS + Element Plus(UI 套件) - **功能**: - 商品列表與詳細頁面 - 加入購物車與移除功能 - 結帳功能,含庫存數量驗證 - 訂單送出後導向訂單詳情頁 --- ## 🛠️ 後台(Admin) - **框架**:Nuxt 3 - **UI 套件**:Element Plus + Tailwind CSS - **資料存取**:透過 API 與 Prisma 操作 PostgreSQL 資料庫 - **功能模組**: - 商品管理(新增 / 編輯 / 刪除 / 庫存設定) - 訂單管理(查看訂單、訂單詳情) - 管理員登入 / 登出 - 管理員管理 (新增、編輯) --- ## 🗄️ 資料庫 - **類型**:PostgreSQL(部署於 Render) - **ORM**:Prisma ORM - **Schema 架構**: - `Product`:商品資訊與庫存數量 - `Order`:訂單總覽資料 - `OrderItem`:訂單中的每一筆商品與數量明細 - `User`: 會員 - `Admin`: 管理員 --- # 🌐 Nuxt 是什麼? Nuxt.js(通常簡稱為 Nuxt)是一個基於 Vue.js 的高階框架,用來構建 現代化 Web 應用程式。它幫助開發者更容易地實作: 1. 伺服器端渲染(SSR) 2. 靜態網站生成(SSG) 3. 單頁應用(SPA) 4. 全端應用(Fullstack) ## 💡 核心特點 #### 功能 說明 ✅ SSR(Server-Side Rendering) 頁面由伺服器預先渲染,對 SEO 友好,載入速度快 ✅ 靜態網站生成(SSG) 預先生成 HTML,可部署到 CDN 上,適合部落格、官網等 ✅ 自動路由 pages/ 資料夾自動變成對應的路由 ✅ 全端支援 可寫 API:server/api/*.ts 是後端邏輯,整合 Prisma、DB 很方便 ✅ 強化 DX(開發體驗) Hot reload、TypeScript 支援、Composable API(setup 語法) ✅ 模組系統 如 auth、i18n、tailwind、pinia、image 等模組一行裝起 ## 🛠 Nuxt 的用途 ### 1. 建立 SEO 友善的網站 像是: 部落格、電商網站、公司介紹頁面。 因為 SSR 可以讓搜尋引擎更容易抓到內容。 ### 2. 開發 Fullstack 應用 Nuxt 3 提供 server API 支援,例如你可以使用: /server/api 目錄撰寫後端 API 搭配 Prisma 使用資料庫 使用 JWT 驗證登入系統 👉 一個 Nuxt 專案就可以包辦前後端,非常適合開發管理後台、會員系統、電商平台等。 ### 3. 建立靜態網站(預渲染) 適合一次編譯後部署上 CDN,像是: 部落格、產品介紹頁、 技術文件 使用 `nuxt generate` 指令即可。 ### 4. 做為 Vue 應用的升級版 你如果本來會寫 Vue,只要學 Nuxt 的額外功能,馬上能上手: 頁面路由 = pages 資料夾 中介層(middleware) 插件系統(plugins) 狀態管理(Pinia) API server(server/api) --- ## Nuxt 畫面架構說明 畫面的整體結構主要由 Header、Sidebar 與 MainContent 三大區塊組成。 ![截圖 2025-04-23 下午2.08.07](https://hackmd.io/_uploads/H1XNSW8kex.png) 在這個結構中,**會根據路由變化的部分僅有 MainContent 區塊。** 在 Nuxt 中,我們可以透過頁面加入以下程式碼,來指定該頁面所使用的 Layout 以及 Middleware: ``` definePageMeta({ layout: 'admin', middleware: ['auth'] }); ``` 加入上述程式碼後,該頁面就會預設使用 `admin` 這個 Layout,並且套用名為 `auth` 的中介層驗證機制。 --- ### Layout 插槽機制 如下圖,`<slot />` 代表的是可替換內容的插槽,Nuxt 會根據當前的路由,自動渲染對應頁面的內容到這個位置: ![截圖 2025-04-23 下午2.21.05](https://hackmd.io/_uploads/Hk2-D-Lkgl.png) 這樣的架構讓開發者能夠有效地拆分畫面、重用結構,並在不同頁面中注入各自的內容,維持一致的 UI 外框設計。 --- ### 全域管理(Pinia + persist) 在Vue當中會使用**Pinia**來做全域狀態管理,搭配 **persist**插件,把需要的狀態儲存到**localstorge**。 在Nuxt中當然也可以使用。只是有些設定上的差異性。 ### Vue main.js ``` const pinia = createPinia(); pinia.use(piniaPluginPersistedstate); app.use(pinia); ``` ### Nuxt nuxt.config.ts ``` import { resolve } from 'path'; export default defineNuxtConfig({ modules: [ '@nuxtjs/tailwindcss', '@pinia/nuxt', 'pinia-plugin-persistedstate/nuxt' ] }); ``` plugins/piniaPersist.client.ts ``` import { defineNuxtPlugin } from '#app' import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' import type { Pinia } from 'pinia' export default defineNuxtPlugin((nuxtApp) => { (nuxtApp.$pinia as Pinia).use(piniaPluginPersistedstate) }) ``` --- 在Nuxt中使用**pinia**,需要注意的事 一般來說Vue中的template會類似下面圖示: ![截圖 2025-04-23 下午3.42.00](https://hackmd.io/_uploads/ryJQcMLJel.png) 但是在Nuxt中會出下面圖示中的錯誤訊息, **Hydration completed but contains mismatches.** ![截圖 2025-04-23 下午3.36.59](https://hackmd.io/_uploads/HJ6zFMUygg.png) 這個主要的原因是, Nuxt (或 Vue SSR) 在執行「Hydration」(將伺服器端渲染的 HTML 轉換為可互動的 Vue 組件)時,發現 伺服器端渲染出來的內容和客戶端接手的內容不一致。 ### 為什麼會出現? #### 1. 使用了 localStorage / window 等 browser-only API 在下面圖中,**currentAdmin**是 藉由pinia persist 自動從 localstorage存取的。 所以才導致 **Hydration completed but contains mismatches.** 出現這個錯誤訊息。 ![截圖 2025-04-23 下午3.45.38](https://hackmd.io/_uploads/HkWThMIkxg.png) ![截圖 2025-04-23 下午3.45.55](https://hackmd.io/_uploads/Sk9TnfUJge.png) ### 解決方法 #### 使用 <ClientOnly></ClientOnly> 包裹不需要SSR的部分。 ![截圖 2025-04-23 下午3.59.42](https://hackmd.io/_uploads/SyXW0fUJgx.png) ### 💬 為什麼這樣就可以? 因為 **<ClientOnly>** 會跳過 SSR,那 SSR 階段根本不會產生這段 HTML,自然也就沒有 hydration 的錯誤。Vue 會在 CSR 階段動態載入它 --- ## Middleware #### 是一種在頁面渲染之前攔截請求並執行邏輯的機制,通常用來實作以下幾種功能: ![截圖 2025-04-23 下午5.34.51](https://hackmd.io/_uploads/B16r4VLJlg.png) #### 在這個專案中,我們使用了兩種類型的 Middleware,分別為: - admin/middleware:屬於 Nuxt 的前端(client-side 或 universal)middleware,主要用於 路由守衛與驗證登入狀態。 - admin/server/middleware:屬於 Nuxt 的後端 Middleware(基於 Nitro server),用於處理例如 CORS 設定、Header 檢查、Token 驗證等伺服器層級邏輯。 如下圖所示,兩者在執行階段與角色上的差異一目了然: ![截圖 2025-04-23 下午5.36.06](https://hackmd.io/_uploads/HyafS481eg.png) #### 程式碼範例參考: **admin/middleware/auth.ts(前端路由驗證 Middleware)** ``` import { useAuthStore } from '~/store/auth'; export default defineNuxtRouteMiddleware(() => { if (process.server) return; const authStore = useAuthStore(); if (!authStore.token || !authStore.admin) { return navigateTo('/login'); } }); ``` **admin/server/middleware/cors.ts(伺服器端 Middleware)** ``` export default defineEventHandler((event) => { setHeader(event, 'Access-Control-Allow-Origin', '*') setHeader(event, 'Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS') setHeader(event, 'Access-Control-Allow-Headers', 'Content-Type, Authorization') // 處理預檢請求 if (getMethod(event) === 'OPTIONS') { return new Response(null, { status: 204 }) } }) ``` --- ## Nuxt 中的 API 運作方式 Nuxt 3 採用了 **Nitro Server** 作為內建的後端伺服器架構,讓你可以 直接在 server/api/ 目錄下撰寫 API,無需額外設定 Express 或其他後端框架。 ✅ 支援 REST / JSON / server functions。 ✅ 可以存取 cookies、headers、query、body 等。 ✅ 適合建構 SSR-friendly 的全端應用。 ## 匹配 HTTP 請求方法 (HTTP Request Method)** 我們可以添加 .get、.post、.put 或 .delete 等檔案名稱後綴,來匹配對應的 HTTP request methods。 - /api/product/index.**get**.ts ---> 取得所有商品資料 - /api/product/index.**post**.ts ---> 新增商品資訊 - /api/product/[id].**get**.ts ---> 取得單一筆商品資訊 - /api/product/[id].**put**.ts ---> 更新單一筆商品資訊 - /api/product/[id].**delete**.ts ---> 刪除單一筆商品資訊 #### 建立第一個測試API Nuxt 會自動掃描 server 目錄中的檔案結構,建立 Server API 時通常以 .js 或 .ts 作為副檔名,並依照官方建議,每個檔案都應該預設匯出 **defineEventHandler()** 函數,並在其 handler 內實作處理邏輯。 **先在 server/api 底下建立一個hello.ts** ``` export default defineEventHandler(() => { return { msg: 'Hello, Nuxt API!' }; }); ``` 訪問http://localhost:3000/api, 會看見回傳的 JSON 資料。 ![截圖 2025-04-24 上午10.22.36](https://hackmd.io/_uploads/rkcv-XDyel.png) 使用VScode的插件**Thunder Client**測試POST Request,成功的話會出現 200 OK ![截圖 2025-04-24 上午10.53.39](https://hackmd.io/_uploads/HJftumPJlg.png) # Prisma 是什麼? Prisma 是一套現代化的 TypeScript ORM(Object-Relational Mapping)工具,可以讓你更簡潔、安全地與資料庫互動。 它支援多種資料庫,包括 PostgreSQL、MySQL、SQLite、SQL Server 等。 ✅ Prisma = 強型別 + 自動補全 + SQL 抽象化 + DB Schema 管理工具! ## 🎯 Prisma 的用途 ✅ 對資料庫進行 CRUD 操作(Create / Read / Update / Delete) ✅ 使用 TypeScript 提供自動補全與型別檢查 ✅ 管理資料庫 Schema(使用 Prisma Migrate) ✅ 與 REST API / GraphQL 無縫整合 ✅ 提供 DB client,不需手寫 SQL ## 🧠 Prisma 的運作流程 ### Prisma 的基本流程分成 3 個部分: 1️⃣ Schema 定義(prisma/schema.prisma) 你用類似 SQL 的語法定義資料表與關聯: ``` model Product { id Int @id @default(autoincrement()) name String description String price Float stock Int orderItems OrderItem[] createdAt DateTime @default(now()) } ``` ### Prisma Client 自動產生 執行以下指令來產生 Prisma Client: ``` npx prisma generate ``` Prisma 會根據你定義的 schema 自動生成可用的 TypeScript 資料庫操作方法: ``` const users = await prisma.product.findMany(); ``` 你會得到: - 型別保護(不會打錯欄位) - 自動補全(更快開發) - 避免 SQL injection 3️⃣ Migrate 管理資料庫 使用 npx prisma migrate 自動同步 Prisma schema 到你的資料庫: ``` npx prisma migrate dev --name init ``` 這會產生: - 一份 migration SQL 檔案 ![截圖 2025-04-24 上午11.37.41](https://hackmd.io/_uploads/HJwzzEDJxx.png) - 自動更新資料庫結構 - 確保團隊協作時 schema 一致 ## 常用 CRUD 操作指令(以 product 為例) ### 1. 取得單筆資料(findUnique) ``` const product = await prisma.product.findUnique({ where: { id } }); ``` ### 2. 取得多筆資料(findMany) ``` const product = await prisma.product.findMany(); ``` ### 3. 條件搜尋(帶過濾條件) ``` const product = await prisma.product.findMany({ where: { price: 10 }, }); ``` ### 4. 新增資料(create) ``` const product = await prisma.product.create({ data: { name: body.name, price: body.price, stock: body.stock, description: body.description || '' } }); ``` ### 5. 修改資料(update) ``` const updated = await prisma.product.update({ where: { id }, data: { name: body.name, price: body.price, stock: body.stock, description: body.description } }); ``` ### 6. 刪除資料(delete) ``` prisma.product.delete({ where: { id } }) ``` ## 部署之前確認完成執行下面程式碼 ``` npx prisma migrate dev --name xxx ``` ``` npx prisma migrate deploy ``` 最後確認 migration 狀態 ``` npx prisma migrate status ``` 最後應該會顯示 ``` 1 migration found in prisma/migrations Database schema is up to date! ``` # 部署到Vercel #### 1. 註冊Vercel,直接使用GitHub登入 #### 2.新增部署專案,點擊**Add New**,選擇「 Project 」 ![截圖 2025-04-25 下午1.43.59](https://hackmd.io/_uploads/HyRIWjdyge.png) #### 3.選擇GitHub倉庫中,你要部署的專案。點擊 「 Import 」 ![截圖 2025-04-25 下午1.46.52](https://hackmd.io/_uploads/B1b9GoOJle.png) #### 4.進入畫面後,可以自訂Project Name,會根據你的 Project Name產生對應的網址。 ![截圖 2025-04-25 下午1.50.28](https://hackmd.io/_uploads/ByenMsO1xx.png) 如果你的專案中有2個子專案要分別部署的話,點擊Edit選擇目前想要部署的專案。 ![截圖 2025-04-25 下午1.52.26](https://hackmd.io/_uploads/HJvhQo_Jlg.png) 點擊後可以看到子專案有哪些,選擇你要的子專案。Vercel會直接辨識你的專案類型。 ![截圖 2025-04-25 下午1.57.20](https://hackmd.io/_uploads/BJAv4od1le.png) 最後按**Deploy**,讓專案開始部署吧! ![截圖 2025-04-25 下午1.58.40](https://hackmd.io/_uploads/SJ_lBi_1eg.png) 部署成功後,你可以在 Vercel 專案的 Domains 區塊中看到對應的網址。 接下來就可以把這個網址分享給其他人,讓大家可以瀏覽你完成的網站囉! 如果你有在這個專案中部署 API,那這個網址也會成為你 API 的對外路徑,記得在前端或其他應用中使用這個網址來呼叫 API。 ## Vercel的環境變數設定 ### 點擊Setting > Environment Variables ![截圖 2025-04-25 下午2.06.03](https://hackmd.io/_uploads/BkpPuodyex.png) ### 新增環境變數 直接在下面圖中紅色框框輸入你的環境變數Key和Value,輸入後按下Save。 ![截圖 2025-04-25 下午2.08.38](https://hackmd.io/_uploads/BJv5viuJlg.png) 然後往下滑動,會看到你剛剛新增的環境變數。 ![截圖 2025-04-25 下午2.06.42](https://hackmd.io/_uploads/SJKMOsdyee.png) ## 部署Vercel可能會發生的錯誤 ### 子專案中的Frontend,只是純靜態部署。 必須要在專案底下建立一個 vercel.json 來處理 rewrites ``` { "rewrites": [ { "source": "/(.*)", "destination": "/" } ] } ``` 這樣任何未知的路徑都會 fallback 到 /index.html,由前端框架接管路由。 否則無法正確處理「非根目錄」的路由,導致 **404 NOT_FOUND**。 ### 如果是使用Nuxt SRR的話,確認nuxt.config.ts 有設定以下的程式碼 ``` export default defineNuxtConfig({ ssr: true, nitro: { preset: 'vercel' } }); ``` # 參考資料 [Nuxt 3 Server API 與 Nitro Engine]( https://ithelp.ithome.com.tw/articles/10301197) [Nuxt](https://nuxt.com/docs/api) [pinia](https://pinia.vuejs.org/ssr/nuxt.html) [pinia-Persistedstate](https://prazdevs.github.io/pinia-plugin-persistedstate/zh/frameworks/nuxt.html) # GitHub [Shop](https://github.com/larryzeng123/larryShop) # 專案網站 [前台購物頁面](https://larry-shop-frontend-tawny.vercel.app/) [後台管理頁面](https://larry-shop-mauve.vercel.app/) [GitHub](https://github.com/Larryz1215/larryShop)