2025 OEM 網站開發筆記 [2] - 用 Dummy Data 熟悉前端開發 Workflow

React + Vite + TypeScript + Tailwind CSS + TanStack Query + Django + MySQL

Posted by Young on 2025-02-07
Estimated Reading Time 7 Minutes
Words 1.4k In Total

前言

先不用實際去後端寫 API Endpoint,直接用 Dummy data 來熟悉整套 Workflow,以列出 Dummy data 產品列表為例,這樣可以更快熟悉整套開發流程,並且可以先把前端的畫面先做出來,等到後端準備好後再直接接上 API Endpoint

後端 setup

  1. 建立 Django 專案
  2. 建立、啟用虛擬環境
  3. 下載並輸出此專案的套件清單(corsheaders、JWT、drf…)
1
2
3
4
5
django-admin startproject backend

pip install ...

pip freeze > requirements.txt

前端 setup

  1. 建立 React + Vite + TypeScript 專案
  2. 下載所有所需額外套件(@tanstack/react-query、axios…)
1
2
3
4
5
npm create vite@latest my-app --template react-ts

cd my-app

npm install

整合 tailwindCSS v4 及 shadcn/ui

導入 tailwindCSS v4 官方教學,現在步驟變更少,但跟 shadcn/ui 整合的方式跟以往不同了:

跑完 tailwindCSS v4 流程並確定沒問題後,到 shadcn/ui 官方教學 這一步一步做

tsconfig.json 加入 compilerOptions

1
2
3
4
5
6
7
8
9
10
11
// tsconfig.json
{
"files": [],
"references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }],
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}

然後執行

1
npm add -D @types/node

再到 vite.config.ts 加入相關 resolve 設定

1
2
3
4
5
6
7
8
9
10
11
12
...
import path from "path"

// https://vite.dev/config/
export default defineConfig({
plugins: [react(), tailwindcss()],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
});

注意這邊若直接複製官方教學的 pnpm dlx shadcn@canary init 會發現報不認得 dlx 指定的錯誤,所以這邊改成執行

1
npx shadcn@canary init

就能看到成功訊息並可以開始選擇 base color 了

1
2
3
4
5
6
7
8
9
10
✔ Preflight checks.
✔ Verifying framework. Found Vite.
✔ Validating Tailwind CSS config. Found v4.
✔ Validating import alias.
? Which color would you like to use as the base color? › - Use arrow-keys. Return to submit.
❯ Neutral
Gray
Zinc
Stone
Slate

接下來直接測試看是否能直接調用元件來使用,看看 Navbar 能否正常顯示,執行:

1
npx shadcn@latest add navigation-menu

此時就能看到 components 資料夾底下多了 ui/navigation-menu.tsx 檔案,

設定 React Query Client

設定 React Query Provider,這樣在任何組件內就都能使用 useQuery

1
2
3
4
5
6
7
8
9
10
11
12
// src/App.tsx
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";

const queryClient = new QueryClient();

ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
</React.StrictMode>
);

統一管理 API 請求,建立 API instance

  • 設定 baseURL,避免每次請求都寫完整 API 網址
  • 設定超時時間 (timeout)
  • 統一 headers
  • 處理 interceptors 來攔截請求 & 回應(例如自動加上 token)
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
// src/api/axiosInstance.ts
import axios from "axios";

const api = axios.create({
baseURL: "https://dummyjson.com", // 設定 API 基礎 URL
timeout: 5000, // 5 秒超時
headers: {
"Content-Type": "application/json",
},
});

// 請求攔截器(可選) → 這裡可以自動加上 token
api.interceptors.request.use(
(config) => {
// 假設有 token,則自動加到 headers
const token = localStorage.getItem("token");
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);

// 回應攔截器(可選) → 統一處理錯誤
api.interceptors.response.use(
(response) => response,
(error) => {
console.error("API 發生錯誤:", error);
return Promise.reject(error);
}
);

export default api;

有了 baseURL,往後就能像 getProducts() 這樣使用 api 來請求 https://dummyjson.com/products ,不用每次都手動寫 baseURL!

1
2
3
4
5
6
7
8
9
// import axios from "axios";
import api from "./axiosInstance";
import type { ProductListResponse } from "../types/products";

export const getProducts = async (): Promise<ProductListResponse> => {
const { data } = await api.get("/products"); // 這裡不需要寫完整 URL
return data;
};
``;

定義 types/ 來管理 TypeScript 型別

這樣 TypeScript 會確保 API 回傳的數據符合預期類型,不符合也能在編譯時期立即發現錯誤,在上方 getProducts() 中,就能確保回傳的數據是 ProductListResponse 類型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// src/types/products.ts
export interface Product {
id: number;
title: string;
description: string;
price: number;
discountPercentage: number;
rating: number;
stock: number;
brand: string;
category: string;
thumbnail: string;
images: string[];
}

export interface ProductListResponse {
products: Product[];
}

建立 hooks 來處理 API 請求

此專案使用 @tanstack/react-query 來處理 API 請求,與傳統這種 reducers 的方式不同:

  • 需要手動 dispatch 各種狀態 (FETCH_START, FETCH_SUCCESS, FETCH_ERROR)
  • 沒有 cache 機制,每次載入頁面都要重新請求
  • 沒有背景同步,數據可能會過時

傳統 reducer 寫法

1
2
3
4
5
6
7
8
9
10
11
12
13
// 列出所有產品
export const productListReducer = (state = { products: [] }, action) => {
switch (action.type) {
case PRODUCT_LIST_REQUEST: // 請求中
return { loading: true, products: [] };
case PRODUCT_LIST_SUCCESS: // 成功
return { loading: false, products: action.payload.products, page: action.payload.page, pages: action.payload.pages }; // 請求到資料,loading 結束
case PRODUCT_LIST_FAIL: // 錯誤
return { loading: false, error: action.payload };
default:
return state;
}
};

使用 useQuery

useQuery 會自動處理 loading、error、data 等狀態,並且有 cache 功能,避免重複請求:

  • 3 行程式碼就完成 useReducer 的所有功能
  • 內建 isLoading, data, error
  • 內建快 cache,5 分鐘內不重複請求
1
2
3
4
5
6
7
8
9
10
11
// src/hooks/useProducts.ts
import { useQuery } from "@tanstack/react-query";
import { getProducts } from "../api/products";

export const useProducts = () => {
return useQuery({
queryKey: ["products"],
queryFn: getProducts,
staleTime: 1000 * 60 * 5, // 5 分鐘內不重新請求
});
};

在 React 頁面元件中使用

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
// src/pages/HomePage.tsx
import { useProducts } from "../hooks/useProducts";
import { Product } from "../types/products";

const HomePage = () => {
const { data, isLoading, isError, error } = useProducts();

if (isLoading) return <p>載入中...</p>;
if (isError) return <p>發生錯誤:{error?.message}</p>;
return (
<div className='p-5'>
<h1 className='text-2xl font-bold mb-4'>產品列表</h1>
<div className='grid grid-cols-3 gap-4'>
{data?.products.map((product: Product) => (
<div key={product.id} className='border p-4 rounded shadow'>
<img src={product.thumbnail} alt={product.title} className='w-full h-40 object-cover rounded' />
<h2 className='text-lg font-semibold mt-2'>{product.title}</h2>
<p className='text-sm text-gray-600'>{product.description}</p>
<p className='text-lg font-bold mt-2'>${product.price}</p>
</div>
))}
</div>
</div>
);
};

export default HomePage;

成功畫面

product_list_done


若您覺得這篇文章對您有幫助,歡迎分享出去讓更多人看到⊂◉‿◉つ~


留言版