Appearance
RouterLink 與程式化導航
上一節把 URL 與頁面元件對應起來,接著要處理「怎麼走過去」。固定在模板裡的導覽用 <RouterLink>;完成某個動作後才決定目的地,則用 useRouter()。而頁面想知道自己目前在哪裡,就呼叫 useRoute()。
兩個 composable 名字很像,但責任不同:
useRouter() | useRoute() | |
|---|---|---|
| 取得什麼 | router 實體 | 目前 route 的響應式物件 |
| 用來做什麼 | push、replace、back | 讀取 params、query、meta、name |
| 心智模型 | 操作導航的工具 | 隨成功導航更新的目前位置 |
元件要移動到別頁時用 router,要根據目前網址顯示內容時用 route。雖然 router 也暴露 currentRoute,一般元件直接使用 useRoute() 會更清楚。
用 RouterLink 建立站內連結
最簡單的 to 是路徑字串:
vue
<template>
<nav aria-label="主要導覽">
<RouterLink to="/">書籍</RouterLink>
<RouterLink to="/orders">我的訂單</RouterLink>
</nav>
</template>1
2
3
4
5
6
2
3
4
5
6
RouterLink 最後仍會渲染成 <a>,保留鍵盤操作、另開分頁與瀏覽器歷史等連結語意;不同的是,站內導航會交給 Vue Router,不會讓整份應用重新載入。
to 也能接路由位置物件。搭配上一節的命名路由,書籍連結不必自己拼字串:
vue
<script setup>
const books = [
{ id: 1, title: '重新認識 Vue.js' },
{ id: 2, title: '0 陷阱!0 誤解!8 天重新認識 JavaScript!' },
]
</script>
<template>
<ul>
<li v-for="book in books" :key="book.id">
<RouterLink :to="{ name: 'book', params: { id: book.id } }">
{{ book.title }}
</RouterLink>
</li>
</ul>
</template>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
小提醒
站內頁面使用 RouterLink;真正前往另一個網站或下載檔案,仍使用一般 <a href="...">。不要把所有連結都硬換成 RouterLink。
目前頁面的 active 樣式
當目標路由與目前位置相符,RouterLink 會自動加上 router-link-active;完全相符時再加 router-link-exact-active。可以直接替導覽標出目前位置:
css
.router-link-active {
color: #0f766e;
font-weight: 700;
}1
2
3
4
2
3
4
假設目前在 /books/1/reviews,指向 /books/1 的父層連結會是 active,但不會是 exact active。前者回答「目前是否在這一支路由裡」,後者回答「是否正好就是這個位置」。
useRouter:由程式決定下一站
使用者點「加入購物車」後才前往購物車,目的地取決於一段行為,不適合用固定連結包住按鈕。useRouter() 會取得目前 app 安裝的 router 實體:
vue
<script setup>
import { useRouter } from 'vue-router'
const router = useRouter()
async function saveCartItem(bookId) {
await Promise.resolve(bookId)
// 真實專案會在這裡呼叫購物車 API
}
async function addToCart(bookId) {
await saveCartItem(bookId)
await router.push({ name: 'cart' })
}
</script>
<template>
<button type="button" @click="addToCart(1)">加入購物車</button>
</template>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
push() 會新增一筆瀏覽紀錄;replace() 會替換目前這筆,常用在登入完成後,不希望使用者按上一頁又回到登入表單時:
js
await router.replace({ name: 'orders' })1
要回到上一筆歷史紀錄,呼叫 router.back():
js
router.back()1
push() 與 replace() 都回傳 Promise。下一步依賴導航完成時,用 await 明確等待,錯誤處理也會比較集中。
useRoute:讀取目前位置
useRouter() 是「執行導航的工具」;useRoute() 則是「目前位置的響應式物件」。讀它之前,先把網址拆開來看:/books/1?keyword=vue 裡,/books/1 是路徑,其中的 1 由路由表的 /books/:id 抽成 params;?keyword=vue 是查詢字串 (query string),以 ? 開頭、用 & 串接多組鍵值,不參與路由比對。兩段分別從 route.params 與 route.query 讀取。例如搜尋頁可以同時讀出兩者:
vue
<script setup>
import { computed } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
const bookId = computed(() => route.params.id)
const keyword = computed(() => route.query.keyword ?? '')
</script>
<template>
<p>書籍 id:{{ bookId }}</p>
<p>搜尋關鍵字:{{ keyword }}</p>
</template>1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
網址 /books/1?keyword=vue 會得到 route.params.id === '1' 與 route.query.keyword === 'vue'。route params 即使看起來是數字,來源仍是 URL 字串;需要數值運算時要自行轉型。
什麼資訊該放 params、什麼該放 query?params 是資源身分的一部分,少了它頁面就不成立:不知道 id 就不知道要顯示哪本書。query 則是可選的修飾,拿掉 ?keyword=vue 頁面仍然成立,只是回到未篩選的狀態。搜尋條件、排序、頁碼這類「同一頁的不同狀態」,交給 query 最合適。
這正是上一章預告的「composable 形狀」:useRoute() 與 useRouter() 都在 <script setup> 中取得當前 app 的路由脈絡,跟從 composable 取得狀態與行為是同一種使用心智。
常見錯誤:把 params 當下取出後失去更新
下面的 bookId 只在 setup 執行時取值一次;從 /books/1 切到 /books/2,它不會跟著變:
js
const route = useRoute()
const bookId = route.params.id // ❌ 普通字串1
2
2
把 route 解構出來也一樣:導航後 route.params 會整組換新,先前解構得到的 params 仍指向舊的那組。若要衍生顯示值,用 computed;若要在 id 改變時執行抓資料等副作用,用 getter 當監聽來源:
js
const bookId = computed(() => route.params.id)
watch(
() => route.params.id,
(id) => loadBook(id),
{ immediate: true }
)1
2
3
4
5
6
7
2
3
4
5
6
7
現在我們會導航,也會讀目前路由了。下一節要把參數交給資料 composable,並讓每個頁面只在真正需要時才下載。
