DanLevy.net

Axiosは不要かもしれない

Fetch APIが救う!

Hero image for Axiosは不要かもしれない

Axiosは必要ないかもしれない

これはAxiosへの攻撃ではありません
むしろ、非常に高性能になったfetch APIを推奨するものです。 🦄

概要

この記事は、私がもっと簡単に見つけられたらよかったと思う、fetchの「欠けている」コードスニペットと一般的なユースケースを集めたものです。

ユースケースがリストにない?

お知らせください ✉️


機能比較

| | fetch | axios | request |

| リクエストとレスポンスのインターセプト |✅ |✅ |✅ |

| リクエストとレスポンスデータの変換 |✅ |✅ |✅ | | リクエストのキャンセル |✅ |✅ |❌ |

| JSONデータの自動変換 | 手動ヘルパー | ✅ | ✅ | | XSRF対策のクライアントサイドサポート | ✅ | ✅ | ✅ |

| 進捗 |✅ |✅ |✅ | | ストリーミング |✅ |✅ |✅ |

| リダイレクト |✅ |✅ |✅ |



この記事を書き始めたとき(2018年末、2024年更新)は、チェックマークが混ざった表で終わると思っていました。

確かに、axiosrequestr2superagentgotなどのライブラリを正当化する特別な ユースケース が存在します。

どうやら、サードパーティのHTTPライブラリの必要性を過大評価していたようです。

fetchを数年(ファイルアップロードやエラー/リトライ対応といった、決して簡単ではないタスクも含めて)使ってきたにもかかわらず、私は依然としてfetchの能力と限界について誤解を抱いていた。

ネイティブの fetch は、JSONレスポンスを自動的にパースしたり、JSONリクエストボディを自動的に文字列化したりしない。

戻り値に対しては response.json() を呼び、送信時には JSON.stringify() を呼ぶ必要がある。その点では Axios のほうがまだ使い勝手が良い。fetch の主張は、小さなヘルパー関数でその差を埋められることが多いという点だ。

さて、fetch で何ができるか見てみよう…

Fetch レシピ

URLからJSONを取得する

basic-example.js
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))

カスタムヘッダー

custom-headers.js
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エラーハンドリング

fetch-custom-error.js
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オプションは、クッキーが自動的に含まれるかどうかを制御します。

cors-example.js
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

posting-json.js
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

post-form-data.js
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 を生成します。

posting-form-encoded-data.js
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))
}

ファイルのアップロード

uploading-files.js
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 属性で設定します:

file-upload-field.html
<input type='file' multiple class='files' name='files' />

そして、次のように使います:

uploading-multiple-files.js
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チェーン内で過剰な処理を行わないでください。実行は継続されます。そして、失敗は長期的なメモリリークを引き起こす可能性があります。

promised-timeout.js
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 を備え、高コストな処理をインターセプトできるようにしています。

promise-timeout.js
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 のインターフェースは以下の通りです(ソースコードは以下で入手可能)

progressHelper-signature.js
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_

使用例を見てみましょう:

progress-helper-usage.js
// 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() のような形になるでしょう:

getBlob.js
const getBlob = url => fetch(url)
.then(progressHelper(console.log)) // progressHelper used inside the .then()
.then(response => response.blob())

ちなみに、Blob は Binary Large Object(バイナリラージオブジェクト)のことです。

以下の2つの使用パターンのうち、どちらか1つを選ぶことが重要です(機能的には同等です):

progressHelper-usage.js
// OPTION #1: no temp streamProcessor var
fetch(...)
.then(progressHelper(console.log))
// ⚠️ OR️ ️⚠️
// OPTION #2: define a `streamProcessor` to hold our console logger
const streamProcessor = progressHelper(console.log)
fetch(...)
.then(streamProcessor)

私の好みは Option #1 です。

ただし、スコープの設計によっては Option #2 を余儀なくされることもあります。

最後に、このレシピの最後の部分、progressHelper:

ソース: Progress Helper
progressHelper.js
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(こちら)に特に感謝します。

再帰的リトライヘルパー

fetch-retry-with-curry.js
/**
* 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())
// Usage
const 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)
fetch-with-retries.js
/** 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())
// Usage
retry(() => getJson('https://api.github.com/orgs/elite-libs'))
.then(console.log)
.catch(console.error)

HTTPリダイレクトの処理

location-redirect.js
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リクエストのキャンセル

cancel-fetch.js
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を利用できます:

Terminal window
npm install node-fetch

ポリフィル+node-fetch後: 99.99%互換

他のご希望の_ユースケース_があれば、私にツイートしてください。❤️