【Vue.js】画面上部にプログレスバーを表示する

# :point_up: はじめに
SPAの場合、非同期通信の間に何かしらのローディングを表示しないと
エンドユーザーはボタンがクリックできていないと思い、複数回クリックしてしまいます。
もちろん2度押し防止のボタンコンポーネントを使用することで多重リクエストを防ぐことはできますが、
画面遷移がある場合にはそのボタンは消えてしまいます。
画面遷移中にもローディングやプログレスバーなどは表示させてあげるべきだと考えています。
そこで今回はプログレスバーを実装してみます。

![progressbar.gif](https://qiita-image-store.s3.amazonaws.com/0/207520/c1e0c417-7ce9-394c-802b-c81fb357cee3.gif)

デモとコードも公開しています。ご参考までに。

[CodeSandbox](https://codesandbox.io/s/9349lmyyo)

# :keyboard: 実装
## 概要
画面遷移中にプログレスバーを表示することを考慮するため
表示非表示の切り替えのフラグは```vuex```を使用し、```store.state```で管理します。
アニメーションは```css```の```keyframes```を使用します。

## ProgressBar.vue
プログレスバーコンポーネントを実装します。

```vue:ProgressBar.vue
<template>
  <div class="progress-bar-bg" v-if="isProcessing">
    <div class="progress-bar"></div>
  </div>
</template>

<script>
export default {
  computed: {
    isProcessing() {
      return this.$store.state.progressBar.processing;
    }
  }
};
</script>
```

### template
プログレスバーの表示非表示は```v-if```で切り替えます。
プログレスバーのアニメーションは```css```で行うため、```class```を付与しています。

### script
リアクティブにするため、```computed```で```store```から表示非表示のフラグを返してあげます。

### style
```css``` の ```keyframes``` でプログレスバーのアニメーションを表現します。

```scss
$animation-time: 5s;

.progress-bar-bg {
  width: 100%;
  height: 3px;
  background: #eee;
  .progress-bar {
    overflow: no-display;
    position: absolute;
    left: 0;
    width: 0;
    height: 3px;
    background: #1496ed;
    background-position: 100px 100px;

    animation-name: moveIndeterminate;
    animation-duration: $animation-time;
    animation-timing-function: all $animation-time cubic-bezier(0.235, 0.285, 0.975, 0.055);
  }
}

@keyframes moveIndeterminate {
  0% {
    width: 0;
  }
  10% {
    width: 15%;
  }
  100% {
    width: 100%;
  }
}
```

#### $animation-time
```$animation-time``` はプログレスバーが```0%``` から```100%``` になるまでの時間を指定しています。
数字が大きくなるほどゆっくり進みます。

#### keyframes
```keyframes``` を```0%``` ```10%``` ```100%``` に設定しています。
([GitHub](https://github.com/vuejs/vue)風アニメーション)


## store/index.js
```js:index.js
export default new Vuex.Store({
  state: {
    progressBar: {
      processing: false
    }
  },
  mutations: {
    [START_PROCESS]({ progressBar }) {
      progressBar.processing = true;
    },
    [END_PROCESS]({ progressBar }) {
      const dom = document.querySelector(".progress-bar");
      dom.style.animationPlayState = "paused";
      dom.style.animation = "none";
      dom.style.width = "100%";
      setTimeout(() => {
        progressBar.processing = false;
      }, 300);
    }
  }
});
```

### state

```store.state.progressBar.processing``` この値が```true``` の場合にプログレスバーを表示します。

### mutations

#### START_PROCESS

```START_PROCESS``` が呼び出されると```processing``` の値を```true``` に更新して、
プログレスバーのアニメーションを開始します。

#### END_PROCESS
```END_PROCESS``` が呼び出されると```processing``` の値を```false``` に更新して、
プログレスバーのアニメーションを停止し非表示にします。

プログレスバーが最後まで到達せず、途中で非表示になると分かりにくいので
```JS``` で```CSS``` のアニメーションを停止して、さらに```width: 100%;``` にしてあげると良いでしょう。

```js
dom.style.animationPlayState = "paused";
dom.style.animation = "none";
dom.style.width = "100%";
```

さらに、処理が終わったことをわかりやすくするために```setTimeout``` を使用します。
```width: 100%;``` にした後に```300 [ms]``` プログレスバーを表示した状態にしてから非表示にします。

```js
setTimeout(() => {
  progressBar.processing = false;
}, 300);
```

## App.vue

今回は```App.vue``` にプログレスバーコンポーネントを設置します。
そして、処理開始時と終了時のタイミングで```store``` に```commit``` します。

```store``` で状態を管理しているため、処理終了時も呼び出さなければいけません。
毎回呼び出さなければいけませんが、```store``` で管理することで、
どのコンポーネントやクラスからでも表示非表示の切り替えができます。

### 処理開始

```js
store.commit(START_PROCESS);
```

### 処理終了

```js
store.commit(END_PROCESS);
```

エラー発生時などにも例外処理などでプログレスバーを非表示にする処理を書いてあげないと、
プログレスバーが表示されたままになるため、注意が必要です。


# :pray: おわりに

全体のソースは[CodeSandbox](https://codesandbox.io/s/9349lmyyo) で確認してください。

個人的にはUXの観点から画面全体を覆うローディングよりもプログレスバーが最適だと思っています。
また、プログレスバー表示時に画面全体を透明の背景で覆ってあげると、2度押し防止にもなります。

ボタンコンポーネントを用意して、クリックするとボタンがローダーに変わるのが一番自然な気はします。
しかし、CSRF対策などで画面をまたいでリクエストが発生する場合には、
画面遷移したタイミングでボタンが消えてしまって不自然だと感じます。
なので、画面上部のプログレスバーがオススメです。

以上