Node.js v15に実装されたAbortController
UI開発者 加藤この記事はミツエーリンクス Advent Calendar 2020 - Adventarの14日目の記事です。
少し前にNode.jsのv15がリリースされました。v15にはAbortControllerの実装が追加されています。
AbortController
は簡単に言うとPromiseなどの非同期処理を中断させるために実装されたインターフェースです。Node.jsだけではなくWeb APIにも存在しており、この度Node.jsに実装されたAbortController
はWeb APIをベースにしています(ただしExperimental扱いです)。
(12/15追記:12/9にリリースされたNode.js 15.4.0でExperimentalではなくなりました。)
今回はAbortController
をどのように使うのかをご紹介したいとおもいます。
AbortControllerの使い方
ここからはNode.jsではなく、Web APIとしてのAbortController
について解説していきます。
繰り返しになりますが、AbortController
とは非同期処理を途中で止めるための仕組みです。
分かりやすいユースケースとして、大きなファイルをダウンロード(fetch)しようとしている時のことを考えてみましょう。
ユーザーがファイルをダウンロードするボタンをタップすると、プログレスバーが表示されて、ダウンロードが開始します。しかし、ユーザーが思っていたよりファイルサイズが大きく「ダウンロードを中止したい!」となったとしても、fetchを途中で止める手段はありません。結局、ユーザーは画面のリロードをするしかダウンロードを止める手段がありません。これはあまり良くないUXでしょう。
このような問題を解決するのがAbortController
です。基本的な使い方は以下のようになります。
const cancelButton = document.getElementById('btn-cancel');
function downloadTooBigFile() {
const abortController = new AbortController();
const {signal} = abortController;
cancelButton.addEventListener('click', function () {
abortController.abort();
}, { once: true });
return new Promise(function (resolve, reject) {
// 巨大なファイルをダウンロード後、resolveする処理
signal.addEventListener('abort', function () {
reject();
}, { once: true });
});
}
AbortController
はAbortSignal
クラスのインスタンスをプロパティに持っています。
AbortSignal
はabort
イベントとaborted
プロパティのみを持つシンプルなクラスですが、fetch APIと組み合わせることができます。
AbortSignalとfetch
fetch APIのRequestはAbortSignal
をオプションとして受け取ることができます。具体的にはfetch
の第二引数にAbortSignal
を渡すことで、外からfetch
を中断させることができます。
(async function (){
const cancelButton = document.getElementById('btn-cancel');
const abortController = new AbortController();
const {signal} = abortController;
cancelButton.addEventListener('click', function() {
abortController.abort();
});
try {
await fetch('https://example.com', {signal});
} catch(err) {
console.error(err.name);
}
})();
abortController.abort
メソッドが実行され、AbortSignal
のabort
イベントが発火すると、例外が発生しcatch
ブロックに処理が流れます。
1つのAbortSignal
は複数のfetch
に指定できるため、同時にたくさんのリクエストを管理したい場合に非常に有効です。
注意点として、一度中断されたAbortSignal
に紐づいているfetch
を再度実行することはできません。
中断したfetch
をもう一度行いたい場合はAbortController
のインスタンスも併せて生成しなおす必要があります。
ちなみに、ServiceWorkerでfetch
イベントを制御している際にクライアント側でabort
した場合のふるまいについては、GitHubにイシューが立てられ議論されているようですが、まだ明確な結論は出ていないようです。
AbortSignalとaddEventListener
AbortSignal
はEventTargetのAddEventListenerOptions
としても設定することができます。
設定したAbortSignal
オブジェクトがabort
された時点でイベントリスナーは削除されます。(removeEventListener
と同等)
const cancelButton = document.getElementById('btn-cancel');
const abortController = new AbortController();
const {signal} = abortController;
window.addEventListener('scroll', function () {
// スクロールごとの処理
}, {signal});
cancelButton.addEventListener('click', function() {
abortController.abort();
});
removeEventListener
では第二引数に渡したリスナー関数が無名関数の場合にはイベントリスナーを削除することができませんでしたが、AbortSignal
を設定すれば、無名関数の場合でも削除できます。
また、複数のリスナー関数を一度に削除できるため、コード量を減らしてシンプルに書くことができるでしょう。
さいごに
1つのAbortController
で複数のfetch
やEvent
を管理することは、それぞれの関係性を正しく理解していないと不具合を生む可能性があります。
しかし逆を言えば、あらかじめ仕様を明確にし、関係性を理解したうえで実装することができれば、複雑化しがちな処理の流れをより分かりやすく管理できるはずです!ぜひ、ご活用ください!