Appearance
動態元件與非同步元件
書籍頁面常見這種介面:「商品介紹」與「讀者評論」兩個頁籤,同一塊區域輪流顯示不同內容。用 v-if/v-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,快取就默默失效了。
onActivated 與 onDeactivated
被 <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 時會是主角。
切換、快取、延遲載入都有了。下一節處理兩個更特別的內建元件:一個把畫面「傳送」到元件樹之外,一個讓非同步的內容優雅地「等」。
