JavaScript Fetch API

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

基本用法

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

// 取得遠端資料
fetch('https://jsonplaceholder.typicode.com/posts/1')
  .then((response) => response.json())
  .then((json) => console.log(json));

// 使用 async/await 語法 (推薦)
async function getData() {
  try {
    const response = await fetch('https://jsonplaceholder.typicode.com/posts/1');
    const json = await response.json();
    console.log(json);
  } catch (error) {
    console.error(error);
  }
}
getData();

Response 物件

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

屬性

const 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) {
  const response = await fetch(url);

  if (!response.ok) {
    throw new Error('HTTP 錯誤:' + response.status);
  }

  return response.json();
}

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

發送 POST 請求

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

async function createUser(userData) {
  const 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();
}

// 使用
const 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 物件
const headers = new Headers({
  'Content-Type': 'text/plain',
  'X-Custom-Header': 'ProcessThisImmediately',
});

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

發送表單資料

發送 FormData

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

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

發送 URL 編碼的表單

const 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,
});

上傳檔案

const fileInput = document.querySelector('input[type="file"]');
const 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) {
  const controller = new AbortController();
  const timeoutId = setTimeout(function () {
    controller.abort();
  }, timeout);

  try {
    const response = await fetch(url, {
      signal: controller.signal,
    });
    clearTimeout(timeoutId);
    return response;
  } catch (err) {
    if (err.name === 'AbortError') {
      throw new Error('請求逾時');
    }
    throw err;
  }
}

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

取消請求

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

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

mode 選項的可能值:

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

實用範例

封裝 API 請求函式

async function api(endpoint, options) {
  const baseUrl = 'https://api.example.com';

  const defaultOptions = {
    headers: {
      'Content-Type': 'application/json',
    },
  };

  const data = { username: 'example' };

  fetch('https://example.com/profile', {
    method: 'POST', // or 'PUT'
    body: JSON.stringify(data), // data can be `string` or {object}!
    headers: new Headers({
      'Content-Type': 'application/json',
    }),
  })
    .then((res) => res.json())
    .catch((error) => console.error('Error:', error))
    .then((response) => console.log('Success:', response));

  const response = await fetch(baseUrl + endpoint, Object.assign({}, defaultOptions, options));

  if (!response.ok) {
    const error = await response.json();
    throw new Error(error.message || 'API 錯誤');
  }

  return response.json();
}

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

Fetch vs XMLHttpRequest

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

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