🔗 React + ASP.NET Core 全端整合
📌 前後端分離架構
前後端分離架構:
React (前端) ASP.NET Core (後端)
┌──────────────┐ ┌──────────────────┐
│ 瀏覽器 SPA │ ←JSON→ │ Web API │
│ localhost:5173│ │ localhost:5000 │
│ JSX → UI │ │ Controller/ │
│ React Router │ │ Minimal API │
│ Zustand/Redux│ │ Entity Framework │
└──────────────┘ └──────────────────┘
⚠️ React 前端和 .NET 後端是完全獨立的應用, 透過 HTTP API(JSON 格式)溝通。這就是為什麼 React 不是原生 JS—— 它需要編譯打包,而 .NET 負責資料處理和商業邏輯。
🌐 Fetch / Axios 呼叫 .NET API
使用 Fetch(原生)
// hooks/useApi.js
import { useState, useEffect } from 'react';
const API_BASE = import.meta.env.VITE_API_URL || 'https://localhost:5001';
export function useFetch(endpoint) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController(); // 用於取消請求
fetch(`${API_BASE}${endpoint}`, {
signal: controller.signal,
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`,
},
})
.then(res => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
})
.then(setData)
.catch(err => {
if (err.name !== 'AbortError') setError(err);
})
.finally(() => setLoading(false));
return () => controller.abort(); // 元件卸載時取消請求
}, [endpoint]);
return { data, loading, error };
}
// 使用
function ChapterList() {
const { data: chapters, loading, error } = useFetch('/api/chapters');
if (loading) return <p>載入中...</p>;
if (error) return <p>錯誤:{error.message}</p>;
return (
<ul>
{chapters.map(ch => <li key={ch.id}>{ch.title}</li>)}
</ul>
);
}
使用 Axios(更方便)
npm install axios
// services/api.js
import axios from 'axios';
const api = axios.create({
baseURL: import.meta.env.VITE_API_URL || 'https://localhost:5001',
timeout: 10000,
});
// 請求攔截器:自動附加 Token
api.interceptors.request.use(config => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// 回應攔截器:統一錯誤處理
api.interceptors.response.use(
response => response,
error => {
if (error.response?.status === 401) {
localStorage.removeItem('token');
window.location.href = '/login'; // Token 過期,導向登入
}
return Promise.reject(error);
}
);
export default api;
// 使用 axios 實例
import api from '../services/api';
async function createChapter(data) {
const response = await api.post('/api/chapters', data);
return response.data;
}
🔒 CORS 設定(ASP.NET Core 端)
// Program.cs — 設定 CORS
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddCors(options =>
{
options.AddPolicy("ReactApp", policy =>
{
policy.WithOrigins("http://localhost:5173", // Vite 開發伺服器
"https://your-app.vercel.app") // 部署網址
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials(); // 如果要帶 Cookie
});
});
var app = builder.Build();
app.UseCors("ReactApp"); // 在 UseRouting 之後
⚠️ 什麼是 CORS? 瀏覽器安全策略:前端(localhost:5173)不能直接呼叫不同網域的 API(localhost:5001)。 CORS 設定告訴瀏覽器「這個前端是被允許的」。
🔑 JWT 認證流程
JWT 認證流程:
1. 使用者輸入帳密 → React 送 POST /api/auth/login
2. .NET 驗證成功 → 回傳 JWT Token
3. React 存到 localStorage
4. 之後每次 API 請求都帶上 Authorization: Bearer {token}
5. .NET 驗證 Token 是否有效
React 端
// context/AuthContext.jsx
import { createContext, useContext, useState, useEffect } from 'react';
import api from '../services/api';
const AuthContext = createContext();
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
// 啟動時檢查 Token
useEffect(() => {
const token = localStorage.getItem('token');
if (token) {
api.get('/api/auth/me')
.then(res => setUser(res.data))
.catch(() => localStorage.removeItem('token'))
.finally(() => setLoading(false));
} else {
setLoading(false);
}
}, []);
const login = async (username, password) => {
const res = await api.post('/api/auth/login', { username, password });
const { token, user } = res.data;
localStorage.setItem('token', token);
setUser(user);
return user;
};
const logout = () => {
localStorage.removeItem('token');
setUser(null);
};
return (
<AuthContext.Provider value={{ user, login, logout, loading }}>
{children}
</AuthContext.Provider>
);
}
export const useAuth = () => useContext(AuthContext);
ASP.NET Core 端
// AuthController.cs
[ApiController]
[Route("api/auth")]
public class AuthController : ControllerBase
{
[HttpPost("login")]
public IActionResult Login([FromBody] LoginDto dto)
{
// 驗證帳密(簡化範例)
var user = _db.Users.FirstOrDefault(
u => u.Username == dto.Username);
if (user == null || !VerifyPassword(dto.Password, user.PasswordHash))
return Unauthorized("帳號或密碼錯誤");
// 產生 JWT Token
var token = GenerateJwtToken(user);
return Ok(new { token, user = new { user.Id, user.Username } });
}
[HttpGet("me")]
[Authorize] // 需要有效 Token
public IActionResult GetMe()
{
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
var user = _db.Users.Find(int.Parse(userId));
return Ok(new { user.Id, user.Username });
}
}
⚡ SignalR 即時通訊
SignalR 讓 React 和 .NET 之間建立 WebSocket 連線,實現即時推播。
ASP.NET Core 端
// Hubs/ChatHub.cs
public class ChatHub : Hub
{
public async Task SendMessage(string user, string message)
{
// 廣播給所有連線的客戶端
await Clients.All.SendAsync("ReceiveMessage", user, message);
}
public async Task JoinRoom(string roomName)
{
await Groups.AddToGroupAsync(Context.ConnectionId, roomName);
}
}
// Program.cs
builder.Services.AddSignalR();
app.MapHub<ChatHub>("/chatHub");
React 端
npm install @microsoft/signalr
// hooks/useSignalR.js
import { useEffect, useRef, useState } from 'react';
import { HubConnectionBuilder, LogLevel } from '@microsoft/signalr';
export function useSignalR() {
const [messages, setMessages] = useState([]);
const connectionRef = useRef(null);
useEffect(() => {
const connection = new HubConnectionBuilder()
.withUrl('https://localhost:5001/chatHub', {
accessTokenFactory: () => localStorage.getItem('token'),
})
.withAutomaticReconnect()
.configureLogging(LogLevel.Information)
.build();
// 監聽伺服器推播
connection.on('ReceiveMessage', (user, message) => {
setMessages(prev => [...prev, { user, message, time: new Date() }]);
});
connection.start()
.then(() => console.log('SignalR 已連線'))
.catch(err => console.error('SignalR 連線失敗', err));
connectionRef.current = connection;
return () => {
connection.stop();
};
}, []);
const sendMessage = async (user, message) => {
await connectionRef.current?.invoke('SendMessage', user, message);
};
return { messages, sendMessage };
}
// components/Chat.jsx
function Chat() {
const { messages, sendMessage } = useSignalR();
const [input, setInput] = useState('');
const { user } = useAuth();
const handleSend = () => {
if (input.trim()) {
sendMessage(user.username, input);
setInput('');
}
};
return (
<div>
<div style={{ height: '400px', overflowY: 'auto' }}>
{messages.map((msg, i) => (
<div key={i}>
<strong>{msg.user}</strong>: {msg.message}
<small>{msg.time.toLocaleTimeString()}</small>
</div>
))}
</div>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSend()}
/>
<button onClick={handleSend}>送出</button>
</div>
);
}
🛠️ 完整範例:React + .NET 待辦應用結構
專案結構:
├── backend/ # ASP.NET Core
│ ├── Controllers/
│ │ ├── AuthController.cs # 認證 API
│ │ └── TodoController.cs # CRUD API
│ ├── Models/
│ │ └── Todo.cs
│ ├── Data/
│ │ └── AppDbContext.cs
│ └── Program.cs # CORS + SignalR + JWT 設定
│
├── frontend/ # React (Vite)
│ ├── src/
│ │ ├── components/
│ │ │ ├── TodoList.jsx
│ │ │ └── Chat.jsx
│ │ ├── context/
│ │ │ └── AuthContext.jsx
│ │ ├── hooks/
│ │ │ ├── useApi.js
│ │ │ └── useSignalR.js
│ │ ├── services/
│ │ │ └── api.js # Axios 實例
│ │ ├── pages/
│ │ │ ├── Login.jsx
│ │ │ └── Dashboard.jsx
│ │ ├── App.jsx # 路由設定
│ │ └── main.jsx # 進入點
│ └── package.json
✅ 本章重點
| 主題 | 技術 |
|---|---|
| API 呼叫 | Fetch / Axios + 攔截器 |
| CORS | .NET 端 AddCors 設定 |
| JWT 認證 | login → 存 Token → 每次請求帶上 |
| 即時通訊 | SignalR Hub + @microsoft/signalr |
| 專案結構 | frontend/ + backend/ 分離 |
💡 React + ASP.NET Core 是業界常見的全端組合, React 負責 UI 和使用者互動,.NET 負責 API、資料庫和商業邏輯。 兩者透過 JSON API 溝通,各自獨立開發和部署。