# 4-2 Vue Router 路由設定
在上一個小節中,我們已經示範如何將 Vue Router 加入到專案當中, 那麼,這個小節將從 Vue Router 的路由設定檔介紹出發,並透過幾個實際的案例來示範 Vue Router 的各種功能。
小提醒
自本章節起,有關 Vue Router 的解說都會搭配 Vue CLI / webpack 來操作,請讀者多加注意。
在上一個小節最後部分曾介紹過,我們可以透過 route.js 檔案來設定 Vue Router 的路由,
而設定的選項基本上都會放在 createRouter 這個 Option 裡頭:
// route.js
import { createRouter, createWebHistory } from 'vue-router'
import Home from './views/Home.vue'
import About from './views/About.vue'
export const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/', component: Home },
{ path: '/about', component: About },
],
});
2
3
4
5
6
7
8
9
10
11
12
其中最重要的就是 history 與 routes 兩個部分了。
# history 路由模式
首先來講講 history,這個選項在 Vue Router v3 以前被稱為 mode, v4 開始改為 history,
指的是 Vue Router 處理前端路由的不同方式,分別是 Hash Mode 以及 HTML5 (History API) Mode 兩種。
# Hash 模式
在 HTML5 的 History API 還沒出現之前,想要控制 URL 又不能換頁,只能透過 URL hash,也就是 # (井號)。
這個 Hash (#) 原本在網頁裡代表的是「錨點」的含義, # 後面接的是這個網頁的某個位置。
https://book.vue.tw/#app
以上面這個網址為例,如果網頁裡面有某個節點的 id="app" 像是 <div id="app"></div> ,
那麼當這個連結被開啟的同時,瀏覽器就會自動把位置捲到這個 <div id="app"></div> 的地方。
而在同一個頁面中,若只是改變了 # 後面的文字,是不會讓整個網頁重整或重新讀取的,
而且當 URL Hash 被更新時,同時也會增加一筆記錄到瀏覽器的瀏覽歷史裡,也就是說,
我們可以透過瀏覽器的「上一頁」或「下一頁」來切換不同的 # 位置,而且也不會引發頁面的重新讀取。
於是當時的人們就利用 Ajax 搭配 hashchange 事件,去監聽 URL Hash 的狀態來決定目前顯示的內容,
這算是最早的前端路由解決方案。
在 Vue Router 裡頭,我們只要將 history 設定為 createWebHashHistory() 即可開啟 Hash Mode:
import { createRouter, createWebHashHistory } from 'vue-router'
const router = createRouter({
history: createWebHashHistory(),
routes: [
//...
],
})
2
3
4
5
6
7
8
而 createWebHashHistory() 預設的路徑為 location.pathname 或 / 根目錄,若想要額外處理則可以在裡面加上路徑的字串,像是 createWebHashHistory('/folder/'),對應的就是 https://example.com/folder/# 。

Hash Mode 的好處是我們無需調整後端的設定,甚至使用 file:// 開頭的檔案協定來直接開啟網頁也能順利運作。
不過這種做法也有缺點,搜尋引擎在收錄頁面的時候,會自動忽略 URL 裡面帶有#號的部分,因此也不利於網站的 SEO。
# HTML5 (History API) 模式
還好自從 HTML5 的出現,新的規範提供了 History API 的擴充,我們可以透過 pushState() 、 replaceState() 的方式更新 URL,
以及原本就有的 history.go() 、 history.back() 來指定頁面的路徑切換,同時也提供了 state 物件來讓開發者暫存與讀取每一頁的狀態。
使用方式也很簡單,我們將 history 設定為 createWebHistory() 即可開啟 HTML5 (History API) Mode:
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(),
routes: [
//...
],
})
2
3
4
5
6
7
8
與 createWebHashHistory() 一樣,createWebHistory() 也允許我們在裡面加入相對路徑的字串,如 createWebHistory('/folder/') ,對應的 URL 就是 https://example.com/folder/ 。

所以,採用 HTML5 Mode 下的網址,就會像原本大家所熟悉的那樣:
https://book.vue.tw/app
但就需要後端路由 (請見 4-1 小節後端路由部分) 搭配,否則當我們重新整理網頁後,就會得到一個 HTTP 404 找不到網頁的錯誤訊息。
備註
Vue Router 預設也是採取 HTML5 (History API) Mode 。
# routes 路由比對
createRouter( ) 的第二個控制項是 routes,是個陣列型態的內容,用來處理路徑與 Vue 實體元件比對的設定。
以我們在 4-1 小節最後介紹的範例:
// route.js
import { createRouter, createWebHistory } from 'vue-router'
import Home from './views/Home.vue'
import About from './views/About.vue'
export const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/', component: Home },
{ path: '/about', component: About },
],
});
2
3
4
5
6
7
8
9
10
11
12
我們在 routes 控制項裡面設定了 path: '/' 對應的會是 Home.vue 這個元件。
換句話說,當我們在專案 http://localhost:8080/ 的 URL 下,在 <router-view> 所顯示的就會是 Home.vue 元件的內容。
相對地,若我們在 http://localhost:8080/about 下,對應出現的就會是 About.vue 這個元件。
# 動態路由
在 routes 控制項裡面,除了如同前面所說一個蘿蔔一個坑之外,Vue Router 也提供了類似後端路由的功能,透過 URL 的動態路徑來讓不同的 URL 路徑都能指向同一個 Vue 元件實體。

這裏我們使用在本書 2-5 小節曾經介紹過的 jsonplaceholder 來作為 API 的提供者。
首先,我們在 app.vue 裡面建立 <router-link> 與 <router-view>,
並透過 v-for 生成 /users/1 ~ /users/5 五個連結:
<template>
<ul>
<li v-for="i in 5" :key="i">
<router-link :to="`/users/${i}`"> /users/{{ i }} </router-link>
</li>
</ul>
<router-view></router-view>
</template>
2
3
4
5
6
7
8
9
再來是 Vue Router 的 route.js 設定檔,我們 routes 選項裡設定 path: "/users/:userId"
以便將 userId 傳入 User 元件,這樣 User 元件才能透過 this.$route.params.userId 順利取得網址的參數:
// route.js
import { createRouter, createWebHistory } from "vue-router";
import User from "./views/User.vue";
export const router = createRouter({
history: createWebHistory(),
routes: [{ path: "/users/:userId", component: User }]
});
2
3
4
5
6
7
8
path 裡面的 :userId 表示 URL 中 /users/ 後面的數值會被傳入 userId,而對應的 component 則是 User.vue。
最後來處理 User 元件 views/User.vue。
由於 routes 已經設定好了,所以在裡面我們可以透過 this.$route.params.userId 來取得網址的 userId:
<template>
<h1>UserID: {{ $route.params.userId }}</h1>
<pre>{{ userInfo }}</pre>
</template>
<script>
export default {
data() {
return {
userInfo: {},
};
},
computed: {
userId() {
// 假設 URL 為 /users/1, $route.params.userId 的值就是 1
return this.$route.params.userId;
},
},
watch: {
userId: async function (val) {
this.userInfo = await this.fetchUserInfo(val);
},
},
methods: {
async fetchUserInfo(id) {
return await fetch("https://jsonplaceholder.typicode.com/users/" + id)
.then((response) => response.json())
.then((json) => json);
},
},
async created() {
this.userInfo = await this.fetchUserInfo(this.userId);
},
};
</script>
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
30
31
32
33
34
35
36
注意,當我們使用動態路由切換頁面在同一個元件上,這個元件並不會被銷毀再重新載入,所以上面的例子中我們使用 watch 來監測 this.$route.params.userId 也就是 this.userId 的更新,並再次透過 this.fetchUserInfo 取得新資料渲染至頁面上。
當使用者點擊 /users/1 ~ /users/5 任何一個連結時,都會對應到 User.vue 元件,並且呼叫 this.fetchUserInfo() 並顯示對應的使用者資訊了。
除了上面說的 watch 監控 $route.params.userId 的更新之外,也可以利用 Vue Router 的 beforeRouteUpdate Hook 來取得 URL 變更前/後的頁面/元件資訊:
async created() {
this.userInfo = await this.fetchUserInfo(this.userId);
},
async beforeRouteUpdate(to, from) {
console.log(to, from);
},
2
3
4
5
6
備註
關於 beforeRouteUpdate 與其他 Hooks 的細節,後面介紹 「Navigation Guards」 時還會有詳細解說。
而 $route 與 to / from 物件內提供的資訊分別有:
{
fullPath,
hash,
matched,
meta,
name,
params,
path,
query
}
2
3
4
5
6
7
8
9
10
$route 物件除了提供 $route.params 讓開發者可存取自行定義的變數名稱外 (如範例中的 userId) ,另外也提供 $route.query 、 $route.hash 以及 $route.path 等屬性來對應 URL 的 location.search、location.hash 與 location.pathname 等資訊。
# 自訂路由參數格式
另外,我們也可以在 path 裡面透過正規表達式 (regexp) 指定 param 裡面的格式,
const routes = [
// /:orderId -> matches only numbers
{ path: '/:orderId(\\d+)' },
// /:productName -> matches anything else
{ path: '/:productName' },
]
2
3
4
5
6
像上面的範例, /:orderId 對應的內容就只能是數字,而 /:productName 則是沒有任何限制。
# 路由比對失敗 (找不到網頁)
另外,當使用者嘗試透過不存在的 URL 進行讀取時,我們也可以透過 '/:pathMatch(.*)*' 來指定「所有的」路由都會連到此元件:
const routes = [
// will match everything and put it under `$route.params.pathMatch`
{ path: '/:pathMatch(.*)*', name: 'NotFound', component: NotFound },
]
2
3
4
這樣就可以確保使用者嘗試進入不在規則內的 URL 都被轉導至 NotFound 元件。
不過需要注意的是,含有星號 * 的路由應該要放在所有規則的最後,以免原本正確的 URL 還沒配對到就被 NotFound 的規則轉走了。
# 非強制的路由參數
除了前面介紹的幾種規則外,我們也可以透過 ? 問號來指定當某個 param 可以是有,也可以沒有的比對情況:
const routes = [
// will match /users and /users/posva
{ path: '/users/:userId?' },
// will match /users and /users/42
{ path: '/users/:userId(\\d+)?' },
]
2
3
4
5
6
像這樣,我們在 :userId 後面加個 ?,就表示當 URL 為 /users 或者 /users/xxx 的時候,
都會被帶到這個路由所指定的元件中。
# 巢狀路由
Vue Router 也允許我們在 <router-view> 渲染的元件內,再放一層 <router-view>,這樣的做法就被稱為「巢狀路由」 (nested routes),或稱「嵌套路由」。

以前一個範例繼續延伸,我們修改 User.vue 並在模板內新增一個 <router-link> 把目標指向 /users/${userId}/posts,
並新增一組 <router-view> 用來顯示目前 User 的所有文章列表:
<!-- User.vue -->
<template>
<h1>User: {{ userId }} - {{ userInfo?.name }}</h1>
<div>username: @{{ userInfo.username }}</div>
<div>email: {{ userInfo.email }}</div>
<div>phone: {{ userInfo.phone }}</div>
<hr />
Show
<router-link :to="`/users/${userId}/posts`">
/users/{{ userId }}/posts
</router-link>
<hr />
<!-- 等等要給 Post.vue 的位置 -->
<router-view></router-view>
</template>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
然後新增一個 Post.vue,同樣在 created 階段透過 fetch 取得遠端的 posts 列表,並顯示在模板中:
<!-- Post.vue -->
<template>
<h1>Post from User-{{ userId }}</h1>
<ol>
<li v-for="post in posts" :key="post.id">
<h3>{{ post.title }}</h3>
<p>{{ post.body }}</p>
</li>
</ol>
</template>
<script>
export default {
data() {
return {
posts: [],
};
},
computed: {
userId() {
// 取得網址路由提供的 userId
return this.$route.params.userId;
},
},
methods: {
// api: 取得指定 user 的 post 列表
async fetchUserPosts(id) {
return await fetch(
"https://jsonplaceholder.typicode.com/posts?userId=" + this.userId
).then((response) => response.json());
},
},
async created() {
this.posts = await this.fetchUserPosts(this.userId);
},
};
</script>
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
30
31
32
33
34
35
36
37
最後,調整 route.js,在原本對應 User 的 routes 增加一個 children 陣列來表示它的第二層路由:
// route.js
import { createRouter, createWebHistory } from "vue-router";
import User from "./views/User.vue";
import Post from "./views/Post.vue";
export const router = createRouter({
history: createWebHistory(),
routes: [{
path: "/users/:userId",
component: User,
children: [
{
// /user/:id/posts - 取得指定 User 的 post 內容
path: 'posts',
component: Post,
},
]
}]
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
小提醒
小心 children 裡面的 path 不要加上 / 開頭,否則會被帶回根目錄。
假設我們在網址列 http://localhost:8080/users/1/ 就可以看到 1 號使用者的資訊,
點一下下方的 「 Show /users/1/posts 」 之後,你會發現網址此時變成 http://localhost:8080/users/1/posts,
而且在下方我們剛剛新增 <router-view> 的位置會顯示 1 號使用者所有的貼文列表:
像這樣,我們可以在 <router-view> 內的元件再套層 <router-view> 來達到頁面元件可以依照路由顯示鑲嵌套層的效果,
這就是 Vue Router 巢狀路由的威力。
# 具名路由
在 routes 物件內,除了 path 之外,我們也可以指定 name 來作為元件的路由參考:
routes: [
{ path: '/', name: 'home', component: Home },
{ path: '/foo', name: 'foo', component: Foo },
{ path: '/bar/:id', name: 'bar', component: Bar }
]
2
3
4
5
如果我們希望新增 <a> 連結,並分別指向這個路由時,我們可以為目標新增 <router-link>,然後將 to 指定給這個目標:
<ul>
<li><router-link :to="{ name: 'home' }">home</router-link></li>
<li><router-link :to="{ name: 'foo' }">foo</router-link></li>
<li><router-link :to="{ name: 'bar', params: { id: 123 }}">bar</router-link></li>
</ul>
2
3
4
5
像上面這樣,對應的 URL 結果就會是 / 、 /foo 以及 /bar/123 了。
# 具名視圖
在 routes 物件內除了 path 可以取名之外,對應的 <router-view> 也可以幫它命名,
如果我們一個網頁裡面同時包含多個 <router-view> 的時候,就是一個很好用的功能。

像這樣,假設我們的 / 同時包含了三組 <router-view>,這個時候我們就可以為 <router-view> 加上 name 屬性:
<router-view class="view nav-block" name="Nav"></router-view>
<router-view class="view header-block" name="Header"></router-view>
<router-view class="view body-block"></router-view>
2
3
這時候,我們需要在 routes 加上 components 來指定每一個 <router-view> 所對應的 Vue 元件:
import Body from './body.vue';
import Header from './header.vue';
import Nav from './nav.vue';
// components 的 s 不要忘記了!
const routes = [
{
path: '/',
components: {
// 沒有設定 `name` 的 <router-view> 我們可以透過 default 指定給它。
default: Body,
Header: Header,
Nav: Nav,
},
},
];
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
像這樣,這三個 <router-view> 就可以正確渲染出對應的元件內容了。
# 巢狀具名視圖
實際開發中,除了首層會有多個 <router-view> 的情況,在子層級的元件也有可能出現多個 <router-view>,像這樣:

Page.vue 的模板結構基本上跟前一個範例差別不大:
<!-- Page.vue -->
<div>
<router-view class="view nav-block" name="Nav"></router-view>
<router-view class="view header-block" name="Header"></router-view>
<router-view class="view body-block"></router-view>
</div>
2
3
4
5
6
在 routes 的設定中,將 components 屬性指定於 children 之下:
import Page from './page.vue';
import Body from './body.vue';
import Header from './header.vue';
import Nav from './nav.vue';
const routes = [
{
path: '/pages',
component: Page,
children: [
components: {
default: Body,
Header: Header,
Nav: Nav,
},
]
}
];
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
在使用上沒有太大的不同,唯一要注意的是 component 與 components 的差異。
# 路由轉址
在 routes 裡面,除了透過 path 之外,我們也可以加上 redirect 選項來指定某個路由要轉址到某個目標。
例如我們可以直接在 redirect 加上路徑像是 /,這時就會將 /home 轉頁到 '/' 的位置。
const routes = [
{ path: '/home', redirect: '/' }
]
2
3
當然也可以透過 name 來指定轉頁的目標,像:
const routes = [
{ path: '/app', redirect: { name: 'appPage' } }
]
2
3
除了直接指定 URL 與 name 之外, redirect 也允許我們透過 function 的形式來指定目標:
const routes = [
{
// /search/screens -> /search?q=screens
path: '/search/:searchText',
redirect: to => {
// the function receives the target route as the argument
// we return a redirect path/location here.
return { path: '/search', query: { q: to.params.searchText } }
},
},
{
path: '/search',
// ...
},
]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
像這樣,我們就可以將 /search/screens 轉址為 /search?q=screens,使原本的 URL path 變成 query string 的一部分。
小提醒
若在 routes 裡面加上 redirect 選項時,原本的 component 可以被省略,因為當頁面被轉走就永遠看不到這個 component 了。
而唯一的例外是前面講過的「巢狀路由」,若此路由規則同時有 children 與 redirect 屬性,則必定要有 component 屬性來處理承接 <router-view> 元件。
# 路由別名
與 redirect 轉址類似,路由別名 alias 雖然也能讓使用者在 /home 看到 / ,也就是 Homepage 的內容,如:
const routes = [
{ path: '/', component: Homepage, alias: '/home' }
]
2
3
但這個 alias: '/home' 表示當使用者透過 /home 瀏覽網頁時,雖然看到的跟 / 一樣的畫面,
但 URL 仍保持在 /home 下,不會被強制轉到 /。
而在 redirect 的情況下, URL 會被轉到 / 的位置。
當然我們也可以同時指定多個別名:
const routes = [
{
path: '/users',
component: UsersLayout,
children: [
// this will render the UserList for these 3 URLs
// - /users
// - /users/list
// - /people
{ path: '', component: UserList, alias: ['/people', 'list'] },
],
},
]
2
3
4
5
6
7
8
9
10
11
12
13
像這樣,透過 children 與陣列的方式,我們可以同時指定 /users 、 /users/list 以及 /people 所渲染的畫面結果是同樣的。
如果要在別名路由加上參數,則是要記得在 path 與 alias 保持一致:
const routes = [
{
path: '/users/:id',
component: UsersByIdLayout,
children: [
// this will render the UserDetails for these 3 URLs
// - /users/24
// - /users/24/profile
// - /24
{ path: 'profile', component: UserDetails, alias: ['/:id', ''] },
],
},
]
2
3
4
5
6
7
8
9
10
11
12
13
像這樣,當 URL 為 /users/24 、 /users/24/profile 以及 /24 就都會渲染相同結果了。
# 路由與 Props
由於 Vue Router 的路由與元件緊密耦合,對元件來說就無法彈性地重複使用,像是:
// component
const User = {
template: '<div>User {{ $route.params.id }}</div>'
}
// routes
const routes = [{ path: '/user/:id', component: User }]
2
3
4
5
6
7
像上面這個 User 元件,由於直接在模板內使用 $route.params.id,
這表示若是元件脫離了 Vue Router 就無法單獨使用,這就失去了我們希望抽離元件希望達到高度重複使用的目的。
而 Vue Router 允許我們將 $route.params 視為 Props 來使用:
// component
const User = {
props: ['id'],
template: '<div>User {{ id }}</div>'
}
// routes
const routes = [{ path: '/user/:id', component: User, props: true }]
2
3
4
5
6
7
8
像這樣,我們只需要在 routes 加上 props 屬性,並設定為 true,
這樣就可以將 params 的 :id 當作 Props 傳入元件來使用,模板內容也會更加簡潔。
另外,如果是前面曾經提到過的具名路由,則需要逐一指定是否開啟 props 的設定:
const routes = [
{
path: '/user/:id',
components: {
default: User,
sidebar: Sidebar
},
props: {
default: true,
sidebar: false
}
}
]
2
3
4
5
6
7
8
9
10
11
12
13
像這樣,元件 User 被指定開啟 props 傳遞,而元件 Sidebar 則是不傳遞 props。
小提醒: props 的其他形態
Props 屬性除了前面介紹的 Boolean 之外,也可以是純物件形式:
// component
const Promotion = {
props: ['newsletterPopup'],
template: `...`
}
// routes
const routes = [
{
path: '/promotion/from-newsletter',
component: Promotion,
props: { newsletterPopup: false }
}
]
2
3
4
5
6
7
8
9
10
11
12
13
14
像這樣,就會將 newsletterPopup 當作一般的 props 傳遞到 Promotion 元件中。
同時,Props 也可以寫成 function 的樣式:
const routes = [
{
path: '/search',
component: SearchUser,
props: route => ({ query: route.query.q })
}
]
2
3
4
5
6
7
此時,若我們的 URL 為 /search?q=vue ,則會傳遞一個 { query: 'vue' } 的 props 給 SearchUser 元件。
# 非同步載入元件
如同第三章介紹的,當我們開發進行到最終階段時,可能會透過如 webpack 等工具來進行打包, 如果我們能將不同的元件在打包時分開成為獨立的模組,並且在需要的時候才將它們載入,像是進入指定的路由才去讀取, 這時我們就可以利用 Vue Router 所內建的功能:
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
// 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/About.vue')
}
]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
我們只需要在 component 屬性透過 import 語法來引入到路由中,
這樣一來無論是打包或者是網站實際執行時,都可以獲得類似 lazy-loading 也就是需要的時候才去載入的效果,
當網站的規模越大,對於整體的效能就會有越大的幫助。