Skip to content

SPA 與前端路由

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

到目前為止,我們做的 Vue 應用都只有一個主要畫面。元件可以切換、資料也能更新,但網址幾乎沒有參與其中。當書店開始有列表、詳情、登入與會員頁,全部塞在同一個 App.vue,不只條件判斷會膨脹,使用者也無法把「某一本書」的網址分享出去。

這一章要讓 URL 正式成為應用程式狀態的一部分。我們會先定義路由,再處理連結、程式化導航、路由資料、lazy loading,最後用導航守衛保護會員頁。

從多頁式網站到 SPA

傳統多頁式應用 (Multi-Page Application, MPA) 每次點連結,瀏覽器會向伺服器要求一份新的 HTML,整頁重新載入。單頁式應用 (Single-Page Application, SPA) 則先載入應用程式殼層;之後 URL 改變時,由前端路由器挑出對應元件,換掉畫面中的一部分。

這裡的「不整頁重載」不是「不跟伺服器溝通」。頁面元件仍然可以呼叫 API 取得資料;差別是 HTML 殼層與 JavaScript 應用不必每次重來。URL 則對應到一個可分享、可加入書籤、可用上一頁返回的畫面。

前端路由與後端路由都會比對 URL,但處理的位置與產物不同:

前端路由後端路由
在哪裡執行瀏覽器中的 JavaScriptWeb 伺服器
對應關係URL → 頁面元件HTTP method + URL → handler
常見結果切換 RouterView 的內容回傳 HTML、JSON 或檔案

例如 Vue Router 把 /books/1 對到 BookDetailView,元件再向後端的 GET /api/books/1 取得資料。前端守衛可以改善操作流程,卻不能取代後端權限檢查;真正的資料存取權仍必須由伺服器驗證。

安裝 Vue Router

若是既有的 Vue 專案,直接安裝官方路由套件:

sh
pnpm add vue-router

若要建立新專案,也可以執行 pnpm create vue@latest,在互動選項勾選 Vue Router。create-vue 的提示可能隨版本調整,請以建立專案當下看到的選項為準;本章沿用第一章的目錄結構,並把路由設定放進 src/router/、頁面元件放進 src/views/

本章以 Vue Router 5.1.0 為基準。Vue Router 5 把 file-based routing 納入套件,但它仍是可選功能;沒有使用 file-based routing 的 v4 設定式路由專案沒有 breaking changes,可以直接沿用本章的 routes 寫法。這一章不展開 file-based routing,先把路由本身的心智建立好。

第一個路由應用

一個最小路由應用有三個接點:路由表把 URL 對到元件,router 實體決定 history 模式,Vue app 用 .use(router) 安裝它。

src/router/routes.js

js
import AboutView from '../views/AboutView.vue'
import HomeView from '../views/HomeView.vue'

export const routes = [
  { path: '/', component: HomeView },
  { path: '/about', component: AboutView },
]

src/router/index.js

js
import { createRouter, createWebHistory } from 'vue-router'
import { routes } from './routes.js'

const router = createRouter({
  history: createWebHistory(),
  routes,
})

export default router

src/main.js

js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router/index.js'

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

src/App.vue

vue
<template>
  <header class="site-header">
    <nav class="site-nav" aria-label="主要導覽">
      <RouterLink to="/">首頁</RouterLink>
      <RouterLink to="/about">關於</RouterLink>
    </nav>
  </header>

  <main class="page">
    <RouterView />
  </main>
</template>

<style scoped>
.site-nav {
  display: flex;
  gap: 1rem;
}

.page {
  padding-block: 2rem;
}
</style>

src/views/HomeView.vue

vue
<template>
  <h1>線上書店</h1>
</template>

src/views/AboutView.vue

vue
<template>
  <h1>關於我們</h1>
</template>

<RouterLink> 改變 URL,Vue Router 比對 routes,最後把 HomeViewAboutView 放進 <RouterView>。點擊兩個連結時,瀏覽器不會整頁重載;但網址、上一頁與下一頁仍照常運作。

History 模式與 Hash 模式

兩種模式都能使用動態路由、巢狀路由與導航守衛,主要取捨在網址形式與部署環境:

考量History 模式Hash 模式
網址https://book.example/abouthttps://book.example/#/about
伺服器收到的路徑/about/#/about 不會送到伺服器
直接開啟或重新整理伺服器必須把找不到的路徑導回 index.html不需要額外的 history fallback
適合情境能設定託管平台、Web server 或 CDN 的正式網站託管環境無法設定 rewrite,或嵌在既有系統中的小型應用

createWebHistory() 產生一般網站慣用的乾淨網址,也是 Vue Router 官方建議與本章採用的預設選擇。它的底層是瀏覽器的 History API:切換路由時改寫網址、留下瀏覽紀錄,但不觸發整頁載入。代價是部署時必須設定 history fallback;否則使用者直接開啟 /about,伺服器找不到這個實體檔案便會回傳 404。開發階段感受不到這件事,因為 Vite dev server 內建了這層 fallback;第一次踩到通常是部署上線之後。完整部署設定留到第十二章。

如果部署環境不能設定 fallback,再選擇 createWebHashHistory()。它把前端路徑放在 # 後方,伺服器始終只需要回傳同一個入口檔案,但網址會多出 hash:

js
import { createRouter, createWebHashHistory } from 'vue-router'

const router = createRouter({
  history: createWebHashHistory(),
  routes,
})

能設定 fallback 就優先用 History;不能設定才用 Hash。 網址的分享與書籤,兩種模式都支援;搜尋引擎能否理解頁面內容還牽涉 SSR、預渲染與 metadata,不是換成 History 模式就自動解決。

我們已經把「網址 → 路由表 → 頁面元件」串起來了。下一節會把書店路由擴成動態路徑、巢狀頁面與 404。