JavaScript Fetch API

Fetch API 是現代瀏覽器內建的 API,用來發送 HTTP 請求,是 XMLHttpRequest (AJAX) 的現代替代方案。Fetch 使用 Promise,語法更簡潔,也更容易和 async/await 搭配使用。

基本用法

fetch() 函式接收一個 URL 作為參數,回傳一個 Promise:

fetch('https://api.example.com/data')
    .then(function(response) {
        return response.json();
    })
    .then(function(data) {
        console.log(data);
    })
    .catch(function(err) {
        console.error('請求失敗:' + err);
    });

使用 async/await 的寫法更簡潔:

async function getData() {
    try {
        var response = await fetch('https://api.example.com/data');
        var data = await response.json();
        console.log(data);
    } catch (err) {
        console.error('請求失敗:' + err);
    }
}

Response 物件

fetch() 回傳的 Promise 會 resolve 成一個 Response 物件,包含以下常用屬性和方法:

屬性

var response = await fetch('/api/data');

response.ok;       // 狀態碼在 200-299 之間為 true
response.status;   // HTTP 狀態碼,如 200, 404, 500
response.statusText; // 狀態文字,如 "OK", "Not Found"
response.headers;  // Headers 物件
response.url;      // 請求的 URL

讀取回應內容

Response 提供多種方法來讀取回應內容,這些方法都回傳 Promise:

response.json();  // 解析為 JSON
response.text();  // 解析為純文字
response.blob();  // 解析為 Blob(二進位資料)
response.arrayBuffer(); // 解析為 ArrayBuffer
response.formData();    // 解析為 FormData
這些方法只能呼叫一次,因為 response body 只能被讀取一次。

檢查請求是否成功

fetch 只有在網路錯誤時才會 reject,HTTP 錯誤狀態碼(如 404、500)不會 reject。所以需要手動檢查 response.ok

async function fetchData(url) {
    var response = await fetch(url);
    
    if (!response.ok) {
        throw new Error('HTTP 錯誤:' + response.status);
    }
    
    return response.json();
}

// 使用
try {
    var data = await fetchData('/api/user');
} catch (err) {
    console.error(err.message);
}

發送 POST 請求

fetch() 的第二個參數是一個選項物件,用來設定請求方法、標頭和內容:

async function createUser(userData) {
    var response = await fetch('/api/users', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json'
        },
        body: JSON.stringify(userData)
    });
    
    if (!response.ok) {
        throw new Error('建立失敗');
    }
    
    return response.json();
}

// 使用
var newUser = await createUser({
    name: 'Mike',
    email: 'mike@example.com'
});

其他 HTTP 方法

// PUT 請求
await fetch('/api/users/1', {
    method: 'PUT',
    headers: {
        'Content-Type': 'application/json'
    },
    body: JSON.stringify({ name: 'Mike Lee' })
});

// PATCH 請求
await fetch('/api/users/1', {
    method: 'PATCH',
    headers: {
        'Content-Type': 'application/json'
    },
    body: JSON.stringify({ name: 'Mike Lee' })
});

// DELETE 請求
await fetch('/api/users/1', {
    method: 'DELETE'
});

設定 Headers

// 方法一:在選項物件中設定
await fetch('/api/data', {
    headers: {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer token123'
    }
});

// 方法二:使用 Headers 物件
var headers = new Headers();
headers.append('Content-Type', 'application/json');
headers.append('Authorization', 'Bearer token123');

await fetch('/api/data', { headers: headers });

發送表單資料

發送 FormData

var formData = new FormData();
formData.append('username', 'mike');
formData.append('password', '123456');

await fetch('/api/login', {
    method: 'POST',
    body: formData  // 不需要設定 Content-Type,瀏覽器會自動設定
});

發送 URL 編碼的表單

var params = new URLSearchParams();
params.append('username', 'mike');
params.append('password', '123456');

await fetch('/api/login', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/x-www-form-urlencoded'
    },
    body: params
});

上傳檔案

var fileInput = document.querySelector('input[type="file"]');
var formData = new FormData();
formData.append('file', fileInput.files[0]);

await fetch('/api/upload', {
    method: 'POST',
    body: formData
});

設定請求逾時

fetch 原生不支援 timeout,但可以用 AbortController 實作:

async function fetchWithTimeout(url, timeout) {
    var controller = new AbortController();
    var timeoutId = setTimeout(function() {
        controller.abort();
    }, timeout);
    
    try {
        var response = await fetch(url, {
            signal: controller.signal
        });
        clearTimeout(timeoutId);
        return response;
    } catch (err) {
        if (err.name === 'AbortError') {
            throw new Error('請求逾時');
        }
        throw err;
    }
}

// 5 秒逾時
var response = await fetchWithTimeout('/api/data', 5000);

取消請求

使用 AbortController 可以取消進行中的請求:

var controller = new AbortController();

// 發送請求
fetch('/api/data', {
    signal: controller.signal
}).then(function(response) {
    return response.json();
}).catch(function(err) {
    if (err.name === 'AbortError') {
        console.log('請求被取消');
    }
});

// 取消請求
controller.abort();

帶入 Cookies

預設情況下,fetch 在同源請求會帶入 cookies,跨域請求則不會。使用 credentials 選項可以控制這個行為:

// 同源請求帶入 cookies(預設)
await fetch('/api/data', {
    credentials: 'same-origin'
});

// 跨域請求也帶入 cookies
await fetch('https://other-domain.com/api', {
    credentials: 'include'
});

// 不帶入 cookies
await fetch('/api/data', {
    credentials: 'omit'
});

CORS 跨域請求

當你向不同網域發送請求時,會受到瀏覽器的同源政策 (Same-Origin Policy) 限制。伺服器需要設定適當的 CORS 標頭才能允許跨域請求。

// 跨域請求
try {
    var response = await fetch('https://api.other-domain.com/data', {
        mode: 'cors',  // 預設值
        credentials: 'include'  // 如果需要帶入 cookies
    });
    var data = await response.json();
} catch (err) {
    console.error('跨域請求失敗:' + err);
}

mode 選項的可能值:

  • cors:允許跨域請求(預設)
  • same-origin:只允許同源請求
  • no-cors:允許跨域請求,但無法讀取回應內容

實用範例

封裝 API 請求函式

async function api(endpoint, options) {
    var baseUrl = 'https://api.example.com';
    
    var defaultOptions = {
        headers: {
            'Content-Type': 'application/json'
        }
    };
    
    var response = await fetch(baseUrl + endpoint, 
        Object.assign({}, defaultOptions, options)
    );
    
    if (!response.ok) {
        var error = await response.json();
        throw new Error(error.message || 'API 錯誤');
    }
    
    return response.json();
}

// 使用
var users = await api('/users');
var newUser = await api('/users', {
    method: 'POST',
    body: JSON.stringify({ name: 'Mike' })
});

Fetch vs XMLHttpRequest

功能FetchXMLHttpRequest
語法Promise-based,簡潔回呼函式,較冗長
串流處理支援有限支援
取消請求AbortControllerxhr.abort()
進度事件不直接支援支援
瀏覽器支援現代瀏覽器所有瀏覽器

如果你的專案不需要支援舊版瀏覽器(如 IE),建議使用 Fetch API。