🔗 Angular + ASP.NET Core 全端整合
📌 前後端分離架構
┌─────────────────────────────────────────────────┐
│ 使用者瀏覽器 │
│ ┌────────────────────────────────────────────┐ │
│ │ Angular SPA (TypeScript) │ │
│ │ • 處理 UI 互動 │ │
│ │ • 路由管理 │ │
│ │ • 狀態管理 │ │
│ │ • 透過 HttpClient 呼叫 API │ │
│ └──────────────┬─────────────────────────────┘ │
│ │ HTTP 請求 (JSON) │
└─────────────────┼───────────────────────────────┘
│
┌─────────────────┼───────────────────────────────┐
│ ASP.NET Core │ Web API │
│ ┌──────────────▼─────────────────────────────┐ │
│ │ Controllers / Minimal API │ │
│ │ • 處理 HTTP 請求 │ │
│ │ • 商業邏輯 │ │
│ │ • 資料驗證 │ │
│ │ • 存取資料庫 │ │
│ └────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────┘
📌 HttpClient 呼叫 .NET API
Angular 端:建立 API Service
// api.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { environment } from '../environments/environment';
export interface Product {
id: number;
name: string;
price: number;
description: string;
}
@Injectable({ providedIn: 'root' })
export class ApiService {
// 根據環境切換 API 網址
private baseUrl = environment.apiUrl; // 例如 https://localhost:5001/api
constructor(private http: HttpClient) { }
getProducts(): Observable<Product[]> {
return this.http.get<Product[]>(`${this.baseUrl}/products`);
}
getProduct(id: number): Observable<Product> {
return this.http.get<Product>(`${this.baseUrl}/products/${id}`);
}
createProduct(product: Omit<Product, 'id'>): Observable<Product> {
return this.http.post<Product>(`${this.baseUrl}/products`, product);
}
updateProduct(id: number, product: Product): Observable<void> {
return this.http.put<void>(`${this.baseUrl}/products/${id}`, product);
}
deleteProduct(id: number): Observable<void> {
return this.http.delete<void>(`${this.baseUrl}/products/${id}`);
}
}
ASP.NET Core 端:API Controller
// ProductsController.cs
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
private readonly AppDbContext _db;
public ProductsController(AppDbContext db) => _db = db;
[HttpGet]
public async Task<ActionResult<List<Product>>> GetAll()
{
return await _db.Products.ToListAsync();
}
[HttpGet("{id}")]
public async Task<ActionResult<Product>> Get(int id)
{
var product = await _db.Products.FindAsync(id);
return product is null ? NotFound() : Ok(product);
}
[HttpPost]
public async Task<ActionResult<Product>> Create(Product product)
{
_db.Products.Add(product);
await _db.SaveChangesAsync();
return CreatedAtAction(nameof(Get), new { id = product.Id }, product);
}
}
📌 CORS 設定
前後端分離時,Angular(localhost:4200)和 .NET API(localhost:5001)在不同的 origin,
瀏覽器會阻擋跨域請求。需要在 .NET 端設定 CORS:
// Program.cs
var builder = WebApplication.CreateBuilder(args);
// 註冊 CORS 政策
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowAngular", policy =>
{
policy.WithOrigins("http://localhost:4200") // Angular 開發伺服器
.AllowAnyHeader() // 允許任何請求標頭
.AllowAnyMethod() // 允許 GET, POST, PUT, DELETE
.AllowCredentials(); // 允許傳送 Cookie
});
});
var app = builder.Build();
// 啟用 CORS(要放在 UseRouting 之後、UseAuthorization 之前)
app.UseCors("AllowAngular");
⚠️ 生產環境請勿使用
AllowAnyOrigin(),應該限定特定的前端網域。
📌 JWT Interceptor 攔截器
Angular 的 HTTP Interceptor 可以在每個請求自動加上 JWT Token:
// auth.interceptor.ts
import { HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
import { AuthService } from './auth.service';
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const authService = inject(AuthService);
const token = authService.getToken();
if (token) {
// 複製請求並加上 Authorization 標頭
const authReq = req.clone({
setHeaders: {
Authorization: `Bearer ${token}`
}
});
return next(authReq);
}
return next(req);
};
// app.config.ts — 註冊攔截器
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { authInterceptor } from './auth.interceptor';
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(withInterceptors([authInterceptor])),
provideRouter(routes)
]
};
Auth Service
// auth.service.ts
@Injectable({ providedIn: 'root' })
export class AuthService {
private tokenKey = 'jwt_token';
constructor(private http: HttpClient, private router: Router) { }
login(credentials: { email: string; password: string }): Observable<any> {
return this.http.post<{ token: string }>('/api/auth/login', credentials).pipe(
tap(response => {
localStorage.setItem(this.tokenKey, response.token);
})
);
}
logout() {
localStorage.removeItem(this.tokenKey);
this.router.navigate(['/login']);
}
getToken(): string | null {
return localStorage.getItem(this.tokenKey);
}
isLoggedIn(): boolean {
const token = this.getToken();
if (!token) return false;
// 檢查 token 是否過期(解析 JWT payload)
const payload = JSON.parse(atob(token.split('.')[1]));
return payload.exp > Date.now() / 1000;
}
}
📌 SignalR 即時通訊整合
SignalR 讓 Angular 和 .NET 之間建立雙向即時通訊:
ASP.NET Core Hub
// ChatHub.cs
using Microsoft.AspNetCore.SignalR;
public class ChatHub : Hub
{
public async Task SendMessage(string user, string message)
{
// 廣播訊息給所有連線的客戶端
await Clients.All.SendAsync("ReceiveMessage", user, message);
}
public async Task JoinRoom(string room)
{
await Groups.AddToGroupAsync(Context.ConnectionId, room);
await Clients.Group(room).SendAsync("ReceiveMessage", "系統", $"{Context.ConnectionId} 加入了 {room}");
}
}
Angular 端:SignalR Client
// chat.service.ts
import { Injectable } from '@angular/core';
import * as signalR from '@microsoft/signalr';
import { BehaviorSubject } from 'rxjs';
export interface ChatMessage {
user: string;
message: string;
timestamp: Date;
}
@Injectable({ providedIn: 'root' })
export class ChatService {
private hubConnection!: signalR.HubConnection;
private messagesSubject = new BehaviorSubject<ChatMessage[]>([]);
messages$ = this.messagesSubject.asObservable();
connect(token: string) {
this.hubConnection = new signalR.HubConnectionBuilder()
.withUrl('https://localhost:5001/chatHub', {
accessTokenFactory: () => token // JWT 認證
})
.withAutomaticReconnect() // 斷線自動重連
.build();
// 監聽伺服器發送的訊息
this.hubConnection.on('ReceiveMessage', (user: string, message: string) => {
const current = this.messagesSubject.value;
this.messagesSubject.next([...current, {
user, message, timestamp: new Date()
}]);
});
// 啟動連線
this.hubConnection.start()
.then(() => console.log('SignalR 已連線'))
.catch(err => console.error('SignalR 連線失敗', err));
}
sendMessage(user: string, message: string) {
this.hubConnection.invoke('SendMessage', user, message);
}
joinRoom(room: string) {
this.hubConnection.invoke('JoinRoom', room);
}
disconnect() {
this.hubConnection.stop();
}
}
// chat.component.ts
@Component({
selector: 'app-chat',
template: `
<div class="chat-room">
<div class="messages">
<div *ngFor="let msg of messages$ | async" class="message">
<strong>{{ msg.user }}:</strong>{{ msg.message }}
<small>{{ msg.timestamp | date:'HH:mm:ss' }}</small>
</div>
</div>
<div class="input-area">
<input [(ngModel)]="newMessage" (keyup.enter)="send()" placeholder="輸入訊息...">
<button (click)="send()">送出</button>
</div>
</div>
`
})
export class ChatComponent implements OnInit, OnDestroy {
messages$ = this.chatService.messages$;
newMessage = '';
constructor(
private chatService: ChatService,
private authService: AuthService
) { }
ngOnInit() {
const token = this.authService.getToken() ?? '';
this.chatService.connect(token);
}
send() {
if (this.newMessage.trim()) {
this.chatService.sendMessage('我', this.newMessage);
this.newMessage = '';
}
}
ngOnDestroy() {
this.chatService.disconnect();
}
}
📌 Environment 設定
// environments/environment.ts(開發環境)
export const environment = {
production: false,
apiUrl: 'https://localhost:5001/api'
};
// environments/environment.prod.ts(生產環境)
export const environment = {
production: true,
apiUrl: 'https://myapp.azurewebsites.net/api'
};
📌 小結
- Angular + ASP.NET Core 是企業級全端組合
- 前後端透過 REST API(JSON)溝通
- CORS 設定是前後端分離的必要步驟
- JWT Interceptor 自動在每個請求加上認證 Token
- SignalR 提供雙向即時通訊(聊天室、通知推播)
- 使用 environment 檔案管理不同環境的 API 位址
- TypeScript 讓前後端的資料結構可以保持一致(前端 interface 對應後端 DTO)