跳至主要内容

[WebAPIs] Fetch API

keywords: Fetch API, AJAX

Fetch API Example @ Gist

TD;DR

// Simple Get Request
fetch('<resourceURL>', init)
.then((response) => response.json()) // response.ok, response.status, response.statusText
.then((json) => console.log(json))
.catch((error) => console.log(error));

重要概念

  • 要留意的是,即使回應是 404 該 Promise 依然會被 resolve,只有在該請求沒辦法被完成時(例如,無網路連線),才會被 reject 進而進到 catch,因此,需要自己判斷 response.ok,如果不 ok 則透過 throw 讓它進入 catch
  • 除非有在選項中帶入 credentials 的設定,否則 fetch() 預設不會傳送 cross-origin cookies

With Error Handling:

fetch('<resourceURL>', init)
.then((response) => {
if (response.ok) {
return response.json();
}
throw new Error('Network response was not ok.');
})
.then((data) => console.log(json))
.catch((error) => console.log(error));

Example:

See the Pen Fetch API sandbox by PJCHEN (@PJCHENder) on CodePen.

Fetch API

最簡單使用 fetch() 的方式就是帶入一個參數,讓入你想要發送請求的資源(resource),接著會回傳包含 Response 物件的 Promise。但這個 Response 只是 HTTP Response,我們可以透過 json() 方法來取出 response 中的 JSON 部分:

// Example GET method request
fetch('https://jsonplaceholder.typicode.com/posts/1')
.then((response) => response.json())
.then((json) => console.log(json))
.catch((error) => console.log(error));

Init Options

fetch() 的第二個參數中可以放入其他相關的設定:

// Example POST method implementation
let data = {
title: 'foo',
body: 'bar',
userId: 1,
};

postData('https://jsonplaceholder.typicode.com/posts', data)
.then((data) => console.log(data)) // JSON from `response.json()` call
.catch((error) => console.error(error));

const init = {
method: 'POST', // *GET, POST, PUT, DELETE, etc.
body: JSON.stringify(data), // must match 'Content-Type' header
headers: {
'user-agent': 'Mozilla/4.0 MDN Example',
'content-type': 'application/json; charset=UTF-8',
Accept: 'application/json',
},
cache: 'no-cache',
mode: 'cors', // no-cors, cors, *same-origin
redirect: 'follow', // *manual, follow, error
referrer: 'no-referrer', // *client, no-referrer
};

function postData(url, data) {
// Default options are marked with *
return fetch(url, init).then((response) => response.json()); // parses response to JSON
}

/* will return
{
id: 101,
title: 'foo',
body: 'bar',
userId: 1
}
*/

HEAD Method:在 HTTP Method 中,HEAD 方法和 GET 方法的效果一樣,差別只在於透過 HEAD 方法拿到的 response.body 會是空的,通常用在只需要取得檔案的 metadata 時使用。

Response

在透過 fetch API 取得的 response 中包含一些可用的屬性:

  • response.ok:如果回應的狀態介於 200-299 之間,則會是 true
  • response.status:回應狀態代碼
  • response.statusText
  • response.redirected:是否轉址,回傳 truefalse
  • response.url:發送請求的 url

另外提供了一些可用的方法:

  • response.json()
  • response.blob()
  • response.text()
  • response.arrayBuffer()
  • response.formData()

Response Interface @ MDN - Web APIs

範例程式碼

Fetch API Sandbox @ PJCHENder CodePen

// Example
fetch('https://jsonplaceholder.typicode.com/posts/1')
.then((response) => {
if (!response.ok) {
throw Error(response.statusText);
}

// Read the response as json.
return response.json();
})
.then((responseAsJson) => {
// Do stuff with the JSON
console.log(responseAsJson);
})
.catch((error) => {
console.log('Looks like there was a problem: \n', error);
});

如果在 Promise 中需要串連的方法較多,可以考慮把方法抽出來:

// Example
fetch('https://jsonplaceholder.typicode.com/posts/1')
.then(validateResponse)
.then(parseResponseToJSON)
.then(doSomething)
.catch(errorHandler);

function validateResponse(response) {
if (!response.ok) {
throw Error(response.statusText);
}
return response;
}

function parseResponseToJSON(response) {
return response.json();
}

function doSomething(data) {
console.log(data);
}

function errorHandler(error) {
console.log(error);
}

其他

檢驗瀏覽器是否支援 fetch

if (!('fetch' in window)) {
console.log('Fetch API not found, try including the polyfill');
return;
}
// We can safely use fetch from now on

使用 Fetch 取得圖片(blob)

在透過 fetch 取得圖片時,會得到的是 blob 物件,因此需要使用 response.blob() 方法:

fetchImage('https://fakeimg.pl/250x100/');

function fetchImage(imageURL) {
fetch(imageURL)
.then(validateResponse)
.then(parseResponseToBlob)
.then(appendImage)
.catch(errorHandler);
}

function validateResponse(response) {
if (response.ok) {
return response;
}
throw Error(response.statusText);
}

function parseResponseToBlob(response) {
return response.blob();
}

function appendImage(blob) {
const img = new Image(250, 100);
img.src = URL.createObjectURL(blob);
document.querySelector('body').appendChild(img);
}

function errorHandler(error) {
console.log(error);
}

使用 POST 傳送 form data

透過 new FormData() 的方法,可以將 HTML 中的表單元素傳送到後端:

// Assuming an HTML <form> with id of 'myForm'
fetch('some-url/comment', {
method: 'POST',
body: new FormData(document.getElementById('myForm')),
});

自訂標頭(Headers)

透過 new Header() 的方法,可以為 Fetch Request 建立自訂的標頭(Header):

var myHeaders = new Headers({
'Content-Type': 'text/plain',
'X-Custom-Header': 'hello world',
});

fetch('/some-url', {
headers: myHeaders,
});

注意:Content-LengthOrigin 等標頭基於安全理由是無法被修改的。設置客制化標頭時,瀏覽器會自動發送預檢請求(preflight request)。

客製化 fetchAPI (fetchWithErrorHandling)

keywords: custom error handling
// src/fetcher.ts
class ResponseError extends Error {
public response: Response;

constructor(message: string, res: Response) {
super(`fetch error: ${message}`);
this.response = res;
}
}

export async function fetchWithErrorHandling<T>(...options: Parameters<typeof fetch>): Promise<T> {
const res = await fetch(...options);
if (!res.ok) {
throw new ResponseError(res.statusText, res);
}

return (await res.json()) as T;
}

export function handleError(err: unknown) {
// logging the error to third party service if needed

if (err instanceof ResponseError) {
switch (err.response.status) {
case 400:
console.error('Bad request');
break;
case 401:
// showUnauthorizedDialog();
console.error('Unauthorized');
break;
case 404:
console.error('Not found');
break;
case 500:
// showInternalServerErrorDialog();
console.error('Internal server error');
break;
default:
throw new Error(`Unhandled fetch error, ${err}`);
}
}
}
// src/api.ts
import { fetchWithErrorHandling, handleError } from './fetcher';

interface Todo {
userId: number;
id: number;
title: string;
completed: boolean;
}

export const fetchTodo = async (id: number) => {
try {
const data = await fetchWithErrorHandling(`https://jsonplaceholder.typicode.com/todos/${id}`);
return data;
} catch (err) {
handleError(err);
return;
}
};

export const createTodo = async (todo: Todo) => {
try {
await fetch('https://jsonplaceholder.typicode.com/todos', {
method: 'POST',
body: JSON.stringify(todo),
});
} catch (err) {
handleError(err);
return;
}
};

參考