Appearance
路由參數、資料與 lazy loading
上一節已經能讀到 route.params.id,但「讀得到」不代表資料會自動更新。從 /books/1 切到 /books/2 時,Vue Router 會重用同一個詳情元件;如果只在掛載時抓一次,畫面就會留在第一本書。
第一版:直接讀 useRoute
上一章完成的 useBookFetch 接受純值、ref 或 getter,並在來源改變時重新請求。因此最直接的接法,是把 route param 包成 getter:
src/views/BookDetailView.vue:
vue
<script setup>
import { useRoute } from 'vue-router'
import { useBookFetch } from '../composables/useBookFetch.js'
const route = useRoute()
const { data: book, error, isLoading } = useBookFetch(
() => route.params.id
)
</script>
<template>
<p v-if="isLoading">載入中⋯</p>
<p v-else-if="error">{{ error.message }}</p>
<article v-else-if="book">
<h1>{{ book.title }}</h1>
</article>
</template>getter 每次執行都重新讀取響應式的 route.params.id。id 改變時,useBookFetch 的 watcher 會重新執行;上一章做好的 AbortController 與 ownership 規則也會取消舊請求,只讓最新回應更新畫面。這就是 composable 能跨章重用的好處。
不過這個版本有一個代價:BookDetailView 綁死 Vue Router。元件必須在安裝 router 的 app 裡才能工作,單獨預覽或測試時也得先準備 route context。
建議版:用 props 解耦頁面與 router
route record 加上 props: true,Vue Router 會把 params 當成 props 傳給頁面元件:
src/router/routes.js:
js
{
path: '/books/:id',
name: 'book',
component: () => import('../views/BookDetailView.vue'),
props: true,
}頁面改從 defineProps() 接收 id,不再知道 Vue Router 的存在:
src/views/BookDetailView.vue:
vue
<script setup>
import { useBookFetch } from '../composables/useBookFetch.js'
const props = defineProps({
id: { type: String, required: true },
})
const { data: book, error, isLoading } = useBookFetch(
() => props.id
)
</script>
<template>
<p v-if="isLoading">載入中⋯</p>
<p v-else-if="error">{{ error.message }}</p>
<article v-else-if="book">
<h1>{{ book.title }}</h1>
</article>
</template>getter 的角色完全沒變,只是來源從 route.params.id 換成 props.id。元件現在可以在 router 之外直接傳入 id="1",更容易重用與測試;路由到 props 的轉接責任留在 route record。這個版本是本章建議的寫法,也直接回扣上一章的可調適輸入。
TypeScript 版本
本書主線使用 JavaScript;若專案採 TypeScript,可以讓 route param prop 明確維持字串型別:
vue
<script setup lang="ts">
const props = defineProps<{
id: string
}>()
</script>頁面元件的 lazy loading
目前的路由表若在頂端靜態 import 每一個頁面,所有頁面都會進入初始 bundle:
js
import BookDetailView from '../views/BookDetailView.vue'
{
path: '/books/:id',
component: BookDetailView,
}route-level lazy loading 改用回傳動態 import 的函式:
js
{
path: '/books/:id',
component: () => import('../views/BookDetailView.vue'),
}Vite 看到動態 import(),會把 BookDetailView.vue 與只有它用到的相依模組拆成另一個 chunk。使用者第一次進入書籍詳情時才下載;之後再次造訪,瀏覽器可以使用已載入的模組。
這正是第四章非同步元件留下的預告。差別是:一般元件位置要用 defineAsyncComponent() 包裝 loader;Vue Router 的 component 欄位原生就接受 loader,直接寫 () => import(),不要再包一層 defineAsyncComponent()。
打包後可以用兩種方式觀察:查看 dist/assets 是否出現各頁面的 chunk,或在瀏覽器 Network 面板開啟「Disable cache」,重新載入首頁,再第一次進入詳情頁,確認新 chunk 在那一刻才發出請求。
lazy loading 解決「何時載入頁面」,props 與 getter 解決「參數改變時如何更新資料」。下一節要處理另一個路由問題:這次導航究竟能不能通過。
