使用 Vue 3 和 Express 實作內容管理系統(三):實作前端路由

前言

本文是前端工作坊的教學文件,介紹如何使用 Vue 3 和 Express 實作內容管理系統,並搭配 Firebase 實現持久化和認證。

開啟專案

開啟前端專案。

1
2
cd simple-cms-ui
code .

建立佈局

修改 App.vue 檔。

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
<script setup>
import HelloWorld from './components/HelloWorld.vue';
</script>

<template>
<nav class="navbar navbar-dark bg-dark fixed-top">
<div class="container-fluid">
<a
class="navbar-brand"
href="/"
>
Simple CMS
</a>
<button
class="navbar-toggler"
type="button"
data-bs-toggle="offcanvas"
data-bs-target="#offcanvasDarkNavbar"
aria-controls="offcanvasDarkNavbar"
aria-label="Toggle navigation"
>
<span class="navbar-toggler-icon" />
</button>
<div
id="offcanvasDarkNavbar"
class="offcanvas offcanvas-end text-bg-dark"
tabindex="-1"
aria-labelledby="offcanvasDarkNavbarLabel"
>
<div class="offcanvas-header">
<h5
id="offcanvasDarkNavbarLabel"
class="offcanvas-title"
>
Simple CMS
</h5>
<button
type="button"
class="btn-close btn-close-white"
data-bs-dismiss="offcanvas"
aria-label="Close"
/>
</div>
<div class="offcanvas-body">
<ul class="navbar-nav justify-content-end flex-grow-1 pe-3">
<li class="nav-item">
<a
class="nav-link active"
href="/"
>
Home
</a>
</li>
<li class="nav-item">
<a
class="nav-link"
href="/about"
>
About
</a>
</li>
</ul>
</div>
</div>
</div>
</nav>
<main class="container py-5">
<HelloWorld msg="Hello, World!" />
</main>
<footer class="footer py-2 bg-dark fixed-bottom text-center">
<span class="text-light">
© {{ new Date().getFullYear() }} Simple CMS
</span>
</footer>
</template>

<style lang="scss" scoped>
main {
margin-top: 56px;
margin-bottom: 40px;
}
</style>

重構佈局

建立 AppHeader.vue 檔,將相關程式碼移動到 AppHeader 元件。

1
2
3
<template>
<!-- ... -->
</template>

建立 AppFooter.vue 檔,將相關程式碼移動到 AppFooter 元件。

1
2
3
<template>
<!-- ... -->
</template>

修改 App.vue 檔,引入元件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<script setup>
import AppFooter from './components/AppFooter.vue';
import AppHeader from './components/AppHeader.vue';
import HelloWorld from './components/HelloWorld.vue';
</script>

<template>
<AppHeader />
<main class="container py-5">
<HelloWorld msg="Hello, World!" />
</main>
<AppFooter />
</template>

<style lang="scss" scoped>
main {
margin-top: 56px;
margin-bottom: 40px;
}
</style>

提交修改。

1
2
3
git add .
git commit -m "Add header and footer components"
git push

實作路由

Ref: https://router.vuejs.org/

認識 Vue Router 套件

客戶端路由的作用是在單頁應用程式(SPA)中將瀏覽器的 URL 和使用者看到的內容綁定起來。當使用者在應用程式中瀏覽不同頁面時,URL 會隨之更新,但頁面不需要從伺服器重新載入。

Vue Router 基於 Vue 的元件系統構建,開發者可以透過設定路由來告訴 Vue Router 為每個 URL 路徑顯示哪些元件。

Hash 模式

Hash 模式使用 createWebHashHistory() 創造:

1
2
3
4
5
6
7
8
import { createRouter, createWebHashHistory } from 'vue-router'

const router = createRouter({
history: createWebHashHistory(),
routes: [
//...
],
})

它在內部傳遞的實際 URL 之前使用了一個哈希字元(#),URL 看起來會有點特殊,例如:http://localhost:5173/#/about。由於這部分 URL 從未被傳送到伺服器,所以它不需要在伺服器層面上進行任何特殊處理。不過,它在 SEO 中可能會有不好的影響。

HTML5 模式

HTML5 模式使用 createWebHistory() 創造:

1
2
3
4
5
6
7
8
import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
history: createWebHistory(),
routes: [
//...
],
})

當使用這種歷史模式時,URL 會看起來很「正常」,例如:http://localhost:5173/about

不過,由於此應用程式是一個單頁的客戶端應用,如果沒有適當的伺服器配置,使用者在瀏覽器中直接訪問,就會得到一個 404 錯誤。

要解決這個問題,需要在伺服器上添加一個簡單的回退路由。如果 URL 不符合任何靜態資源,它應提供與應用程式中的 index.html 相同的頁面。

建立路由

安裝依賴套件。

1
npm install vue-router@4

建立 src/router 資料夾。

1
mkdir src/router

src/router 資料夾,建立 index.js 檔,在這裡定義所有的路由與對應的頁面。因為是後台系統,不需要考慮對 SEO 的影響,因此這裡使用 Hash 模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import HomeView from '../views/HomeView.vue';
import { createRouter, createWebHashHistory } from 'vue-router';

const router = createRouter({
history: createWebHashHistory(),
routes: [
{
path: '/',
name: 'home',
component: HomeView,
},
{
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('../views/AboutView.vue'),
},
],
});

export default router;

建立 src/views 資料夾。

1
mkdir src/views

src/views 資料夾,建立 HomeView.vue 檔,作為「首頁」頁面。

1
2
3
4
5
6
7
<script setup>
//
</script>

<template>
Home
</template>

src/views 資料夾,建立 AboutView.vue 檔,作為「關於」頁面。

1
2
3
4
5
6
7
<script setup>
//
</script>

<template>
About
</template>

修改 main.js 檔,引入路由定義檔。

1
2
3
4
5
6
7
8
9
10
11
import { Popover } from 'bootstrap';
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
import './style.scss';

createApp(App)
.use(router)
.mount('#app');

// ...

修改 App.vue 檔,將 HelloWorld 元件替換為 RouterView 元件,來讓 Vue Router 渲染對應 URL 所指定的頁面。

1
2
3
4
5
6
7
8
9
10
11
<!-- ... -->

<template>
<AppHeader />
<main class="container py-5">
<RouterView />
</main>
<AppFooter />
</template>

<!-- ... -->

修改 AppHeader.vue 檔,將常規的 <a> 標籤,替換為 RouterLink 元件,來讓 Vue Router 能夠在不重新載入頁面的情況下改變 URL。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!-- ... -->

<ul class="navbar-nav justify-content-end flex-grow-1 pe-3">
<li class="nav-item">
<RouterLink
class="nav-link active"
:to="{ name: 'home' }"
>
Home
</RouterLink>
</li>
<li class="nav-item">
<RouterLink
class="nav-link"
:to="{ name: 'about' }"
>
About
</RouterLink>
</li>
</ul>

<!-- ... -->

提交修改。

1
2
3
git add .
git commit -m "Add vue router"
git push

練習

練習一:將側邊欄的連結改成迴圈

讓側邊欄的連結改成迴圈的寫法,可以讓程式碼變得更簡潔,更容易維護。

做法

修改 AppHeader.vue 檔。

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<script setup>
const links = [
{
title: 'Home',
name: 'home',
},
{
title: 'About',
name: 'about',
},
];
</script>

<template>
<nav class="navbar navbar-dark bg-dark fixed-top">
<div class="container-fluid">
<!-- ... -->
<div
id="offcanvasDarkNavbar"
class="offcanvas offcanvas-end text-bg-dark"
tabindex="-1"
aria-labelledby="offcanvasDarkNavbarLabel"
>
<!-- ... -->
<div class="offcanvas-body">
<ul class="navbar-nav justify-content-end flex-grow-1 pe-3">
<template
v-for="(link, i) in links"
:key="i"
>
<li
class="nav-item"
>
<router-link
class="nav-link"
:to="{
name: link.name,
}"
>
{{ link.title }}
</router-link>
</li>
</template>
</ul>
</div>
</div>
</div>
</nav>
</template>

練習二:切換 active 樣式

當切換路由時,超連結的 active 也要跟著切換。

做法

修改 AppHeader.vue 檔。

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
<script setup>
import { useRoute } from 'vue-router';

const route = useRoute();

const links = [
{
title: 'Home',
name: 'home',
},
{
title: 'About',
name: 'about',
},
];
</script>

<template>
<nav class="navbar navbar-dark bg-dark fixed-top">
<div class="container-fluid">
<!-- ... -->
<div
id="offcanvasDarkNavbar"
class="offcanvas offcanvas-end text-bg-dark"
tabindex="-1"
aria-labelledby="offcanvasDarkNavbarLabel"
>
<!-- ... -->
<div class="offcanvas-body">
<ul class="navbar-nav justify-content-end flex-grow-1 pe-3">
<template
v-for="(link, i) in links"
:key="i"
>
<li
class="nav-item"
>
<router-link
class="nav-link"
:class="{
'active': link.name === route.name,
}"
:to="{
name: link.name,
}"
>
{{ link.title }}
</router-link>
</li>
</template>
</ul>
</div>
</div>
</div>
</nav>
</template>

提交修改。

1
2
3
git add .
git commit -m "Update links"
git push

程式碼