Skip to content

動態元件與非同步元件

重新認識 Vue.js: 008 天絕對看不完的 Vue.js 3.x 指南

書籍頁面常見這種介面:「商品介紹」與「讀者評論」兩個頁籤,同一塊區域輪流顯示不同內容。用 v-ifv-else 當然做得出來,但頁籤一多,模板就變成一長串條件。這種「同一個位置、輪流換元件」的場景,Vue 有專門的寫法。

:is 切換元件

<component :is="..."> 是一個「代位」標籤:is 綁到哪個元件,這個位置就渲染哪個元件。

src/components/BookIntro.vue

vue
<template>
  <p>從 Vite 到 Pinia,008 天絕對看不完的現代 Vue 指南。</p>
</template>

src/components/BookReviews.vue

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

const draft = ref('')
</script>

<template>
  <div class="reviews">
    <p>目前還沒有評論。</p>
    <textarea v-model="draft" placeholder="寫下你的評論草稿"></textarea>
  </div>
</template>

<style scoped>
.reviews {
  display: grid;
  gap: 0.5rem;
}
</style>

src/App.vue

vue
<script setup>
import { ref } from 'vue'
import BookIntro from './components/BookIntro.vue'
import BookReviews from './components/BookReviews.vue'

const tabs = { BookIntro, BookReviews }
const currentTab = ref('BookIntro')
</script>

<template>
  <main class="app">
    <nav class="tabs">
      <button type="button" @click="currentTab = 'BookIntro'">商品介紹</button>
      <button type="button" @click="currentTab = 'BookReviews'">讀者評論</button>
    </nav>

    <component :is="tabs[currentTab]" />   <!-- currentTab 是誰,就渲染誰 -->
  </main>
</template>

<style scoped>
.app {
  padding: 2rem;
}

.tabs {
  display: flex;
  gap: 0.5rem;
  margin-bottom: 1rem;
}
</style>

按下頁籤,currentTab 改變,<component :is> 的位置就換渲染另一個元件。要加第三個頁籤,在 tabs 物件多放一個元件、加一顆對應的按鈕即可;切換邏輯本身不必增加任何 v-if 分支。

<KeepAlive>:把狀態留下來

上面的範例藏著一個行為:在「讀者評論」打了一半的草稿,切去「商品介紹」再切回來,草稿不見了

這不是 bug。:is 切換元件時,被換掉的元件是真的卸載:實體銷毀、狀態消失,跟上一章 v-if 的行為一致;切回來時是全新的實體,draft'' 重新開始。

如果希望切走的元件「留著」,把 <component :is> 包進 <KeepAlive>

vue
<template>
  <KeepAlive>
    <component :is="tabs[currentTab]" />
  </KeepAlive>
</template>

<KeepAlive> 會把切換掉的元件實體快取起來,而不是銷毀;切回來時直接復用原本的實體。再試一次剛才的操作:草稿還在。

快取不一定要全包。include 可以指定只快取哪些元件:

vue
<template>
  <KeepAlive include="BookReviews">
    <component :is="tabs[currentTab]" />
  </KeepAlive>
</template>

include 比對的是元件的名稱。使用 <script setup> 的元件,名稱預設取自檔名BookReviews.vue 的名稱就是 BookReviews。這個對應是 include 能不能生效的關鍵,改了檔名而忘了改 include,快取就默默失效了。

onActivatedonDeactivated

<KeepAlive> 快取的元件,切走時不會觸發 onUnmounted(它沒有被卸載),切回來也不會重跑 onMounted(實體是舊的)。上一章點到的那對 hooks 在這裡登場:

js
import { onActivated, onDeactivated } from 'vue'

onActivated(() => {
  // 每次「切回來」都會執行(含第一次掛載)
})

onDeactivated(() => {
  // 每次「被切走」都會執行(含最終卸載)
})

常見用途:評論頁籤切回來時重新抓最新資料、切走時暫停輪詢。生命週期的心智不變,只是 KeepAlive 元件的「離場/回場」由這一對 hooks 負責。

非同步元件:defineAsyncComponent

到目前為止,所有元件都是在 import 的當下就載入。但有些元件很重、又不一定會被用到,例如一個只有點開「銷售報表」才需要的圖表元件。defineAsyncComponent() 讓元件用到的時候才載入

src/components/SalesReport.vue

vue
<template>
  <section class="report">
    <h2>銷售報表</h2>
    <p>(想像這裡是一個很重的圖表元件)</p>
  </section>
</template>

<style scoped>
.report {
  border: 1px solid #cbd5e1;
  border-radius: 8px;
  padding: 1rem;
}
</style>

src/App.vue

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

// 傳入動態 import:元件第一次要渲染時,才會去載入這個檔案
const SalesReport = defineAsyncComponent(() =>
  import('./components/SalesReport.vue')
)

const showReport = ref(false)
</script>

<template>
  <main class="app">
    <button type="button" @click="showReport = true">
      查看銷售報表
    </button>

    <SalesReport v-if="showReport" />
  </main>
</template>

<style scoped>
.app {
  display: grid;
  gap: 1rem;
  padding: 2rem;
  justify-items: start;
}
</style>

差別在打包結果:一般 import 的元件會被打進主要的 bundle;defineAsyncComponent 搭配動態 import(),Vite 會把 SalesReport.vue 拆成獨立的檔案,使用者按下按鈕那一刻才下載。頁面初次載入因此更輕。

用瀏覽器開發者工具的 Network 面板實際看一次:初次載入沒有 SalesReport 的檔案,按下按鈕的瞬間才出現一筆新請求。

非同步元件最大宗的應用場景是「每個頁面一個元件、進到該頁才載入」,那與路由密不可分,第六章講 Vue Router 的 lazy loading 時會是主角。

切換、快取、延遲載入都有了。下一節處理兩個更特別的內建元件:一個把畫面「傳送」到元件樹之外,一個讓非同步的內容優雅地「等」。