☕ NEW! 完成新手任務即可參加抽獎!LINE 星巴克禮券等你拿,名額有限!        🎉 推廣活動:邀請好友註冊 DevLearn,累積推薦抽 LINE 星巴克禮券! 活動詳情 →        🔥 活動期間 2026/4/1 - 5/31 |已有 0 人參加       
vue 進階

🔗 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)      │   │
│  └──────────────────────────────────┘   │
└─────────────────────────────────────────┘

核心重點回顧

  1. Vue 是 JavaScript 框架——不是語言、不是原生語法
  2. ref()computed()v-model 都是 Vue 封裝的 API
  3. fetch()localStorageWebSocket 是瀏覽器原生 API
  4. 前後端溝通用 REST API (HTTP) 或 SignalR (WebSocket)
  5. CORS 是瀏覽器的安全機制,開發時可用 Vite Proxy 繞過
  6. JWT Token 存前端,每次 API 請求帶在 Header 裡

💡 全端整合小提醒

  • 開發時用 Vite Proxy 比設 CORS 更方便
  • JWT 不要存敏感資料(它只是 Base64 編碼,不是加密)
  • SignalR 連線斷掉會自動重連(withAutomaticReconnect()
  • 正式部署時用反向代理(Nginx)把前後端放在同一個 domain,免去 CORS 問題
  • 記住:學好 JavaScript 基礎是一切的根本! 框架會變,語言不會

💡 大家的想法 · 0

載入中...
💬 即時聊天室 🟢 0 人在線
😀 😎 🤓 💻 🎮 🎸 🔥
➕ 新問題
📋 我的工單
💬 LINE 社群
🔒
需要註冊才能使用此功能
註冊帳號即可解鎖測驗、遊戲、簽到、筆記下載等所有功能,完全免費!
免費註冊