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

🔗 前後端整合:Fetch API 與 AJAX

📌 什麼是前後端通訊?

比喻:前後端通訊就像餐廳的服務生和廚房 🍽️

前端是外場的服務生,負責接待客人、記錄點餐(使用者介面)。 後端是廚房的廚師,負責備料、烹飪、出餐(資料處理和儲存)。 AJAX / Fetch API 就是服務生和廚房之間的「傳菜系統」—— 服務生把點單送進廚房(Request),廚房做好菜後傳出來(Response)。 整個過程中客人不需要離開座位(頁面不需要重新整理)。


📌 XMLHttpRequest vs Fetch API

// === 舊方法:XMLHttpRequest(了解即可,現在已少用) ===
const xhr = new XMLHttpRequest(); // 建立一個 XMLHttpRequest 物件
xhr.open("GET", "/api/users", true); // 設定方法、網址和是否非同步
xhr.onreadystatechange = function() { // 監聽狀態變化
    if (xhr.readyState === 4 && xhr.status === 200) { // 狀態 4 表示完成,200 表示成功
        const data = JSON.parse(xhr.responseText); // 手動解析 JSON 字串
        console.log(data); // 印出資料
    }
};
xhr.send(); // 送出請求

// === 新方法:Fetch API(推薦使用) ===
fetch("/api/users") // 送出 GET 請求,回傳 Promise
    .then(response => response.json()) // 將回應解析為 JSON(也是 Promise)
    .then(data => console.log(data)) // 取得解析後的資料
    .catch(error => console.error("錯誤:", error)); // 捕捉任何錯誤

// === 使用 async/await(最推薦的寫法) ===
async function getUsers() { // 宣告非同步函式
    try {
        const response = await fetch("/api/users"); // 等待回應
        if (!response.ok) { // 檢查 HTTP 狀態(fetch 不會自動拋出 HTTP 錯誤)
            throw new Error(`伺服器錯誤:${response.status}`); // 手動拋出錯誤
        }
        const data = await response.json(); // 等待 JSON 解析完成
        return data; // 回傳資料
    } catch (error) {
        console.error("取得使用者失敗:", error); // 印出錯誤訊息
        throw error; // 重新拋出讓呼叫者處理
    }
}

📌 GET / POST / PUT / DELETE 請求

// === GET 請求:取得資料(像服務生去廚房拿現成的菜) ===
async function getProducts(category) { // 依分類取得產品列表
    const url = new URL("/api/products", window.location.origin); // 建立 URL 物件
    url.searchParams.append("category", category); // 加入查詢參數 ?category=xxx
    url.searchParams.append("page", "1"); // 加入分頁參數 ?page=1

    const response = await fetch(url); // 送出 GET 請求(預設就是 GET)
    return await response.json(); // 解析並回傳 JSON 資料
}

// === POST 請求:建立新資料(像客人點一道新菜) ===
async function createProduct(product) { // 建立新產品
    const response = await fetch("/api/products", { // 送出 POST 請求
        method: "POST", // 設定 HTTP 方法為 POST
        headers: { // 設定請求標頭
            "Content-Type": "application/json", // 告訴伺服器送的是 JSON
            "Authorization": `Bearer ${getToken()}` // 附上驗證 Token
        },
        body: JSON.stringify(product) // 將 JavaScript 物件轉成 JSON 字串
    });

    if (!response.ok) { // 檢查回應狀態
        const error = await response.json(); // 解析錯誤訊息
        throw new Error(error.message || "建立失敗"); // 拋出錯誤
    }

    return await response.json(); // 回傳新建立的產品資料
}

// 呼叫範例
const newProduct = await createProduct({ // 傳入產品資料物件
    name: "筆記型電腦", // 產品名稱
    price: 35000, // 產品價格
    category: "electronics" // 產品分類
});

// === PUT 請求:更新整筆資料(像要求廚房重做一道菜) ===
async function updateProduct(id, product) { // 更新指定 ID 的產品
    const response = await fetch(`/api/products/${id}`, { // 送出 PUT 請求到指定 ID
        method: "PUT", // 設定 HTTP 方法為 PUT
        headers: {
            "Content-Type": "application/json" // 內容類型為 JSON
        },
        body: JSON.stringify(product) // 將更新資料轉成 JSON
    });
    return await response.json(); // 回傳更新後的資料
}

// === DELETE 請求:刪除資料(像退掉一道菜) ===
async function deleteProduct(id) { // 刪除指定 ID 的產品
    const response = await fetch(`/api/products/${id}`, { // 送出 DELETE 請求
        method: "DELETE", // 設定 HTTP 方法為 DELETE
        headers: {
            "Authorization": `Bearer ${getToken()}` // 刪除需要驗證權限
        }
    });

    if (!response.ok) { // 檢查是否成功
        throw new Error("刪除失敗"); // 失敗時拋出錯誤
    }

    return response.status === 204; // 204 No Content 表示刪除成功
}

📌 JSON 序列化與反序列化

// JavaScript 物件 → JSON 字串(序列化)
const user = { // 建立一個 JavaScript 物件
    name: "小明", // 姓名屬性
    age: 25, // 年齡屬性
    hobbies: ["coding", "reading"] // 興趣陣列
};

const jsonString = JSON.stringify(user); // 將物件轉成 JSON 字串
console.log(jsonString); // 印出:{"name":"小明","age":25,"hobbies":["coding","reading"]}

// 美化輸出(第三個參數是縮排空格數)
const prettyJson = JSON.stringify(user, null, 2); // 用 2 個空格縮排
console.log(prettyJson); // 印出格式化的 JSON(方便閱讀)

// JSON 字串 → JavaScript 物件(反序列化)
const jsonData = '{"name":"小華","age":30}'; // 一段 JSON 字串
const parsed = JSON.parse(jsonData); // 將 JSON 字串解析為物件
console.log(parsed.name); // 印出:小華

// 搭配 C# 後端的對應
// C# 端:public class User { public string Name { get; set; } public int Age { get; set; } }
// JavaScript 注意:C# 的 PascalCase 會被 System.Text.Json 轉成 camelCase
const userFromApi = await fetch("/api/users/1").then(r => r.json()); // 從 API 取得使用者
console.log(userFromApi.name); // camelCase:name(不是 Name)
console.log(userFromApi.age); // camelCase:age(不是 Age)

📌 與 ASP.NET Core API 搭配

// 完整的 CRUD 服務類別
class ProductService { // 封裝所有產品相關的 API 呼叫
    constructor(baseUrl = "/api/products") { // 建構子設定基礎 URL
        this.baseUrl = baseUrl; // 儲存基礎 URL
    }

    async getAll() { // 取得所有產品
        const response = await fetch(this.baseUrl); // 送出 GET 請求
        if (!response.ok) throw new Error("取得產品列表失敗"); // 錯誤處理
        return await response.json(); // 回傳 JSON 資料
    }

    async getById(id) { // 依 ID 取得單一產品
        const response = await fetch(`${this.baseUrl}/${id}`); // 送出 GET 請求到指定 ID
        if (response.status === 404) return null; // 找不到就回傳 null
        if (!response.ok) throw new Error("取得產品失敗"); // 其他錯誤
        return await response.json(); // 回傳產品資料
    }

    async create(product) { // 建立新產品
        const response = await fetch(this.baseUrl, { // POST 到基礎 URL
            method: "POST", // HTTP 方法
            headers: { "Content-Type": "application/json" }, // 設定內容類型
            body: JSON.stringify(product) // 將產品物件轉成 JSON
        });
        if (!response.ok) { // 檢查回應
            const errors = await response.json(); // 解析伺服器的驗證錯誤
            throw new Error(JSON.stringify(errors)); // 將錯誤訊息拋出
        }
        return await response.json(); // 回傳建立的產品
    }

    async update(id, product) { // 更新產品
        const response = await fetch(`${this.baseUrl}/${id}`, { // PUT 到指定 ID
            method: "PUT", // HTTP 方法
            headers: { "Content-Type": "application/json" }, // 設定內容類型
            body: JSON.stringify(product) // 將更新資料轉成 JSON
        });
        if (!response.ok) throw new Error("更新失敗"); // 錯誤處理
        return await response.json(); // 回傳更新後的產品
    }

    async delete(id) { // 刪除產品
        const response = await fetch(`${this.baseUrl}/${id}`, { // DELETE 指定 ID
            method: "DELETE" // HTTP 方法
        });
        return response.ok; // 回傳是否成功
    }
}

// 使用範例
const service = new ProductService(); // 建立服務實例
const products = await service.getAll(); // 取得所有產品

📌 CORS 跨域問題與解法

// 什麼是 CORS?
// 當前端(http://localhost:3000)向不同來源的後端(http://localhost:5000)
// 發送請求時,瀏覽器會因為「同源政策」而阻擋。

// 錯誤訊息通常長這樣:
// Access to fetch at 'http://localhost:5000/api/data' from origin
// 'http://localhost:3000' has been blocked by CORS policy

// 解法在 ASP.NET Core 後端設定(不是前端能處理的!)
// 在 Program.cs 中:
// builder.Services.AddCors(options =>
// {
//     options.AddPolicy("AllowFrontend", policy =>
//     {
//         policy.WithOrigins("http://localhost:3000")  // 允許的來源
//               .AllowAnyHeader()                       // 允許任何標頭
//               .AllowAnyMethod()                       // 允許任何 HTTP 方法
//               .AllowCredentials();                    // 允許攜帶 Cookie
//     });
// });
// app.UseCors("AllowFrontend");

// 前端攜帶 Cookie 的設定
const response = await fetch("http://localhost:5000/api/data", { // 跨域請求
    method: "GET", // HTTP 方法
    credentials: "include" // 重點!告訴瀏覽器要攜帶 Cookie
});

📌 FormData 上傳檔案

// 使用 FormData 上傳檔案
async function uploadFile(file) { // 接受一個 File 物件
    const formData = new FormData(); // 建立 FormData 物件
    formData.append("file", file); // 加入檔案,key 為 "file"
    formData.append("description", "產品圖片"); // 也可以加入其他欄位

    const response = await fetch("/api/upload", { // 送出 POST 請求
        method: "POST", // HTTP 方法
        body: formData // 直接傳 FormData,不需要設定 Content-Type(瀏覽器會自動設定)
        // 注意:不要手動設定 Content-Type,否則 boundary 會不正確!
    });

    if (!response.ok) throw new Error("上傳失敗"); // 錯誤處理
    return await response.json(); // 回傳上傳結果
}

// 搭配 HTML input 使用
const fileInput = document.querySelector("#fileInput"); // 取得檔案輸入元素
fileInput.addEventListener("change", async (e) => { // 監聽檔案選擇事件
    const file = e.target.files[0]; // 取得選擇的第一個檔案
    if (!file) return; // 如果沒選擇檔案就跳過

    if (file.size > 5 * 1024 * 1024) { // 檢查檔案大小(5MB 上限)
        alert("檔案大小不能超過 5MB"); // 顯示錯誤提示
        return; // 中止上傳
    }

    try {
        const result = await uploadFile(file); // 呼叫上傳函式
        console.log("上傳成功:", result); // 印出結果
    } catch (error) {
        console.error("上傳錯誤:", error); // 印出錯誤
    }
});

// 多檔案上傳
async function uploadMultipleFiles(files) { // 接受 FileList 或 File 陣列
    const formData = new FormData(); // 建立 FormData
    for (const file of files) { // 遍歷所有檔案
        formData.append("files", file); // 用相同的 key 加入多個檔案
    }

    const response = await fetch("/api/upload/multiple", { // 送出到多檔上傳端點
        method: "POST", // HTTP 方法
        body: formData // 送出 FormData
    });
    return await response.json(); // 回傳結果
}

🤔 我這樣寫為什麼會錯?

❌ 錯誤 1:fetch 的 HTTP 錯誤不會自動拋出例外

// 錯誤:以為 fetch 失敗會自動進入 catch
try {
    const response = await fetch("/api/not-found"); // 即使 404,fetch 也不會拋出錯誤!
    const data = await response.json(); // 試圖解析 404 頁面會出錯
    console.log(data); // 可能得到意外結果
} catch (error) {
    console.error(error); // 只有網路斷線才會進入這裡
}

// 正確:手動檢查 response.ok
try {
    const response = await fetch("/api/not-found"); // 送出請求
    if (!response.ok) { // 手動檢查 HTTP 狀態碼是否在 200-299 之間
        throw new Error(`HTTP 錯誤 ${response.status}: ${response.statusText}`); // 手動拋出
    }
    const data = await response.json(); // 只有成功才解析
    console.log(data); // 使用資料
} catch (error) {
    console.error("請求失敗:", error.message); // 現在能正確捕捉 HTTP 錯誤
}

❌ 錯誤 2:POST 時忘記設定 Content-Type

// 錯誤:沒有設定 Content-Type,伺服器不知道收到的是 JSON
const response = await fetch("/api/products", { // 送出 POST 請求
    method: "POST", // HTTP 方法
    body: JSON.stringify({ name: "手機" }) // 有轉 JSON 字串
    // 但沒有設定 Content-Type!伺服器會當成純文字處理
});

// 正確:明確設定 Content-Type 為 application/json
const response = await fetch("/api/products", { // 送出 POST 請求
    method: "POST", // HTTP 方法
    headers: {
        "Content-Type": "application/json" // 告訴伺服器這是 JSON 格式
    },
    body: JSON.stringify({ name: "手機" }) // 將物件轉成 JSON 字串
});

❌ 錯誤 3:上傳檔案時手動設定 Content-Type

// 錯誤:手動設定 Content-Type 會破壞 FormData 的 boundary
const formData = new FormData(); // 建立 FormData
formData.append("file", file); // 加入檔案
const response = await fetch("/api/upload", { // 送出請求
    method: "POST", // HTTP 方法
    headers: {
        "Content-Type": "multipart/form-data" // 錯誤!手動設定會缺少 boundary
    },
    body: formData // FormData 內容
});

// 正確:讓瀏覽器自動設定 Content-Type(包含正確的 boundary)
const formData = new FormData(); // 建立 FormData
formData.append("file", file); // 加入檔案
const response = await fetch("/api/upload", { // 送出請求
    method: "POST", // HTTP 方法
    // 不要設定 Content-Type!瀏覽器會自動加上正確的 multipart/form-data 和 boundary
    body: formData // 直接傳 FormData
});

💡 大家的想法 · 0

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