Axiosは不要かもしれない
Fetch APIが救う!
Axiosは必要ないかもしれない
これはAxiosへの攻撃ではありません。
むしろ、非常に高性能になったfetch APIを推奨するものです。 🦄
概要
この記事は、私がもっと簡単に見つけられたらよかったと思う、fetchの「欠けている」コードスニペットと一般的なユースケースを集めたものです。
-
Fetchリクエストのキャンセル ✨new✨
ユースケースがリストにない?
機能比較
| | fetch | axios | request |
| リクエストとレスポンスのインターセプト |✅ |✅ |✅ |
| リクエストとレスポンスデータの変換 |✅ |✅ |✅ | | リクエストのキャンセル |✅ |✅ |❌ |
| JSONデータの自動変換 | 手動ヘルパー | ✅ | ✅ | | XSRF対策のクライアントサイドサポート | ✅ | ✅ | ✅ |
| 進捗 |✅ |✅ |✅ | | ストリーミング |✅ |✅ |✅ |
| リダイレクト |✅ |✅ |✅ |
この記事を書き始めたとき(2018年末、2024年更新)は、チェックマークが混ざった表で終わると思っていました。
確かに、axiosやrequest、r2、superagent、gotなどのライブラリを正当化する特別な ユースケース が存在します。
どうやら、サードパーティのHTTPライブラリの必要性を過大評価していたようです。
fetchを数年(ファイルアップロードやエラー/リトライ対応といった、決して簡単ではないタスクも含めて)使ってきたにもかかわらず、私は依然としてfetchの能力と限界について誤解を抱いていた。
ネイティブの fetch は、JSONレスポンスを自動的にパースしたり、JSONリクエストボディを自動的に文字列化したりしない。
戻り値に対しては response.json() を呼び、送信時には JSON.stringify() を呼ぶ必要がある。その点では Axios のほうがまだ使い勝手が良い。fetch の主張は、小さなヘルパー関数でその差を埋められることが多いという点だ。
さて、fetch で何ができるか見てみよう…
Fetch レシピ
URLからJSONを取得する
fetch('https://api.github.com/orgs/nodejs') .then(response => response.json()) .then(data => { console.log(data) // result from `response.json()` above }) .catch(error => console.error(error))カスタムヘッダー
fetch('https://api.github.com/orgs/nodejs', { headers: new Headers({ 'User-agent': 'Mozilla/4.0 Custom User Agent' })}).then(response => response.json()).then(data => { console.log(data)}).catch(error => console.error(error))HTTPエラーハンドリング
const isOk = response => response.ok ? response.json() : Promise.reject(new Error('Failed to load data from server'))
fetch('https://api.github.com/orgs/nodejs') .then(isOk) // <= Use `isOk` function here .then(data => { console.log(data) // Prints result from `response.json()` }) .catch(error => console.error(error))CORSの例
CORSは主にサーバー側でチェックされます — そのため、サーバー側の設定が正しいことを確認してください。
credentialsオプションは、クッキーが自動的に含まれるかどうかを制御します。
fetch('https://api.github.com/orgs/nodejs', { credentials: 'include', // Useful for including session ID (and, IIRC, authorization headers)}).then(response => response.json()).then(data => { console.log(data) // Prints result from `response.json()`}).catch(error => console.error(error))JSONのPOST
postRequest('http://example.com/api/v1/users', {user: 'Dan'}) .then(data => console.log(data)) // Result from the `response.json()` call
function postRequest(url, data) { return fetch(url, { credentials: 'same-origin', // 'include', default: 'omit' method: 'POST', // 'GET', 'PUT', 'DELETE', etc. body: JSON.stringify(data), // Use correct payload (matching 'Content-Type') headers: { 'Content-Type': 'application/json' }, }) .then(response => response.json()) .catch(error => console.error(error))}HTML <form> のPOST
postForm('http://example.com/api/v1/users', 'form#userEdit') .then(data => console.log(data))
function postForm(url, formSelector) { const formData = new FormData(document.querySelector(formSelector))
return fetch(url, { method: 'POST', // 'GET', 'PUT', 'DELETE', etc. body: formData // a FormData will automatically set the 'Content-Type' }) .then(response => response.json()) .catch(error => console.error(error))}フォームエンコードデータ
application/x-www-form-urlencoded の Content-Type でデータを POST するには、URLSearchParams を使ってクエリ文字列のようにデータをエンコードします。
例えば、new URLSearchParams({a: 1, b: 2}) は a=1&b=2 を生成します。
postFormData('http://example.com/api/v1/users', {user: 'Mary'}) .then(data => console.log(data))
function postFormData(url, data) { return fetch(url, { method: 'POST', // 'GET', 'PUT', 'DELETE', etc. body: new URLSearchParams(data), headers: new Headers({ 'Content-type': 'application/x-www-form-urlencoded; charset=UTF-8' }) }) .then(response => response.json()) .catch(error => console.error(error))}ファイルのアップロード
postFile('http://example.com/api/v1/users', 'input[type="file"].avatar') .then(data => console.log(data))
function postFile(url, fileSelector) { const formData = new FormData() const fileField = document.querySelector(fileSelector)
formData.append('username', 'abc123') formData.append('avatar', fileField.files[0])
return fetch(url, { method: 'POST', // 'GET', 'PUT', 'DELETE', etc. body: formData // Coordinate the body type with 'Content-Type' }) .then(response => response.json()) .catch(error => console.error(error))}複数ファイルのアップロード
複数ファイルのアップロード要素を multiple 属性で設定します:
<input type='file' multiple class='files' name='files' />そして、次のように使います:
postFile('http://example.com/api/v1/users', 'input[type="file"].files') .then(data => console.log(data))
function postFile(url, fileSelector) { const formData = new FormData() const fileFields = document.querySelectorAll(fileSelector)
// Add all files to formData Array.prototype.forEach.call(fileFields.files, f => formData.append('files', f)) // Alternatively for PHPeeps, use `files[]` for the name to support arrays // Array.prototype.forEach.call(fileFields.files, f => formData.append('files[]', f))
return fetch(url, { method: 'POST', // 'GET', 'PUT', 'DELETE', etc. body: formData // Coordinate the body type with 'Content-Type' }) .then(response => response.json()) .catch(error => console.error(error))}タイムアウト
以下は、「Partial Application」パターンを使用した汎用的なPromiseタイムアウトです。
どんなPromiseインターフェースでも動作します。指定したPromiseチェーン内で過剰な処理を行わないでください。実行は継続されます。そして、失敗は長期的なメモリリークを引き起こす可能性があります。
function promiseTimeout(msec) { return promise => { const timeout = new Promise((yea, nah) => setTimeout(() => nah(new Error('Timeout expired')), msec)) return Promise.race([promise, timeout]) }}
promiseTimeout(5000)(fetch('https://api.github.com/orgs/nodejs')) .then(response => response.json()) .then(data => { console.log(data) // Prints result from `response.json()` in getRequest }) .catch(error => console.error(error)) // Catches any timeout (or other failure)
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Alternative example:fetchTimeout(5000, 'https://api.github.com/orgs/nodejs') .then(console.log)// Alternative implementation:function fetchTimeout(msec, ...args) { return raceTimeout(fetch(...args))
function raceTimeout(promise) { const timeout = new Promise((yea, nah) => setTimeout(() => nah(new Error('Timeout expired')), msec)) return Promise.race([promise, timeout]) }}そして、より複雑な例として、追跡フラグ __timeout を備え、高コストな処理をインターセプトできるようにしています。
function promiseTimeout(msec) { return (promise) => { let isDone = false promise.then(() => isDone = true) const timeout = new Promise((yea, nah) => setTimeout(() => { if (!isDone) { promise.__timeout = true nah(new Error('Timeout expired')) } }, msec)) return Promise.race([promise, timeout]) }}
promiseTimeout(5000)(fetch('https://api.github.com/orgs/nodejs')).then(response => response.json()).then(data => { console.log(data) // Prints result from `response.json()` in getRequest}).catch(error => console.error(error))ダウンロード進捗ヘルパー
アップロード進捗は現在、Chrome以外ではやや不安定です。
Progress Handlerは、以下に示す手法でラップを回避し、fetch呼び出しをクロージャで囲みません。
👍
progressHelper のインターフェースは以下の通りです(ソースコードは以下で入手可能)
const progressHelper = require('./progressHelper.js')
const handler = ({loaded, total}) => { console.log(`Downloaded ${loaded} of ${total}`)}// handler args: ({ loaded = Kb, total = 0-100% })const streamProcessor = progressHelper(handler)// => streamProcessor is a function for use with the response _stream_使用例を見てみましょう:
// The progressHelper could be inline w/ .then() below...const streamProcessor = progressHelper(console.log)
fetch('https://fetch-progress.anthum.com/20kbps/images/sunrise-progressive.jpg') .then(streamProcessor) // note: NO parentheses because `.then` needs to get a function .then(response => response.blob()) .then(blobData => { // ... set as base64 on an <img src="base64..."> })再利用可能な画像ダウンローダーは、getBlob() のような形になるでしょう:
const getBlob = url => fetch(url) .then(progressHelper(console.log)) // progressHelper used inside the .then() .then(response => response.blob())ちなみに、Blob は Binary Large Object(バイナリラージオブジェクト)のことです。
以下の2つの使用パターンのうち、どちらか1つを選ぶことが重要です(機能的には同等です):
// OPTION #1: no temp streamProcessor varfetch(...) .then(progressHelper(console.log))
// ⚠️ OR️ ️⚠️
// OPTION #2: define a `streamProcessor` to hold our console loggerconst streamProcessor = progressHelper(console.log)fetch(...) .then(streamProcessor)私の好みは Option #1 です。
ただし、スコープの設計によっては Option #2 を余儀なくされることもあります。
最後に、このレシピの最後の部分、progressHelper:
ソース: Progress Helper
function progressHelper(onProgress) { return (response) => { if (!response.body) return response
let loaded = 0 const contentLength = response.headers.get('content-length') const total = !contentLength ? -1 : parseInt(contentLength, 10)
return new Response( new ReadableStream({ start(controller) { const reader = response.body.getReader() return read()
function read() { return reader.read() .then(({ done, value }) => { if (done) return void controller.close() loaded += value.byteLength onProgress({ loaded, total }) controller.enqueue(value) return read() }) .catch(error => { console.error(error) controller.error(error) }) } } }) ) }}クレジット: Anthum Chris氏と彼の優れたProgress+FetchのPoC(こちら)に特に感謝します。
再帰的リトライヘルパー
/** * A **Smarter** retry wrapper with currying! */function retryCurry(fn, retriesLeft = 5) { const retryFn = (...args) => fn(...args) .catch(err => retriesLeft > 0 ? retryFn(fn, retriesLeft - 1) : Promise.reject(err) }) return retryFn}
const getJson = (url) => fetch(url) .then(response => response.json())
// Usageconst retryGetJson = retryCurry(getJson, 3);
// Now you can pass any arguments through to your function!retryGetJson('https://api.github.com/orgs/elite-libs') .then(console.log) .catch(console.error)/** Basic retry wrapper for Promises */function retryPromise(fn, retriesLeft = 5) { return fn() .catch(err => retriesLeft > 0 ? retryPromise(fn, retriesLeft - 1) : Promise.reject(err) })}
const getJson = (url) => fetch(url) .then(response => response.json())
// Usageretry(() => getJson('https://api.github.com/orgs/elite-libs')) .then(console.log) .catch(console.error)HTTPリダイレクトの処理
const checkForRedirect = (response) => { // Check for temporary redirect (307), or permanent (308) if (response.status === 307 || response.status === 308) { const location = response.headers.get('location') if (!location) { return Promise.reject(new Error('Invalid HTTP Redirect! No Location header.')); } // You can change the behavior here to any custom logic: // e.g. open a "confirm" modal, log the redirect url, etc. return fetch(location) // Bonus: this will handle recursive redirects ✨ .then(checkForRedirect) } return response};
fetch('https://api.github.com/orgs/elite-libs') // Next line will handle redirects .then(checkForRedirect) .then(response => response.json()) .then(console.log) .catch(console.error)fetchリクエストのキャンセル
const httpWithTimeout = (timeout = 5000, url) => { const controller = new AbortController(); // Set an Nsec cancellation timeout const timer = setTimeout(() => controller.abort(), timeout);
return fetch(url, { signal: controller.signal }) .then(response => { clearTimeout(timer); // not required but closes open ref return response.text(); }).then(text => { console.log(text); });}互換性
2022年時点で、fetch APIは広くサポートされており、すべてのモダンブラウザおよびNodeJS v18以降のバージョンで利用可能です。
IEをサポートする必要がある場合は、github/fetchパッケージ(GitHubの素晴らしいチームがメンテナンスしています)を使ってfetchをポリフィルできます。
IE8まで遡ることも可能です - 結果は保証できません.
古いNodeJSでも、node-fetchパッケージを使えばfetchAPIを利用できます:
npm install node-fetchポリフィル+node-fetch後: 99.99%互換 ✅
他のご希望の_ユースケース_があれば、私にツイートしてください。❤️