Ajaxをキャンセルする方法はありますか?キャンセルする必要がありますか?
/ 9 min read
Ajaxのキャンセル
xhr
に詳しい人なら、Ajaxをフロントエンドでキャンセルすることができることを知っているでしょう。それにはXMLHttpRequest.abort()
を使用します。もちろん、今は刀耕火種の時代ではありません。面接以外では、基本的にxhr
を手書きすることはありません。誰もが知っているaxios
には2つのキャンセル方法があります。
まずは古いタイプのcancelTokenです:
const CancelToken = axios.CancelToken;const source = CancelToken.source();
axios .get("/user/12345", { cancelToken: source.token, }) .catch(function (thrown) { if (axios.isCancel(thrown)) { console.log("リクエストがキャンセルされました", thrown.message); } else { // エラーの処理 } });
axios.post( "/user/12345", { name: "新しい名前", }, { cancelToken: source.token, },);
// リクエストをキャンセルする(メッセージパラメータはオプションです)source.cancel("ユーザーによって操作がキャンセルされました。");
次に、新しいもの(実際には新しくない)であるAbortControllerです:
const controller = new AbortController();
axios .get("/foo/bar", { signal: controller.signal, }) .then(function (response) { //... });// リクエストをキャンセルするcontroller.abort();
cancelTokenとsignalはaxiosに渡されると、いずれもXMLHttpRequest.abort()
が呼び出される仕組みになっています。
onCanceled = (cancel) => { if (!request) { return; } reject(!cancel || cancel.type ? new CanceledError(null, config, request) : cancel); request.abort(); request = null;};
config.cancelToken && config.cancelToken.subscribe(onCanceled);if (config.signal) { config.signal.aborted ? onCanceled() : config.signal.addEventListener("abort", onCanceled);}
cancelTokenは、axiosにリクエストのキャンセルを通知するためにパブリッシュ/サブスクライブパターンを使用しています。これはaxios自体が実装したものですが、tc39の提案であるキャンセル可能なプロミスの提案に由来しています。ただし、この提案は廃止されました。
一方、AbortControllerは既にブラウザで使用できるインターフェースです。その名前からもわかるように、これはアクションを中止するための専用のコントローラーです。mdnの例ではAjaxリクエストが使用されていますが、最新かつ最も人気のあるfetchです。これからもaxiosとfetchの実践は一致していることがわかります。
function fetchVideo() { controller = new AbortController(); // 新しいコントローラーを作成する const signal = controller.signal; fetch(url, { signal }) // fetchメソッドにsignalを渡す .then(function (response) { console.log("ダウンロード完了", response); }) .catch(function (e) { console.log("ダウンロードエラー:" + e.message); });}
abortBtn.addEventListener("click", function () { if (controller) controller.abort(); // controller.abortを呼び出してfetchをキャンセルする console.log("ダウンロードが中止されました");});
AbortControllerの他の用途
もちろん、AbortControllerはAjaxの中止だけでなく、DOM仕様文書を見ると、2つの使用例があります:
一つは、AbortControllerを使用してイベントリスナーをキャンセルする便利な例です:
dictionary AddEventListenerOptions : EventListenerOptions { boolean passive = false; boolean once = false; AbortSignal signal;};
signal
をAddEventListener
に渡すことで、abort()
を実行するとイベントリスナーがキャンセルされます。このメソッドは、匿名コールバック関数に特に便利です。
もう一つの例は、Promiseの中止に使用されます。これは、比較的簡潔で自己説明的な方法です…ただし、実際にはAbortControllerでなくてもこの機能を実現できます。Promiseのreject
を取得する方法さえわかれば十分です。私はこの例の重点は、signalのonabortを使用する方法を学ぶことだと思います:
const controller = new AbortController();const signal = controller.signal;
startSpinner();
doAmazingness({ ..., signal }) .then(result => ...) .catch(err => { if (err.name == 'AbortError') return; showUserErrorMessage(); }) .then(() => stopSpinner());
// …
controller.abort();
function doAmazingness({signal}) { return new Promise((resolve, reject) => { signal.throwIfAborted();
// すごいことを始めて、完了したらresolve(result)を呼び出す。 // ただし、シグナルを監視する: signal.addEventListener('abort', () => { // すごいことを中止し、: reject(signal.reason); }); });}
要するに、signalは簡易なメッセージ送信機であり、特定の操作をキャンセルする機能を持っています。パブサブオブジェクトを自分で実装したくない場合は、これを使用するだけで済みます。
AbortControllerの紹介はここまでにしましょうか、みなさんはタイトルを忘れてしまったでしょうか…最後に、Ajaxのキャンセルは本当に役に立つのか、について議論してみましょう。
キャンセルするかしないか、それが問題です
実際、このAjaxのキャンセルはフロントエンドだけの話であり、バックエンドは中止する必要がないため、送信されたリクエストは実行されます。バックエンドは特別な処理をしていない限り、10秒のリクエストをキャンセルしてもバックエンドはまだ処理を続けます。
では、いくつかの記事で見かける「最後のリクエストだけを保持する」という「最適化」は本当に意味があるのでしょうか?
場合によっては、データを変更するPOSTなどのリクエストに対しては、遅いレスポンスが返ってきてもサーバーは既に処理をしているため、前のPOSTをキャンセルして新たに送信することは愚かな行為です。
GETに関しては、一部の限定的な操作に対しては効果があるかもしれません。例えば、非常に長いテーブルを取得しようとしたが取得できず、ユーザーが検索を使って少量のデータを素早く取得してレンダリングした場合、長いテーブルが実際に返ってきたときに検索結果が上書きされるため、この場合はキャンセルが本当に有効です。また、ダウンロードやアップロードのキャンセルもあるかもしれませんが、ほとんど使用されることはないでしょう。
最後に、理にかなっているけれども実際にはあまり役に立たない利点を挙げましょう:キャンセルした後にはリクエストのスロットが1つ空くということです。なぜなら、ブラウザはドメインごとに同時に行えるリクエストの数に制限があるからです。ほとんどの場合、キャンセルよりもタイムアウトの方が実用的です。ええ…5、6個の非常に遅いリクエストを同時に処理している場合を除いては、順番に処理されるのが比較的速いですから…
個人的なアドバイスとしては、この「キャンセル」というのは非常に特殊なケースの特別な処理であるということを知っておくだけで十分であり、インターセプターでキャンセル操作を行う必要はありません。