キーボードを打ち込むたびなどの高頻度で何かするたびに外部と非同期で通信する際、使うと便利な機能である通信キャンセル機能の紹介です。
ブラウザ上で非同期通信をしたい時、最後の1通信だけ有効となればいい場合がしばしばあります。それは例えばページネーションでページを次々と移動する、サジェスト検索で文字を打ち込むたびに検索する、といった場合です。この時、毎回通信を完了するとページ上が目まぐるしく変わる上、通信完了順によっては最後にブラウザ上から送った通信の結果が最終結果にならないときがあります。これを避けるために非同期通信そのもののキャンセルが望まれます。
axios は JavaScript の非同期通信用ライブラリです、よくウェブページを開いたままAPIと通信するために使います。
Axios
axios/axios: Promise based HTTP client for the browser and node.js
axios の中には通信キャンセル用の機能が備わっており、詳しくは次リンクにあります。
axios/axios: Promise based HTTP client for the browser and node.js#cancellation
v0.22.0前後で使うべき機能が変わります。v0.22.0以降の場合は AbortController という仕組みを使う方がいいです。具体的には次の様に使えます。
// https://github.com/axios/axios#cancellation から引用 // 通信キャンセル用のインスタンスを用意 const controller = new AbortController(); // 通信キャンセル用のインスタンスと通信をつなげる axios.get('/foo/bar', { signal: controller.signal }).then(function(response) { //... }); // 通信をキャンセルする // cancel the request controller.abort()
これで通信をキャンセルできます。キャンセルした通信は次の様にキャンセル済みとなります。スクリーンショットはGoogleChrome上で撮りました。
単独でのキャンセルならば axios のコード例そのままでよいのですが、想定するシーンは同じAPIを高速で連打される中で最後の一つだけ有効にするというパターンですので少々工夫が必要です。例えば素の JavaScript であれば次の様にすることができます。
document.addEventListener('DOMContentLoaded', function (){ /** @var {AbortController|null} 現在通信中の axios をキャンセルするためのコントローラーを保持する変数 */ let currentController = null; // ボタンをクリックする度に axios を走らせる document.getElementById('send-btn').addEventListener('click', function (){ if(currentController){ // 既存の通信が存在するならばここでキャンセル currentController.abort(); } // 通信キャンセル用のインスタンスを用意 const controller = new AbortController(); // 既存の通信のコントローラーとしてこのイベント中の axios を制御する AbortController を変数に格納 currentController = controller; // axios で通信 axios.get('/tmp.php', { signal: controller.signal }).finally(function() { // 通信が完了したならば、キャンセル用のコントローラーを入れる変数を空にする currentController = null; }); }) })
どこかに通信中の axios を停止するための AbortController を用意しておき、通信の度にそこをチェックして、必要ならば通信をキャンセルします。
Reactも方針的には同じなのですが、React自体の処理の重さなのかレンダリングアルゴリズムが原因なのか変数の中に一つコントローラーを保持するのみでは事故が起きやすくなるため、工夫が必要になります。具体的には次の様に配列に通信中コントローラーを格納し、常に全てを見ます。
function App() { /** * レンダリングに直接関係なく、速度重視のために ref を使用 * この中に通信中のコントローラーを貯めていく */ const currentControllerStack = React.useRef([]); const handleClick = () => { currentControllerStack.current = currentControllerStack.current.map(c =>{ // 既存の通信が存在するならばここでキャンセル c.abort() return null; }).filter(c=>!!c);// キャンセル済みのコントローラーは null に変換して filter で削除 // 通信キャンセル用のインスタンスを用意 const controller = new AbortController(); // 既存の通信のコントローラーとしてこのイベント中の axios を制御する AbortController を通信中コントローラー配列に追加 currentControllerStack.current.push(controller); // axios で通信 axios.get('/tmp.php', { signal: controller.signal }).finally(function () { // 通信が完了したならば、キャンセル用のコントローラーを通信中コントローラー配列から除去する currentControllerStack.current = currentControllerStack.current.filter(c => c !== controller); }); } return <button onClick={handleClick}>送信</button> } ReactDOM.createRoot(document.getElementById('app')).render(<App/>);
これで最新の操作による通信結果のみを返すことができ、非同期通信の結果をいい感じに反映しやすくなります