🔗 Vue + ASP.NET Core 全端整合
📌 前後端分離 vs 同源部署
前後端分離
Vue (前端) ASP.NET Core (後端)
localhost:5173 ←→ localhost:5000
Vercel / Netlify ←→ Azure / Railway
↓ ↓
瀏覽器載入 HTML/JS 提供 API (JSON)
呼叫後端 API 處理商業邏輯
渲染畫面 存取資料庫
同源部署(Vue 打包後放進 .NET 專案)
ASP.NET Core
wwwroot/
├── index.html ← Vue 打包產出
├── assets/
│ ├── index-xxx.js
│ └── index-xxx.css
└── favicon.ico
Controllers/
└── ApiController.cs ← 後端 API
📌 Vue 呼叫 ASP.NET Core API
後端 API(C#)
// Controllers/TodoController.cs
[ApiController]
[Route("api/[controller]")]
public class TodoController : ControllerBase
{
private readonly AppDbContext _db;
public TodoController(AppDbContext db) => _db = db;
// GET api/todo
[HttpGet]
public async Task<IActionResult> GetAll()
{
var todos = await _db.Todos.OrderByDescending(t => t.CreatedAt).ToListAsync();
return Ok(todos);
}
// POST api/todo
[HttpPost]
public async Task<IActionResult> Create([FromBody] CreateTodoDto dto)
{
var todo = new Todo
{
Title = dto.Title,
IsCompleted = false,
CreatedAt = DateTime.UtcNow
};
_db.Todos.Add(todo);
await _db.SaveChangesAsync();
return CreatedAtAction(nameof(GetAll), new { id = todo.Id }, todo);
}
// PUT api/todo/5
[HttpPut("{id}")]
public async Task<IActionResult> Update(int id, [FromBody] UpdateTodoDto dto)
{
var todo = await _db.Todos.FindAsync(id);
if (todo == null) return NotFound();
todo.Title = dto.Title ?? todo.Title;
todo.IsCompleted = dto.IsCompleted ?? todo.IsCompleted;
await _db.SaveChangesAsync();
return Ok(todo);
}
// DELETE api/todo/5
[HttpDelete("{id}")]
public async Task<IActionResult> Delete(int id)
{
var todo = await _db.Todos.FindAsync(id);
if (todo == null) return NotFound();
_db.Todos.Remove(todo);
await _db.SaveChangesAsync();
return NoContent();
}
}
前端 API 封裝(Vue)
// api/todo.js
const API_BASE = import.meta.env.VITE_API_BASE_URL || '/api'
// 這些全都是原生 JavaScript 的 fetch API!
// Vue 沒有自己的 HTTP 客戶端
export async function fetchTodos() {
const response = await fetch(`${API_BASE}/todo`)
if (!response.ok) throw new Error('取得待辦清單失敗')
return response.json()
}
export async function createTodo(title) {
const response = await fetch(`${API_BASE}/todo`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title })
})
if (!response.ok) throw new Error('新增失敗')
return response.json()
}
export async function updateTodo(id, data) {
const response = await fetch(`${API_BASE}/todo/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
})
if (!response.ok) throw new Error('更新失敗')
return response.json()
}
export async function deleteTodo(id) {
const response = await fetch(`${API_BASE}/todo/${id}`, {
method: 'DELETE'
})
if (!response.ok) throw new Error('刪除失敗')
}
Vue 元件使用 API
<!-- views/TodoApp.vue -->
<template>
<div class="todo-app">
<h1>Vue + .NET 待辦應用</h1>
<!-- 新增 -->
<form @submit.prevent="handleAdd">
<input v-model="newTitle" placeholder="新增待辦..." />
<button type="submit" :disabled="loading">
{{ loading ? '處理中...' : '新增' }}
</button>
</form>
<!-- 載入狀態 -->
<div v-if="loading && !todos.length">載入中...</div>
<div v-if="error" class="error">{{ error }}</div>
<!-- 列表 -->
<ul>
<li v-for="todo in todos" :key="todo.id"
:class="{ completed: todo.isCompleted }">
<input
type="checkbox"
:checked="todo.isCompleted"
@change="toggleTodo(todo)"
/>
<span>{{ todo.title }}</span>
<button @click="handleDelete(todo.id)">🗑️</button>
</li>
</ul>
<p>共 {{ todos.length }} 項,{{ remainingCount }} 項未完成</p>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { fetchTodos, createTodo, updateTodo, deleteTodo } from '@/api/todo'
const todos = ref([])
const newTitle = ref('')
const loading = ref(false)
const error = ref('')
const remainingCount = computed(() =>
todos.value.filter(t => !t.isCompleted).length
)
// 載入待辦清單
onMounted(async () => {
loading.value = true
try {
todos.value = await fetchTodos()
} catch (err) {
error.value = err.message
} finally {
loading.value = false
}
})
async function handleAdd() {
if (!newTitle.value.trim()) return
loading.value = true
try {
const todo = await createTodo(newTitle.value.trim())
todos.value.unshift(todo)
newTitle.value = ''
} catch (err) {
error.value = err.message
} finally {
loading.value = false
}
}
async function toggleTodo(todo) {
try {
const updated = await updateTodo(todo.id, {
isCompleted: !todo.isCompleted
})
const index = todos.value.findIndex(t => t.id === todo.id)
if (index !== -1) todos.value[index] = updated
} catch (err) {
error.value = err.message
}
}
async function handleDelete(id) {
try {
await deleteTodo(id)
todos.value = todos.value.filter(t => t.id !== id)
} catch (err) {
error.value = err.message
}
}
</script>
📌 CORS 設定
前後端分離時,瀏覽器會阻擋不同來源的請求(CORS 政策)。
// Program.cs — ASP.NET Core CORS 設定
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddCors(options =>
{
options.AddPolicy("VueDev", policy =>
{
policy.WithOrigins("http://localhost:5173") // Vue 開發伺服器
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials(); // 如果要帶 Cookie
});
});
var app = builder.Build();
app.UseCors("VueDev"); // 套用 CORS 政策
Vite 開發代理(免設 CORS)
// vite.config.js
export default defineConfig({
server: {
proxy: {
// 開發時 /api/* 的請求轉發到 .NET 後端
'/api': {
target: 'http://localhost:5000',
changeOrigin: true
}
}
}
})
📌 JWT 認證整合
後端發 Token
// Controllers/AuthController.cs
[ApiController]
[Route("api/auth")]
public class AuthController : ControllerBase
{
[HttpPost("login")]
public IActionResult Login([FromBody] LoginDto dto)
{
// 驗證帳號密碼...
var token = GenerateJwtToken(user);
return Ok(new { token, user = new { user.Name, user.Email } });
}
private string GenerateJwtToken(User user)
{
var key = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes("your-secret-key-at-least-32-chars"));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var claims = new[]
{
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
new Claim(ClaimTypes.Name, user.Name),
new Claim(ClaimTypes.Email, user.Email)
};
var token = new JwtSecurityToken(
issuer: "DotNetLearning",
audience: "VueApp",
claims: claims,
expires: DateTime.UtcNow.AddDays(7),
signingCredentials: creds
);
return new JwtSecurityTokenHandler().WriteToken(token);
}
}
前端存取 Token
// api/auth.js
export async function login(email, password) {
const res = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
})
if (!res.ok) throw new Error('登入失敗')
const data = await res.json()
// 存到 localStorage(原生瀏覽器 API)
localStorage.setItem('token', data.token)
localStorage.setItem('user', JSON.stringify(data.user))
return data
}
// 帶 Token 的 API 請求
export async function authFetch(url, options = {}) {
const token = localStorage.getItem('token')
return fetch(url, {
...options,
headers: {
...options.headers,
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}` // JWT 放在 Header
}
})
}
📌 SignalR 即時通訊整合
後端 Hub
// Hubs/ChatHub.cs
public class ChatHub : Hub
{
public async Task SendMessage(string user, string message)
{
// 廣播訊息給所有連線的客戶端
await Clients.All.SendAsync("ReceiveMessage", user, message);
}
public override async Task OnConnectedAsync()
{
await Clients.All.SendAsync("UserJoined", Context.ConnectionId);
await base.OnConnectedAsync();
}
}
Vue 前端連接 SignalR
npm install @microsoft/signalr
// composables/useSignalR.js
import { ref, onMounted, onUnmounted } from 'vue'
import { HubConnectionBuilder, LogLevel } from '@microsoft/signalr'
export function useSignalR(hubUrl) {
const connection = ref(null)
const isConnected = ref(false)
const messages = ref([])
onMounted(async () => {
// SignalR 客戶端也是 JavaScript 寫的!
connection.value = new HubConnectionBuilder()
.withUrl(hubUrl)
.withAutomaticReconnect()
.configureLogging(LogLevel.Information)
.build()
// 監聽伺服端推送的訊息
connection.value.on('ReceiveMessage', (user, message) => {
messages.value.push({ user, message, time: new Date() })
})
connection.value.on('UserJoined', (userId) => {
console.log(`使用者 ${userId} 加入了`)
})
try {
await connection.value.start()
isConnected.value = true
console.log('SignalR 已連接')
} catch (err) {
console.error('SignalR 連接失敗:', err)
}
})
onUnmounted(() => {
connection.value?.stop()
})
// 傳送訊息
async function sendMessage(user, message) {
if (!connection.value) return
await connection.value.invoke('SendMessage', user, message)
}
return { isConnected, messages, sendMessage }
}
<!-- views/Chat.vue -->
<template>
<div class="chat">
<div class="status">
{{ isConnected ? '🟢 已連接' : '🔴 連接中...' }}
</div>
<div class="messages">
<div v-for="(msg, i) in messages" :key="i" class="message">
<strong>{{ msg.user }}</strong>: {{ msg.message }}
<small>{{ msg.time.toLocaleTimeString() }}</small>
</div>
</div>
<form @submit.prevent="handleSend">
<input v-model="inputMessage" placeholder="輸入訊息..." />
<button type="submit" :disabled="!isConnected">送出</button>
</form>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useSignalR } from '@/composables/useSignalR'
const { isConnected, messages, sendMessage } = useSignalR('/chathub')
const inputMessage = ref('')
const userName = ref('匿名使用者')
async function handleSend() {
if (!inputMessage.value.trim()) return
await sendMessage(userName.value, inputMessage.value.trim())
inputMessage.value = ''
}
</script>
📌 總結:Vue 全端開發架構
┌─────────────────────────────────────────┐
│ Vue.js 前端 │
│ ┌─────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 元件系統 │ │ Vue Router│ │ Pinia │ │
│ └─────────┘ └──────────┘ └──────────┘ │
│ ↕ fetch / axios ↕ │
│ ↕ SignalR (WebSocket) ↕ │
├─────────────────────────────────────────┤
│ ASP.NET Core 後端 │
│ ┌─────────┐ ┌──────────┐ ┌──────────┐ │
│ │Controller│ │ SignalR │ │ 中介軟體 │ │
│ │ (API) │ │ (Hub) │ │ (Auth) │ │
│ └─────────┘ └──────────┘ └──────────┘ │
│ ↕ Entity Framework ↕ │
│ ┌──────────────────────────────────┐ │
│ │ 資料庫 (SQL/PostgreSQL) │ │
│ └──────────────────────────────────┘ │
└─────────────────────────────────────────┘
核心重點回顧
- Vue 是 JavaScript 框架——不是語言、不是原生語法
ref()、computed()、v-model都是 Vue 封裝的 APIfetch()、localStorage、WebSocket是瀏覽器原生 API- 前後端溝通用 REST API (HTTP) 或 SignalR (WebSocket)
- CORS 是瀏覽器的安全機制,開發時可用 Vite Proxy 繞過
- JWT Token 存前端,每次 API 請求帶在 Header 裡
💡 全端整合小提醒
- 開發時用 Vite Proxy 比設 CORS 更方便
- JWT 不要存敏感資料(它只是 Base64 編碼,不是加密)
- SignalR 連線斷掉會自動重連(
withAutomaticReconnect()) - 正式部署時用反向代理(Nginx)把前後端放在同一個 domain,免去 CORS 問題
- 記住:學好 JavaScript 基礎是一切的根本! 框架會變,語言不會