vue-router + ajaxで前のリクエストが終わっていないときに前のリクエストをキャンセルする

こんにちは、かみけんと申します:relaxed:
最近はVue.js(vuex + vue-router)でSPAを作っています。

今回はサイドバーなどの実装をしていたのですが、例えばリンクをクリックした直後に『あ、ごめん、ホントはこっちだった (ポチッ)』というときのような、前のリクエストが終わっていないときのrouterでの前のリクエストのajaxのキャンセルについて意外と情報がなかったのでまとめてみます。
(なお、今回の例でajaxに用いているライブラリはaxiosになります。）

## まずはrouterからajaxの処理が呼ばれるあたりから
コードを紹介していきます。
実際に実装したものから中身をゴリゴリ削ったもので、ココに書いてあるコードについてはテストをはしていませんが概ねの流れは掴めるはずです。

### components周り
まずはリクエスト処理を呼んでいるrouterで指定されているようなcomponentのコードです。
ここは多分至って普通です。vue-routerのドキュメントを参考にしたものです。

```js:component.vue
<script>
// Request はaxiosを扱っているjsファイルに置き換えて参考にしてください
import Request from 'your/request.js'
export default {  
    // ... いろいろと略 ...
    created() {                                                      
        this.fetchData()                                             
    },                                                               
    watch: {                                                         
        // ルートが変更されたらこのメソッドを再び呼び出し            
        '$route': 'fetchData'                                        
    },                                                               
    methods: {           
        fetchData() {
            // vuexでLoaderのフラグを管理してたりする場合のフラグ
            this.$store.dispatch('setIsLoading', true)
            Request.post(this.$route.params.id, (result, errorMessage) => {
                this.$store.dispatch('setIsLoading', false)
                // 成功/失敗時の処理とか
                // (失敗時はdataや$storeなどに値が入らないように注意)
                // ローダー画面などをリクエスト中に代わりに出す場合には、
                // ajaxキャンセル時はローダー画面を一応終了するようにしたほうが良いかも
            }
        }
    },    
    // ... いろいろと略 ...
}
```
</script>

### Request周り
次はリクエスト関連を扱うaxios周りの実装です。
キャンセルする時のtokenの管理をしたり、キャンセル用のメソッドも用意します。

```js:request.js
import axios from 'axios'

class Request {
    constructor() {
        this.CancelToken = axios.CancelToken
        this.sources = {}
    }

    // ポストする部分のメインの処理
    post(id, callbackFn) {
        // POSTパラメータを作る
        let params = new URLSearchParams()
        params.append('id', id);

        // キャンセル用のトークンを作る(ついでに用途に応じてtokenが別々に保存されるようにする)
        let sourceKey = 'content1' // 実際の実装は用途に応じて動的に変えている
        this.sources[sourceKey] = this.CancelToken.source()
        let cancelToken = {
            cancelToken: this.sources[sourceKey].token
        }

        axios.post('url/to/api', params, cancelToken).then(response => {
            // ... いろいろと略 (エラーハンドリングとか) ...
            callbackFn(response, err)
        }).catch(thrown => {
            // キャンセルしたときは何もしたくないときの例
            // コールバック先でキャンセルによる終了が分かればOKです(実装はお任せします)
            let err = (axios.isCancel(thrown)) ? null : thrown
            callbackFn(null, err)
        })    
    }

    // キャンセルしたいときに叩く
    cancel(sourceKey) {
        if (typeof this.sources[sourceKey] != 'undefined') {
            this.sources[sourceKey].cancel('Operation canceled by the user.');
        }
    }
}

// シングルトンで生成
export default new Request()
```
（実際の実装はもっとメソッド分けましたが、ここでは流れが追いやすいようにまとめました）

### Vueインスタンス
キャンセル発火の処理はルートのVueインスタンスの生成の前に書きます

```js:index.js
import Vue     from 'vue/dist/vue.js'
import Router  from 'vue-router'
import routes  from './routes.js'

// 上記のaxiosを扱うJSファイル
import Request from 'your/request.js'

// ... いろいろと略 ...

Vue.use(Router)
const router = new Router({routes})

// キャンセル処理用
router.beforeEach((to, from, next) => {
    // 前の通信が終わる前に次がリクエストされる場合は前をキャンセル
    Request.cancel('content1')
    next()
})

// ... いろいろと略 ...

new Vue({
    el: '#app',
    router,
    store,
    render: h => h(App)
})
```

## 結果
![cancel.gif](https://qiita-image-store.s3.amazonaws.com/0/73446/bf3a998a-a37d-2737-2123-b29885939eb6.gif)
ちゃんとキャンセルできました:muscle:
これで、前のリクエストを待つ必要はなくなりますね:laughing:

## ハマったところなど
リクエスト時にv-ifで切り替えてローダーっぽい画面に出すような処理にしていたのですが、
キャンセル処理になってもローダーからちゃんと戻ってコンポーネントがレンダリングされるようにしないと、次のrouter処理が発火しない現象に悩まされたりしたので、一応ご注意ください。

あとは、キャンセルしたくない処理とかももちろんあると思いますので、cancelのトークンの管理などで分けて実装するなどで気をつけましょう。

### 参考
abort all Axios requests when change route use vue-router
https://stackoverflow.com/questions/51439338/abort-all-axios-requests-when-change-route-use-vue-router