0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Vue 3の非同期コンポーネントを学ぶ|defineAsyncComponentで遅延読み込みしてみる

0
Posted at

前回は<KeepAlive>で画面の状態を残し、<Teleport>でモーダルを前面に表示する形を試しました。
今回は続きとして、必要になったタイミングでコンポーネントを読み込む「非同期コンポーネント」を整理します。

最初からすべての画面を読み込むのではなく、詳細画面のように必要になるまで表示しない部品を後から読み込む考え方を学びたいです。

まだ途中段階なので、勘違いなどあれば教えていただけるとうれしいです。

今日やったこと

defineAsyncComponent()を使い、コンポーネントを遅延読み込みするサンプルを考えました。

通常は、次のようにimportしたコンポーネントを最初から読み込みます。

<script setup>
import ResultDetail from './components/ResultDetail.vue'

// 通常のimportでは、画面を開いた時点でResultDetailも読み込まれる
</script>

<template>
  <ResultDetail />
</template>

非同期コンポーネントでは、defineAsyncComponent()と動的import()を使います。

<script setup>
import { defineAsyncComponent } from 'vue'

// ResultDetailが実際に必要になったタイミングで読み込む
const AsyncResultDetail = defineAsyncComponent(() =>
  import('./components/ResultDetail.vue')
)
</script>

<template>
  <!-- 読み込みが完了したあと、ResultDetailを表示する -->
  <AsyncResultDetail />
</template>

この書き方では、ResultDetail.vueを別のファイルとして分け、表示が必要になるまで読み込みを後回しにできます。たとえば、画像解析結果の一覧は最初に表示し、重い詳細パネルは「詳細を見る」操作のあとで読み込む、といった使い方ができそうです。Vue.js公式ドキュメント ja.vuejs

キャラクター 「最初から全部の画面を準備するのではなく、必要な部品だけ後から受け取るイメージなんだね。」

ゲームのマップにたとえると、最初に全エリアを読み込むのではなく、入口から先へ進むときに次のエリアを準備する感覚に少し近そうです。ただし、画面を速くするために何でも非同期にすればよいわけではなく、最初に必要な部品と後から使う部品を分けて考える必要がありそうです。

つまずいたところ

遅延読み込み中や読み込み失敗時に、何を画面へ表示するかも必要だと気付きました。

defineAsyncComponent()へ設定オブジェクトを渡すと、ローディング中・エラー時・タイムアウト時に表示するコンポーネントを指定できます。

  • loader: 実際にコンポーネントを読み込む処理
  • loadingComponent: 読み込み中に表示するコンポーネント
  • errorComponent: 読み込みに失敗したときに表示するコンポーネント
  • delay: ローディング表示まで待つ時間
  • timeout: 読み込みを待つ最大時間

ローディング表示には、デフォルトで200msの遅延があります。短い読み込みで「読み込み中」が一瞬だけ表示され、ちらつくのを避けるためのようです。Vue.js公式ドキュメント ja.vuejs

最初は「読み込み中の表示は必要なのか」と思いましたが、利用者からすると何も表示されない状態よりも、処理中だと分かるほうが安心できそうです。

画像解析結果の詳細画面であれば、「詳細を読み込んでいます」と表示しておき、失敗した場合は「読み込みに失敗しました。もう一度試してください」と見せる形が使えそうです。

キャラクター 「読み込めるまで何も出ないと、ボタンが効いたのか分からないね。待っている状態も画面で伝えたいところだ。」

今の理解(メモ)

今の自分の理解では、非同期コンポーネントは「コンポーネントファイルを必要なときに読み込む」ための仕組みです。

ここでは、ローディング・エラー・詳細表示をそれぞれコンポーネントとして分けます。

src/
├─ App.vue
└─ components/
   ├─ LoadingPanel.vue
   ├─ ErrorPanel.vue
   └─ ResultDetail.vue

App.vue

サンプルでは、ボタンを押したあとに詳細パネルを表示します。
動作を見やすくするため、setTimeout()で1秒だけ待つ処理を入れています。これはローディング画面を確認するための例であり、実際の通信速度を再現するものではありません。

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

// ローディング中に表示するコンポーネント
import LoadingPanel from './components/LoadingPanel.vue'

// 読み込み失敗時やtimeout超過時に表示するコンポーネント
import ErrorPanel from './components/ErrorPanel.vue'

// 詳細パネルを表示するかどうかを管理する
const isDetailOpen = ref(false)

// 非同期で読み込む詳細コンポーネントを定義する
const AsyncResultDetail = defineAsyncComponent({
  // ResultDetail.vueを必要になったときに読み込む
  // setTimeoutはローディング表示を確認するために1秒待たせるサンプル
  loader: () =>
    new Promise((resolve) => {
      setTimeout(() => {
        resolve(import('./components/ResultDetail.vue'))
      }, 1000)
    }),

  // 読み込み中に表示するコンポーネント
  loadingComponent: LoadingPanel,

  // ローディング画面をすぐに表示する
  delay: 0,

  // 読み込み失敗時に表示するコンポーネント
  errorComponent: ErrorPanel,

  // 3秒を超えても読み込みが完了しなければエラー扱いにする
  timeout: 3000
})

const openDetail = () => {
  // 詳細パネルの表示を開始する
  isDetailOpen.value = true
}

const closeDetail = () => {
  // 詳細パネルを非表示にする
  isDetailOpen.value = false
}
</script>

<template>
  <main class="app">
    <h1>非同期コンポーネントのサンプル</h1>

    <!-- 詳細パネルの読み込みを開始する -->
    <button @click="openDetail">画像解析結果の詳細を見る</button>

    <!-- 詳細を開いているときだけ非同期コンポーネントを表示する -->
    <section v-if="isDetailOpen" class="detail-area">
      <!-- 読み込み中・成功・失敗で表示内容が自動的に切り替わる -->
      <AsyncResultDetail />

      <!-- 表示エリアを閉じる -->
      <button @click="closeDetail">閉じる</button>
    </section>
  </main>
</template>

<style scoped>
.app {
  /* 画面の幅を抑えて中央に配置する */
  max-width: 720px;
  margin: 40px auto;
  padding: 24px;
}

.detail-area {
  /* 詳細表示エリアをカード風に見せる */
  margin-top: 24px;
  padding: 20px;
  border: 1px solid #d0d7de;
  border-radius: 8px;
}
</style>

ボタンを押すと、最初はLoadingPanel.vueが表示されます。1秒後にResultDetail.vueの読み込みが完了すると、詳細画面へ切り替わります。

LoadingPanel.vue

ローディング中に表示するコンポーネントです。

<template>
  <section class="loading-panel">
    <!-- 読み込み中であることを画面上で伝える -->
    <p>画像解析結果の詳細を読み込んでいます...</p>
  </section>
</template>

<style scoped>
.loading-panel {
  /* ローディング中の表示を目立たせる */
  padding: 16px;
  color: #57606a;
  background: #f6f8fa;
  border-radius: 8px;
}
</style>

ResultDetail.vue

読み込みに成功したあとに表示する詳細コンポーネントです。

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

// 詳細画面内で使う仮の表示状態
const isRawDataOpen = ref(false)

const toggleRawData = () => {
  // ボタンで追加情報を表示・非表示にする
  isRawDataOpen.value = !isRawDataOpen.value
}
</script>

<template>
  <section class="result-detail">
    <h2>画像解析結果の詳細</h2>
    <p>判定結果: 対象を検出しました。</p>
    <p>信頼度: 92%</p>

    <!-- 詳細内の追加情報を切り替える -->
    <button @click="toggleRawData">
      追加情報を{{ isRawDataOpen ? '閉じる' : '見る' }}
    </button>

    <!-- ボタンを押したときだけ追加情報を表示する -->
    <p v-if="isRawDataOpen">ここに解析時の補足情報を表示できます。</p>
  </section>
</template>

<style scoped>
.result-detail {
  /* 読み込み後の詳細画面の見た目 */
  padding: 16px;
  border-radius: 8px;
  background: #eef6ff;
}
</style>

ErrorPanel.vue

読み込みに失敗した場合や、timeoutを超えた場合に表示するコンポーネントです。

<script setup>
// 非同期コンポーネントの読み込み失敗時、Vueからerrorが渡される
defineProps({
  error: {
    type: Object,
    default: null
  }
})
</script>

<template>
  <section class="error-panel">
    <h2>詳細を読み込めませんでした</h2>
    <p>通信状態を確認して、もう一度お試しください。</p>

    <!-- 開発中はエラー内容を確認するために表示する -->
    <p v-if="error">エラー内容: {{ error.message }}</p>
  </section>
</template>

<style scoped>
.error-panel {
  /* エラー状態だと分かるように色を変える */
  padding: 16px;
  color: #9a1c1c;
  background: #fff1f1;
  border: 1px solid #f1b8b8;
  border-radius: 8px;
}
</style>

timeoutを確認したい場合は、loader内のsetTimeout()を4秒へ変更すると、3秒のtimeoutを超えます。するとErrorPanel.vueが表示されるはずです。

// 3秒のtimeoutを超えるため、エラー表示を試せる
setTimeout(() => {
  resolve(import('./components/ResultDetail.vue'))
}, 4000)

ただし、実際のアプリでは意図的に遅らせるのではなく、通信障害やファイル取得の失敗などに備えてエラー表示を用意することが目的です。

次にやりたいこと

まずはローディング・成功・エラーの3つの表示を実際に確認したいです。

  • delay: 0とdelay: 200を切り替える
    ローディング表示がすぐ出る場合と、短い読み込みでは表示されない場合を見比べたいです
  • setTimeout()を4秒にする
    timeout: 3000を超えたときにErrorPanel.vueが表示されるか確認したいです
  • ResultDetail.vueへ画像データをpropsで渡す
    一覧から選択した画像の詳細を表示する形へ近づけたいです
  • Teleportと組み合わせる
    非同期で読み込んだ詳細画面をモーダルとして表示できるか試したいです
  • どのコンポーネントを遅延読み込みするか考える
    最初に必要な一覧画面と、あとから開く詳細・設定画面を分けて整理したいです
キャラクター 「まずは読み込み成功だけでなく、待っているときと失敗したときの画面も確認しておきたいね。」

今日の整理

非同期コンポーネントは、後から必要になる画面を必要なタイミングで読み込むための仕組みとして理解しています。

用語 今の理解での役割 利用イメージ 次に確認したいこと
defineAsyncComponent() 非同期で読み込むコンポーネントを定義する 詳細画面・設定画面 同期importとの使い分け
loader コンポーネントを取得する処理 import('./components/ResultDetail.vue') 通信失敗時の動作
loadingComponent 読み込み中に表示する部品 「読み込み中です」の表示 スピナー表示
errorComponent 読み込み失敗時に表示する部品 再試行を促すメッセージ エラー内容の見せ方
delay ローディング表示までの待ち時間 ちらつきの抑制 画面に合う時間
timeout 読み込みを待つ最大時間 長時間待機時のエラー表示 適切な秒数
<Teleport> 表示位置をDOM上で移す 詳細をモーダルで表示 非同期コンポーネントとの組み合わせ

おわりに

defineAsyncComponent()を使うと、コンポーネントを必要になったタイミングで読み込めることが分かりました。詳細画面のように、最初から全員が開くわけではない部品には使えそうです。

また、ローディング中・エラー時の画面も一緒に用意することで、利用者が「今どうなっているか」を判断しやすくなると感じました。今後は、一覧画面から選んだ画像解析結果を非同期で詳細表示する形を試していきたいです。

学習中の内容なので、勘違いや理解不足もあると思います。もし気になる点があれば教えてもらえるとうれしいです。

 注意事項
 本ブログに掲載している内容は、私個人の見解であり、
 所属する組織の立場や戦略、意見を代表するものではありません。
 あくまでエンジニアとしての経験や考えを発信していますのでご了承ください。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?