Skip to content

路由參數、資料與 lazy loading

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

上一節已經能讀到 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 解決「參數改變時如何更新資料」。下一節要處理另一個路由問題:這次導航究竟能不能通過。