如果你看到主控台出現 .「... CORS error…」這個錯誤, 他是來自於 同源政策(Same-Origin Policy) 的關係,這個政策是瀏覽器內建的安全機制,顧名思義就是只允許同來源的請求。如果你從不同域名、協議或埠口的伺服器發起資源請求,它就會把你擋下來。
通常是開發階段才會需要前端來處理,如果是到測試環境跟正式環境,就要由後端設定CORS的允許網址,或是在 同源主機 上部署,或是在 devOps 的時候設定 反向代理。
而這篇會提供在開發環境時候的設定跟測試、開發時的nginx範例。
說個題外話,雖然多學一點是一點,也會常聽到就多幫一點忙,但我覺得還是要區分清楚工作範圍跟權責,不是單純的收下一句「能者多勞」,如果要你部署跟調整設定的話,你會的就不只是前端的工作,而是你執行了前端跟devOps的工作,你也學會了前端跟devOps,談薪的時候就能明確的知道自己的價值。
環境 | 解決方案 | 前端設定位置 | 後端/代理設定位置 | 說明 |
---|---|---|---|---|
開發 | Angular CLI 代理 | proxy.conf.json |
無需額外設定 | 開發伺服器處理代理 |
測試/正式 | 後端 CORS | 無需額外設定 | 後端應用程式伺服器 | API 直接允許跨域 |
測試/正式 | Nginx 反向代理 | 無需額外設定 | Web 伺服器 (Nginx) | 代理層統一處理 |
測試/正式 | Docker 反向代理 | 無需額外設定 | 容器編排設定 | 容器間網路處理 |
瀏覽器是在發送請求「之前」就知道是否跨域
URL 1 | URL 2 | 是否同源 | 原因 |
---|---|---|---|
http://localhost:4200 |
http://localhost:4200/api |
✅ | 路徑不影響同源判斷 |
http://localhost:4200 |
http://localhost:4200/api/users |
✅ | 路徑不影響同源判斷 |
http://localhost:4200 |
http://localhost:3000 |
❌ | port 不同 (4200 vs 3000) |
http://localhost:4200 |
https://localhost:4200 |
❌ | protocol 不同 (http vs https) |
http://localhost:4200 |
http://127.0.0.1:4200 |
❌ | host 不同 (localhost vs 127.0.0.1) |
http://localhost |
http://localhost:80 |
✅ | HTTP 預設 port 80 |
https://localhost |
https://localhost:443 |
✅ | HTTPS 預設 port 443 |
http://example.com |
http://www.example.com |
❌ | subdomain 不同 |
在開發環境的時候可以這樣設定,請在跟angular.js同層的位置放入proxy.json,也要記得在 angular.json裡面告訴專案啟動的時候要使用proxy.json這個檔案。
我就沒有一一列出所有方法囉!以下是參考概念
proxy.conf.json
*// proxy.conf.json*
{
"/api/*": {
"target": "http://localhost:3000", // 根據後端實際提供url設定
"secure": false, //此處設定跟http有關,http是false,https是true
}
}
專案資料夾結構(建議位置)
my-angular-project/
├── src/
│ ├── app/
│ ├── assets/
│ └── index.html
├── angular.json ← 與這個同一層
├── package.json ← 與這個同一層
├── proxy.conf.json ← 放在這裡
├── tsconfig.json
└── node_modules/
angular.json
{
"projects": {
"your-app-name": {
"architect": {
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"proxyConfig": "proxy.conf.json" ← 放在這裡,在開發環境使用
}
}
}
}
}
}
這裡提供nginx.conf的設定,理論上把讀取編譯檔的路徑換掉就好,就是這段 [ root {檔案路徑}/dist/{檔案路徑} ] ,大概類似像這樣 root C:/Users/dora/Documents/itHelp/dist/front;
這份檔案要放到你電腦中的nginx資料夾中,端看你電腦的OS種類位置會有不同,可以再問問GPT或是留言跟我討論。
# nginx.conf - Angular + API 反向代理配置
# 全局設置
#user nobody;
worker_processes 1; # 工作進程數量,通常設為 CPU 核心數
events {
worker_connections 1024; # 每個工作進程的最大連接數
}
http {
# 基本設置
include mime.types; # 包含 MIME 類型定義
default_type application/octet-stream;
sendfile on; # 高效文件傳輸
keepalive_timeout 65; # 連接保持時間
server {
listen 80; # 監聽 80 端口
server_name localhost; # 伺服器名稱
# Angular 打包後的靜態文件路徑
root {檔案路徑}/dist/{檔案路徑};
index index.html;
# 啟用 Gzip 壓縮
gzip on;
gzip_types text/css application/javascript application/json image/svg+xml;
# API 反向代理 - 解決跨域問題的關鍵
location /api/ {
proxy_pass http://localhost:3000; # 轉發到後端 API
proxy_http_version 1.1;
proxy_set_header Host $host; # 保持原始 Host
proxy_set_header X-Real-IP $remote_addr; # 客戶端真實 IP
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme; # 原始協議
proxy_set_header Upgrade $http_upgrade; # WebSocket 支援
proxy_set_header Connection 'upgrade';
proxy_cache_bypass $http_upgrade;
}
# 靜態資源快取設置
location ~* \.(js|mjs|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|otf)$ {
expires 1y; # 快取 1 年
add_header Cache-Control "public, immutable";
try_files $uri =404; # 資源不存在時返回 404
}
# Angular SPA 路由支援
location / {
try_files $uri $uri/ /index.html; # 找不到文件時返回 index.html
}
# 錯誤頁面設置
error_page 404 /404.html;
error_page 500 502 503 504 /50x.html;
# 安全 Headers
add_header X-Frame-Options "SAMEORIGIN" always; # 防止點擊劫持
add_header X-Content-Type-Options "nosniff" always; # 防止 MIME 嗅探
add_header X-XSS-Protection "1; mode=block" always; # XSS 防護
# 隱藏 Nginx 版本資訊
server_tokens off;
}
}
反向代理其實就是轉接、轉寄、轉發的動作,以寫信舉例的話,這間公司不允許你寄信給國外公司,所以你寄給國內代理公司,國內代理公司就會再幫你轉寄給國外。
你 → 寫信給「國內代理地址」→ 代理公司 → 轉寄到國外朋友
│ │ │
│ 看起來是國內信件 │ 實際國際轉寄
│ │ │
公司政策:「國內信件,OK!」 隱藏的轉寄過程 真正的收件人
反向代理的架構圖如下,從代理伺服器代理到API伺服器上
瀏覽器請求 http://localhost:80
↓
代理伺服器 (port 80)
│ 實現方式:Node.js 或 Nginx │
↙ ↘
靜態檔案 API代理到
(Angular) localhost:3000
其實我每次都覺得反向代理的反向兩個字很難理解。
但應該跟歷史比較有關,一開始習慣是客戶端到伺服器,所以從伺服器到客戶端,就被稱為反向了。
正向代理(Forward Proxy):
反向代理(Reverse Proxy):
處理完同源政策問題之後,就可以開始串接API了,中小型的專案通常會分層三層進行。
http-base.service → user.service → user.component
// 1. HTTP基礎服務 - http-base.service.ts
import { Injectable, inject } from '@angular/core';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError, map } from 'rxjs/operators';
export interface ApiResponse<T> {
data: T;
message: string;
status: number;
}
@Injectable({
providedIn: 'root'
})
export class HttpBaseService {
private readonly http = inject(HttpClient);
private readonly baseUrl = 'https://api.example.com/v1';
protected get<T>(endpoint: string): Observable<T> {
return this.http.get<ApiResponse<T>>(this.buildUrl(endpoint))
.pipe(
map(response => response.data),
catchError(this.handleError)
);
}
protected post<T>(endpoint: string, data: any): Observable<T> {
return this.http.post<ApiResponse<T>>(this.buildUrl(endpoint), data)
.pipe(
map(response => response.data),
catchError(this.handleError)
);
}
protected put<T>(endpoint: string, data: any): Observable<T> {
return this.http.put<ApiResponse<T>>(this.buildUrl(endpoint), data)
.pipe(
map(response => response.data),
catchError(this.handleError)
);
}
protected delete<T>(endpoint: string): Observable<T> {
return this.http.delete<ApiResponse<T>>(this.buildUrl(endpoint))
.pipe(
map(response => response.data),
catchError(this.handleError)
);
}
private buildUrl(endpoint: string): string {
return `${this.baseUrl}/${endpoint.replace(/^\//, '')}`;
}
private handleError(error: HttpErrorResponse): Observable<never> {
let errorMessage = '發生未知錯誤';
if (error.error instanceof ErrorEvent) {
// 客戶端錯誤
errorMessage = `錯誤:${error.error.message}`;
} else {
// 服務端錯誤
switch (error.status) {
case 400:
errorMessage = '請求參數錯誤';
break;
case 401:
errorMessage = '未授權,請重新登入';
break;
case 403:
errorMessage = '權限不足';
break;
case 404:
errorMessage = '資源不存在';
break;
case 500:
errorMessage = '伺服器內部錯誤';
break;
default:
errorMessage = `錯誤代碼:${error.status}`;
}
}
console.error('HTTP錯誤:', error);
return throwError(() => new Error(errorMessage));
}
}
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { HttpBaseService } from './http-base.service';
export interface User {
id: number;
name: string;
email: string;
phone?: string;
avatar?: string;
createdAt: string;
}
export interface CreateUserRequest {
name: string;
email: string;
phone?: string;
}
export interface UpdateUserRequest extends Partial<CreateUserRequest> {
avatar?: string;
}
@Injectable({
providedIn: 'root'
})
export class UserService extends HttpBaseService {
private readonly endpoint = 'users';
// 獲取用戶列表
getUsers(): Observable<User[]> {
return this.get<User[]>(this.endpoint);
}
...
}
@Component({
selector: 'app-day8',
standalone: true,
imports: [
CommonModule,
FormsModule,
ReactiveFormsModule,
...
],
templateUrl: './day8.component.html',
styleUrl: './day8.component.scss'
})
export class Day8Component {
private readonly userService = inject(UserService);
private readonly fb = inject(FormBuilder);
private readonly message = inject(NzMessageService);
// 響應式狀態
users = signal<User[]>([]);
loading = signal(false);
error = signal<string | null>(null);
// 計算屬性
userCount = computed(() => this.users().length);
// 響應式表單
userForm: FormGroup = this.fb.group({
name: ['', [Validators.required, Validators.minLength(2)]],
email: ['', [Validators.required, Validators.email]],
phone: ['']
});
constructor() {
this.loadUsers();
}
// 載入用戶列表
loadUsers(): void {
this.loading.set(true);
this.error.set(null);
this.userService.getUsers().subscribe({
next: (users) => {
this.users.set(users);
this.loading.set(false);
this.message.success(`成功載入 ${users.length} 位用戶`);
},
error: (error) => {
this.error.set(error.message);
this.loading.set(false);
this.message.error('載入用戶失敗');
}
});
}
...
}