画面の読み込みをスタイリッシュに待つ方法

## 概要

- 表示に時間のかかる情報を扱う際、ユーザーを待たせてしまうことがある
- Qiitaの関連ページの読み込み表示は、読み込み中であることを視覚的に伝えながら、ユーザーの待ち時間を許容させる効果がある
- 同様の読み込み表示を自分のアプリケーションに実装したい

## 前提条件

- Vue.jsの基本的な知識があること
- SCSSを使用できる環境が整っていること

:::note info
cssでもできるが、scssを学習中であるためscssで実装している
必要であれば、scss->cssに直して作成してほしい
:::

## SCSSでのスタイリング

### 読み込み中のスケルトンカードのスタイル定義

```scss
.loading-card {
  background-color: #f5f5f5; // 背景色の設定
  border-radius: 8px; // 角丸の設定
  padding: 16px; // 内側の余白設定
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); // 影の設定
  animation: loading-animation 1.5s infinite; // アニメーションの適用
  height: 150px; // 高さの設定
}
```

### アニメーションの定義

```scss
@keyframes loading-animation {
  0% {
    background-color: #f5f5f5; // 開始時の背景色
  }
  50% {
    background-color: #e0e0e0; // 中間地点の背景色
  }
  100% {
    background-color: #f5f5f5; // 終了時の背景色
  }
}
```

- `@keyframes`を使ってアニメーションを定義
- 背景色を変化させることで、読み込み中であることを表現

### レスポンシブデザインの適用

```scss
.example-list {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 20px;

  &-container {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); // レスポンシブなカラム設定
    grid-gap: 20px; // カード間の間隔設定
    width: 100%; // 幅の設定
  }
}

.loading-container {
  @extend .example-list-container;
}
```

- `grid-template-columns`と`repeat()`関数を使ってレスポンシブなカラム設定
- `grid-gap`でカード間の間隔を設定
- `.loading-container`は`.example-list-container`と同じスタイルを継承

## Vue.jsでの実装

### テンプレートの準備

```html
<template>
  <div class="example-list">
    <div v-if="loading" class="loading-container">
      <div v-for="i in 20" :key="i" class="loading-card"></div>
    </div>
    <div v-else-if="examples.length > 0" class="example-list-container">
      <!-- 実際のデータの表示 -->
    </div>
    <div v-else class="no-examples">No examples found.</div>
  </div>
</template>
```

- `v-if`ディレクティブを使って、`loading`の状態に応じて表示を切り替える
- `v-for`ディレクティブを使って、20個のスケルトンカードを表示

### APIの代替処理

```javascript
onMounted(async () => {
  try {
    // 3秒待機してからデータを取得する
    await new Promise((resolve) => setTimeout(resolve, 3000))

    // ランダムでエラーを発生させる
    if (Math.random() < 0.5) {
      throw new Error('Failed to fetch examples')
    }

    // ダミーデータを設定する
    examples.value = [
      { id: 1, title: 'Example 1', description: 'This is example 1' },
      { id: 2, title: 'Example 2', description: 'This is example 2' },
      { id: 3, title: 'Example 3', description: 'This is example 3' },
    ]
  } catch (error) {
    console.error('Failed to fetch examples:', error)
  } finally {
    loading.value = false
  }
})
```

- `onMounted`ライフサイクルフックで、コンポーネントのマウント後に実行
- 3秒待機してからデータの取得をシミュレート
- ランダムでエラーを発生させる
- ダミーデータを設定する
- `finally`ブロックで、`loading`状態を`false`に設定し、読み込み完了を示す

## 全体のコード

```vue
<template>
  <div class="example-list">
    <div v-if="loading" class="loading-container">
      <div v-for="i in 20" :key="i" class="loading-card"></div>
    </div>
    <div v-else-if="examples.length > 0" class="example-list-container">
      <div
        v-for="example in examples"
        :key="example.id"
        class="example-card"
      >
        <h3 class="example-title">{{ example.title }}</h3>
        <p class="example-description">説明: {{ example.description }}</p>
      </div>
    </div>
    <div v-else class="no-examples">No examples found.</div>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue'

const examples = ref([])
const loading = ref(true)

onMounted(async () => {
  try {
    await new Promise((resolve) => setTimeout(resolve, 3000))

    if (Math.random() < 0.5) {
      throw new Error('Failed to fetch examples')
    }

    examples.value = [
      { id: 1, title: 'Example 1', description: 'This is example 1' },
      { id: 2, title: 'Example 2', description: 'This is example 2' },
      { id: 3, title: 'Example 3', description: 'This is example 3' },
    ]
  } catch (error) {
    console.error('Failed to fetch examples:', error)
  } finally {
    loading.value = false
  }
})
</script>

<style lang="scss" scoped>
.example-list {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 20px;

  &-container {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
    grid-gap: 20px;
    width: 100%;
  }
}

.loading-container {
  @extend .example-list-container;
}

.loading-card {
  background-color: #f5f5f5;
  border-radius: 8px;
  padding: 16px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  animation: loading-animation 1.5s infinite;
  height: 150px;

  @keyframes loading-animation {
    0% {
      background-color: #f5f5f5;
    }
    50% {
      background-color: #e0e0e0;
    }
    100% {
      background-color: #f5f5f5;
    }
  }
}

.example-card {
  background-color: #f5f5f5;
  border-radius: 8px;
  padding: 16px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);

  .example-title {
    margin-bottom: 8px;
  }
}

.no-examples {
  font-size: 18px;
  font-weight: bold;
}
</style>
```

## まとめ
今回は簡単に点滅するだけのアニメーションを追加した。個人的には満足できるが、Qiitaのものはもっと動きがあり面白く、実装は複雑になりそうだとも感じた。しかし、この程度の簡単なアニメーションであっても簡単に`Loading...`と出したり、`スピナー`を回したりするよりも`待たされている感は低減する`ため、`多用はできないがどうしても重い処理の場合などは実装するとユーザー体験が大幅に向上しそうである`と感じた。