フォームの送信を強化するために JavaScript fetch
API を使用したことがある場合は、誤って重複リクエスト/競合状態のバグを導入した可能性があります。今日は、この問題とそれを回避するための推奨事項について説明します。
(よろしかったら最後に動画を)
非常に基本的なことを考えてみましょう
<form method="post"> <label for="name">Name</label> <input id="name" name="name" /> <button>Submit</button> </form>
送信ボタンを押すと、ブラウザはページ全体を更新します。
送信ボタンをクリックした後、ブラウザーがどのようにリロードされるかに注意してください。
ページの更新は、常にユーザーに提供したいエクスペリエンスとは限らないため、一般的な代替手段は使用することですfetch
API を使用してフォーム データを送信します。
単純化したアプローチは、以下の例のようになります。
ページ (またはコンポーネント) がマウントされたら、フォーム DOM ノードを取得し、フォームを使用してfetch
要求を構築するイベント リスナーを追加します。preventDefault()
メソッドを呼び出します。
const form = document.querySelector('form'); form.addEventListener('submit', handleSubmit); function handleSubmit(event) { const form = event.currentTarget; fetch(form.action, { method: form.method, body: new FormData(form) }); event.preventDefault(); }
さて、JavaScript の有力者が GET と POST、およびリクエストの本文とfetch
リクエストは意図的にシンプルにしています。これは主な焦点ではないからです。
ここでの重要な問題はevent.preventDefault()
です。このメソッドは、ブラウザが新しいページをロードしてフォームを送信するデフォルトの動作を実行するのを防ぎます。
ここで、画面を見て [送信] をクリックすると、ページがリロードされていないことがわかりますが、[ネットワーク] タブに HTTP 要求が表示されています。
ブラウザはページ全体のリロードを行わないことに注意してください。
残念ながら、JavaScript を使用してデフォルトの動作を防止することで、デフォルトのブラウザ動作にはないバグを実際に導入してしまいました。
プレーンを使用する場合
これを JavaScript の例と比較すると、すべてのリクエストが送信され、キャンセルされることなくすべてが完了していることがわかります。
各リクエストには異なる時間がかかる場合がありますが、開始された順序とは異なる順序で解決される可能性があるため、これは問題になる可能性があります。つまり、これらのリクエストの解決に機能を追加すると、予期しない動作が発生する可能性があります。
例として、リクエストごとにインクリメントする変数を作成できます (「 totalRequestCount
」)。 handleSubmit
関数を実行するたびに、合計カウントをインクリメントするだけでなく、現在の数を取得して現在のリクエストを追跡できます (「 thisRequestNumber
」)。
fetch
リクエストが解決されると、対応する番号をコンソールに記録できます。
const form = document.querySelector('form'); form.addEventListener('submit', handleSubmit); let totalRequestCount = 0 function handleSubmit(event) { totalRequestCount += 1 const thisRequestNumber = totalRequestCount const form = event.currentTarget; fetch(form.action, { method: form.method, body: new FormData(form) }).then(() => { console.log(thisRequestNumber) }) event.preventDefault(); }
ここで、送信ボタンを何度も押すと、2、3、1、4、5 のように異なる数字がコンソールに間違った順序で出力されることがあります。これはネットワークの速度によって異なりますが、私たち全員が同意できると思います。これは理想的ではないということです。
ユーザーがいくつかのfetch
リクエストを連続してトリガーし、完了すると、アプリケーションが変更内容でページを更新するシナリオを考えてみましょう。リクエストが順不同で解決されるため、ユーザーは最終的に不正確な情報を表示する可能性があります。
これは非 JavaScript の世界では問題ではありません。ブラウザは以前のリクエストをキャンセルし、最新のリクエストが完了した後にページをロードして、最新バージョンをロードするからです。しかし、ページの更新は魅力的ではありません。
JavaScript 愛好家にとって良いニュースは、両方を使用できることです。
もう少し足を運ぶ必要があります。
fetch
API ドキュメントを見ると、 AbortController
とfetch
オプションのsignal
プロパティを使用してフェッチを中止できることがわかります。次のようになります。
const controller = new AbortController(); fetch(url, { signal: controller.signal });
AbortContoller
のシグナルをfetch
リクエストに提供することで、 AbortContoller
のabort
メソッドがトリガーされたときにいつでもリクエストをキャンセルできます。
JavaScript コンソールでより明確な例を確認できます。 AbortController
を作成し、 fetch
リクエストを開始してから、すぐにabort
メソッドを実行してみてください。
const controller = new AbortController(); fetch('', { signal: controller.signal }); controller.abort()
すぐにコンソールに例外が表示されるはずです。 Chromium ブラウザーでは、「Uncaught (in promise) DOMException: The user aborted a request.」と表示されます。また、[ネットワーク] タブを調べると、ステータス テキストが「(キャンセル)」の失敗した要求が表示されます。
これを念頭に置いて、フォームの送信ハンドラーにAbortController
を追加できます。ロジックは次のようになります。
AbortController
を確認します。存在する場合は、中止します。
AbortController
を作成します。
AbortController
を削除します。
これを行うにはいくつかの方法がありますが、ここではWeakMap
を使用して、送信された各<form>
DOM ノードとそれぞれのAbortController
の間の関係を保存します。フォームが送信されると、それに応じてWeakMap
確認および更新できます。
const pendingForms = new WeakMap(); function handleSubmit(event) { const form = event.currentTarget; const previousController = pendingForms.get(form); if (previousController) { previousController.abort(); } const controller = new AbortController(); pendingForms.set(form, controller); fetch(form.action, { method: form.method, body: new FormData(form), signal: controller.signal, }).then(() => { pendingForms.delete(form); }); event.preventDefault(); } const forms = document.querySelectorAll('form'); for (const form of forms) { form.addEventListener('submit', handleSubmit); }
重要なことは、アボート コントローラーを対応するフォームに関連付けることができることです。そのためには、フォームの DOM ノードをWeakMap
のキーとして使用すると便利です。
これで、 fetch
要求にAbortController
のシグナルを追加し、以前のコントローラーを中止し、新しいコントローラーを追加し、完了時にそれらを削除できます。
うまくいけば、それはすべて理にかなっています。
ここで、そのフォームの送信ボタンを何度も押すと、最新の API リクエストを除くすべての API リクエストがキャンセルされることがわかります。
これは、その HTTP 応答に応答するすべての関数が、期待どおりに動作することを意味します。
ここで、上記と同じカウントおよびロギング ロジックを使用すると、送信ボタンを 7 回押すと、( AbortController
による) 6 つの例外と、コンソールに「7」のログが 1 つ表示されます。
再度送信して、リクエストが解決されるまで十分な時間を確保すると、コンソールに「8」が表示されます。また、送信ボタンを何度も押しても、例外と最終リクエスト数が正しい順序で表示され続けます。
リクエストが中止されたときにコンソールに DOMExceptions が表示されないようにするロジックを追加したい場合は、 fetch
リクエストの後に.catch()
ブロックを追加し、エラーの名前が「 AbortError
」と一致するかどうかを確認します。
fetch(url, { signal: controller.signal, }).catch((error) => { // If the request was aborted, do nothing if (error.name === 'AbortError') return; // Otherwise, handle the error here or throw it back to the console throw error });
この投稿全体は JavaScript で強化されたフォームに焦点を当てていましたが、 fetch
リクエストを作成するときはいつでもAbortController
を含めることをお勧めします。まだAPIに組み込まれていないのは本当に残念です。しかし、うまくいけば、これはそれを含めるための良い方法を示しています.
また、このアプローチでは、ユーザーが送信ボタンを何度もスパム送信するのを防ぐことはできないことにも注意してください。ボタンは引き続きクリック可能であり、リクエストは引き続き発生します。これにより、レスポンスを処理するためのより一貫した方法が提供されます。
残念ながら、ユーザーが送信ボタンをスパムした場合、それらのリクエストは引き続きバックエンドに送られ、不要なリソースを大量に消費する可能性があります。
いくつかの単純な解決策は、送信ボタンを無効にしている可能性があります。
スクリプト化されたリクエストによる不正行為には対処しません。
サーバーへのリクエストが多すぎることによる悪用に対処するには、おそらくいくつかの
また、レート制限は、リクエストの重複、競合状態、一貫性のない UI 更新などの元の問題を解決しないことにも注意してください。理想的には、両方を使用して両端をカバーする必要があります。
とにかく、今日はここまでです。これと同じテーマを取り上げたビデオを見たい場合は、これを見てください。
読んでいただきありがとうございます。この記事が気に入ったらどうぞ
最初に公開された