Vue-Router

學 SPA 的好朋友。

簡述

雖然很久很久以前有寫過 Vue-Router,不過那次實在是…寫得沒有很好,想起來都覺得黑歷史 XD,不過藉由這次重溫 Vue 可以來好好練習一下。

起手式

這邊的範例會用 Vue-CLI 來從頭建立附帶「Vue-Router」的專案,反正主要是要練習路由的觀念,等你懂了以後要在現有專案裡面導入應該也不會有太大問題。

專案建好後先注意到 /src/router/index.js 這個檔案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
import AboutView from '../views/AboutView.vue'

const routes = [
{
path: '/',
name: 'home',
component: HomeView
},
{
path: '/about',
name: 'about',
component: AboutView

// 這是預設的設定,我下面會再解釋
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
// component: () => import(/* webpackChunkName: "about" */ '../views/AboutView.vue')
}
]

const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes
})

export default router

首先要在 Vue 建立路由的基本慣例是寫一隻獨立的 router.js,只要是跟路由有關的東西都會全部寫在這裡。這個跟 React 直接塞在 Component 中的做法還蠻不一樣的,我覺得各有各的好處。

接著來一段一段解釋 code 的部分:

const routes = [{...}]

陣列中的每個物件都代表一個路由,像 '/' 會對應到 HomeView/about 會對應的 AboutView,這個應該蠻好理解的。

const router = createRouter({{...})

前面是設定路由,這邊才是正式配置(Register)路由,其實就是透過 Vue-Router 提供的 method 來做配置,這邊設定了兩個東西:

  1. 使用 history API,另一種是 hash API,簡單來說就是網址有沒有 # 的差別(但如果你有要部署的話最好是多看一些資料搞清楚實際差別,不然一定會踩雷)
  2. 設定路由資料(就是前面建立的 routes

至於我註解起來的那一段是跟「延遲載入」相關的設定。簡單來說就是等我「進到那個路由時再幫我加載需要的 JS 就好,不需要在一開始就載入所有路由」,知道這樣就差不多了,如果真的想知道細節可以參考官方文件

router-link-帶你趴趴走

接著來介紹 Vue-Router 提供的元件 <router-link>,打開 /src/views/App.vue 會看到這個內容:

1
2
3
4
5
6
7
<template>
<nav>
<router-link to="/">Home</router-link> |
<router-link to="/about">About</router-link>
</nav>
<router-view />
</template>

它的用途就跟 <a> 很像,不過有一個很大的差別在於:會不會發 request 去拿 HTML

<a> 的預設行為就是會發一個 request 到 server,但我們在寫 Vue 的時候並不想要這個行為,所以才會有 <router-link> 這個東西來取代它。

順道一提,<router-view /> 就是用來顯示某個路由的內容,可以想成是 React 的 <Routes>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function App() {
return (
<Router>
<nav>
<Link to='/'>Home</Link>
<Link to='/about'>About</Link>
</nav>
{/* router-view */}
<Routes>
<Route {...props} />
<Route {...props} />
<Route {...props} />
</Routes>
</Router>
)
}

接下來補充一些 router-link 的特性,這些特性在某些時候還蠻方便的。

自帶 active class

假設我現在在 /about 的路由,那對應的 link 就會有這樣的 class:

active-link

會自動添加 router-link-activerouter-link-exact-active,通常會拿後面拿個來用(完全匹配)。

總之如果你想要針對 current 做 active 樣式的話這會是一個很方便的機制。

動態路徑

除了原本的寫法以外,你也可以用 v-bind 的方式來指定,這個我直接示範比較快:

1
2
3
4
5
6
7
<template>
<nav>
<router-link :to="{ name: 'home' }">Home</router-link> |
<router-link :to="{ name: 'about' }">About</router-link>
</nav>
<router-view />
</template>

裡面可以傳入我們在 router.js 中寫好的 routes 物件來做對應(忘記內容的話可以拉回去上面看)。

雖然我覺得這樣的缺點是沒辦法一眼看出網址是啥,不過換來的好處是當你要改 path 時完全不需要來改 <router-link>,因為他是參考 name 的值,我覺得這在大型專案上會是很大的優勢。

動態路由參數

假設你有一個路由是 /Products,而它底下可能會有不同的產品頁面,會用 /Products/A/Products/B 之類的方式來表示。

這個時候你不可能去 hardcode 每一個路由,就會用「動態」的方式來撰寫。

在 Vue 寫的方式是這樣:

1
2
3
4
5
6
7
8
9
10
11
12
const routes = [
{
path: '/products',
name: 'ProductList',
component: ProductList
},
{
path: '/jobs/:id',
name: 'ProductDetail', // 用 : 來表示
component: ProductDetail
}
]

所以這個 :id 就會是動態的值,如果我到 /job/A 的話 :id 就會是 Ajob/B 就會是 B,以此類推。

取得參數值

有了動態參數以後通常會需要在對應路由的元件(ProductDetail)來透過他來打 API 之類的,所以這邊介紹一下怎麼取得參數值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<template>
<div>
<h2>Job Detail</h2>
<!-- 取得參數值 -->
<p>Job detail: {{ $route.params.id }}</p>
</div>
</template>

<script>
export default {
created() {
// 取得參數值
console.log('id', this.$route.params.id)
}
}
</script>

$route 是一個物件,裡面會放蠻多跟路由有關的資訊,不過這邊主要會用到的是 params,代表動態參數。

route-object

除了透過 $route 以外還有一種比較特別的方法,就是透過 props

1
2
3
4
5
6
7
8
9
10
11
12
<template>
<div>
<h2>Job Detail</h2>
<p>Job detail: {{ id }}</p>
</div>
</template>

<script>
export default {
props: ['id']
}
</script>

要怎麼拿到這個 props?很簡單,只要到 router.js 開啟這功能就好:

1
2
3
4
5
6
7
8
const routes = [
{
path: '/jobs/:id',
name: 'JobDetail',
component: JobDetail,
props: true // 啟用 props 功能
}
]

手動切換路由

除了用 <router-link> 以外,也可以用手動的方式來切換路由,技術上來說叫做「Programmatic Navigation」:

1
2
3
4
5
6
7
8
9
10
11
12
13
<template>
<h2>Product List</h2>
<div v-for="product of products" :key="product.id">
<h3
@click="$router.push({
name: 'ProductsDetail',
params: { id: product.id }
})"
>
{{ product.title }}
</h3>
</div>
</template>

如果你覺得這樣子看起來很麻煩的話也可以寫成這樣子:

1
2
3
4
5
6
<template>
<h2>Product List</h2>
<div v-for="product of products" :key="product.id">
<h3 @click="$router.push(`/products/${product.id}`)">{{ product.title }}</h3>
</div>
</template>

更多的寫法可以參考這裡

重新導向

這邊直接看 code 吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
const routes = [
// 假設這是新的
{
path: '/jobs',
name: 'JobList',
component: JobList
},
// 假設這是舊的
{
path: '/all-jobs',
redirect: 'jobs'
}
]

為了把舊的網址都導向到新的位置,我們可以像上面這樣子來重新導向。

404 頁面

Vue 的寫法比較特別一點,要透過 params 搭配正則表達式的方式來做,像這樣:

1
2
3
4
5
6
7
const routes = [
{
path: '/:catchAll(.*)',
name: 'NotFound',
component: NotFound
}
]

裡面的 catchAll 是自訂名稱,你喜歡的話也可以叫 abc。總之重點在於 () 中的正則表達式,只要符合條件就會進到這個路由。

小陷阱-在不同路上使用同的元件

這是我自己實作時踩到的雷,先來看看當時的情境:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const routes = [
{
path: '/',
name: 'home',
component: Home
},
{
path: '/add-new-project',
name: 'AddProject',
component: Form // 共用元件
},
{
path: '/project/:id',
name: 'EditProject',
component: Form, // 共用元件
props: true
}
]

之所以會共用同一個 Form 是因為這兩個只差在:

  1. 表單的初始值
  2. 提交表單時要更新資料 or 建立資料

可是當時就遇到了這樣的狀況:

bug

明明就切換到不同的路由上了,但內容卻被緩存了下來,甚至根本沒有被 unmount(自己用生命週期 hook 實測的結果)。

不過在 Vue 裡面這是正常的現象,AKA「節能機制」。簡單來說就是 Vue 會盡可能把能重複利用的東西留下來用的關係。

要解決這個辦法的作法就是加上 key,至於要加在哪?以我們這邊的問題來說是出現在「路由顯示上」,所以很直覺的就會加在這裡:

1
<router-view :key="$route.path" />

只要讓每一個路由都有唯一的 key,Vue 就會直接視為是不同的東西,強制重新渲染,就能解決這個問題了。

詳細可以參考這篇

Vue-Composition API 前言 Vue-好像很神秘的「h」
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×