概要
- 表示に時間のかかる情報を扱う際、ユーザーを待たせてしまうことがある
- Qiitaの関連ページの読み込み表示は、読み込み中であることを視覚的に伝えながら、ユーザーの待ち時間を許容させる効果がある
- 同様の読み込み表示を自分のアプリケーションに実装したい
前提条件
- Vue.jsの基本的な知識があること
- SCSSを使用できる環境が整っていること
cssでもできるが、scssを学習中であるためscssで実装している
必要であれば、scss->cssに直して作成してほしい
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; // 高さの設定
}
アニメーションの定義
@keyframes loading-animation {
0% {
background-color: #f5f5f5; // 開始時の背景色
}
50% {
background-color: #e0e0e0; // 中間地点の背景色
}
100% {
background-color: #f5f5f5; // 終了時の背景色
}
}
-
@keyframes
を使ってアニメーションを定義 - 背景色を変化させることで、読み込み中であることを表現
レスポンシブデザインの適用
.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での実装
テンプレートの準備
<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の代替処理
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
に設定し、読み込み完了を示す
全体のコード
<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...
と出したり、スピナー
を回したりするよりも待たされている感は低減する
ため、多用はできないがどうしても重い処理の場合などは実装するとユーザー体験が大幅に向上しそうである
と感じた。