iT邦幫忙

2025 iThome 鐵人賽

DAY 18
0
Vue.js

邊學邊做:Vue.js 實戰養成計畫系列 第 18

Day 18:多重軌道 — 動態路由與巢狀路由

  • 分享至 

  • xImage
  •  

1) 動態路由是什麼?

路徑帶變數的路由,例如:/planet/:slug

  • :slug 會被解析成參數(e.g. mars)。
  • 適合「多個同類型頁面」:星球、文章、商品…
  • 可用 props: trueprops: route => ({ slug: route.params.slug }) 把參數當 props 傳給元件(較好測試、較好型別化)。

2) 巢狀路由是什麼?

  • 在父頁面內,再切換子頁面。
  • 例如:/planet/mars/overview/planet/mars/moons/planet/mars/research
    父頁面放 <RouterView /> 當「內層跳躍門」。

3) 什麼時候用?

動態路由:資料集很多且同樣模板(每顆星球一頁)。
巢狀路由:同一顆星球裡有多個分頁(總覽/衛星/研究)。

在我們昨天的專案上實作

A. 擴充路由:src/router/index.js
在 Day 17 的基礎上,給 planet 路由加入 children:

import { createRouter, createWebHistory } from 'vue-router'
import Home from '../views/Home.vue'
import Planet from '../views/Planet.vue'

// 子頁面
import PlanetOverview from '../views/planet/Overview.vue'
import PlanetMoons from '../views/planet/Moons.vue'
import PlanetResearch from '../views/planet/Research.vue'
import NotFound from '../views/NotFound.vue'

const router = createRouter({
  history: createWebHistory(),
  routes: [
    { path: '/', name: 'home', component: Home },

   {
  path: '/planet/:slug',
  name: 'planet',  
  component: Planet,
  props: route => ({ slug: route.params.slug }),
  children: [
    { path: '', redirect: { name: 'planet-overview' } },
    { path: 'overview', name: 'planet-overview', component: PlanetOverview, props: true },
    { path: 'moons',    name: 'planet-moons',    component: PlanetMoons,    props: true },
    { path: 'research', name: 'planet-research', component: PlanetResearch, props: true },
  ]
},

    { path: '/:pathMatch(.*)*', name: '404', component: NotFound }
  ],
  scrollBehavior: () => ({ top: 0 })
})

export default router

觀念重點:/planet/:slug 是父路由,底下的 overview / moons / research 是 子路由。URL 會變成 /planet/mars/moons 這種「多層級」路徑。

B. 父頁面要放內層 <RouterView />src/views/Planet.vue
在 Day 17 的 Planet.vue 補上「內頁分頁 + 子視圖」:

<template>
  <!-- 有資料就顯示星球頁 -->
  <main class="wrap" v-if="planet">
    <RouterLink class="back" :to="{ name: 'home' }">← 返回星際地圖</RouterLink>

    <header class="hero">
      <div class="emoji">{{ planet.emoji }}</div>
      <div>
        <h1>{{ planet.name }}</h1>
        <p class="summary">{{ planet.summary }}</p>
      </div>
    </header>

    <!-- 分頁導航 -->
    <nav class="tabs">
      <RouterLink :to="{ name: 'planet-overview', params: { slug } }">總覽</RouterLink>
      <RouterLink :to="{ name: 'planet-moons',    params: { slug } }">衛星</RouterLink>
      <RouterLink :to="{ name: 'planet-research', params: { slug } }">研究</RouterLink>
    </nav>

    <!-- 巢狀子頁:若沒匹配到任何子頁,出現友善訊息 -->
    <RouterView v-slot="{ Component }">
      <component :is="Component" v-if="Component" :slug="slug" />
      <p v-else class="hint">請從上方分頁選擇一個子頁面開始探索。</p>
    </RouterView>

    <!--(可選)偵錯:看到目前 slug 與是否有子頁 -->
    <!-- <pre style="opacity:.5">slug={{ slug }}</pre> -->
  </main>

  <!-- 找不到星球資料 -->
  <main v-else class="wrap">
    <h1>找不到這顆星球</h1>
    <p>請返回 <RouterLink :to="{ name: 'home' }">星際地圖</RouterLink></p>
  </main>
</template>

<script setup>
import { toRef, computed } from 'vue'
import { getPlanetBySlug } from '../data/planets'

const props = defineProps({ slug: String })
const slug = toRef(props, 'slug')

// 若 slug 找不到,planet 會是 undefined,會走到 v-else 分支
const planet = computed(() => getPlanetBySlug(slug.value))
</script>

<style scoped>
.wrap { max-width: 860px; margin: 40px auto; padding: 0 16px; }
.hero { display:flex; gap:16px; align-items:center; margin:12px 0 16px; }
.hero .emoji { font-size:56px; }
.summary { color:#475569; }
.tabs { display:flex; gap:12px; margin: 16px 0 24px; }
.tabs a { text-decoration:none; padding:6px 10px; border-radius:8px; border:1px solid #e2e8f0; color:#0f172a; }
.tabs a.router-link-active { background:#0f172a; color:#fff; border-color:#0f172a; }
.hint { color:#64748b; }
</style>

C. 建立子頁面

  1. 總覽src/views/planet/Overview.vue
<template>
  <section>
    <h2>🛰️ 星球總覽</h2>
    <ul>
      <li>距離太陽:{{ planet.distance }}</li>
      <li>表面重力:{{ planet.gravity }}</li>
      <li>可居住性:{{ planet.habitable ? '🟢 候選' : '⚪ 尚不適宜' }}</li>
    </ul>
    <p style="color:#475569; margin-top:8px;">探索建議:{{ planet.tips }}</p>
  </section>
</template>

<script setup>
import { computed } from 'vue'
import { getPlanetBySlug } from '../../data/planets'
const props = defineProps({ slug: String })
const planet = computed(() => getPlanetBySlug(props.slug))
</script>
  1. 衛星src/views/planet/Moons.vue
    (示範每顆星球的衛星數據;沒有資料就顯示「無主要衛星」)
<template>
  <section>
    <h2>🌙 主要衛星</h2>
    <ul v-if="moons.length">
      <li v-for="m in moons" :key="m.name">
        {{ m.name }} — {{ m.note }}
      </li>
    </ul>
    <p v-else>此星球暫無主要衛星或未收錄。</p>
  </section>
</template>

<script setup>
const props = defineProps({ slug: String })

// 簡單示意資料
const db = {
  earth: [{ name: '月球', note: '潮汐與自轉穩定器' }],
  mars: [
    { name: '火衛一 Phobos', note: '近軌小衛星' },
    { name: '火衛二 Deimos', note: '遠軌小衛星' }
  ],
  jupiter: [
    { name: '木衛一 Io', note: '火山活動活躍' },
    { name: '木衛二 Europa', note: '冰殼下的海洋' },
    { name: '木衛三 Ganymede', note: '太陽系最大衛星' },
    { name: '木衛四 Callisto', note: '布滿隕石坑' }
  ]
}
const moons = db[props.slug] || []
</script>
  1. 研究src/views/planet/Research.vue
    (示範巢狀頁面裡的互動)
<template>
  <section>
    <h2>🔬 研究任務</h2>
    <p>填寫要在 {{ slug }} 進行的研究重點:</p>
    <form @submit.prevent="add">
      <input v-model.trim="draft" placeholder="例如:取樣分析 / 冰層鑽探" />
      <button>加入</button>
    </form>
    <ul>
      <li v-for="(t,i) in tasks" :key="i">{{ t }}</li>
    </ul>
  </section>
</template>

<script setup>
import { ref } from 'vue'
const props = defineProps({ slug: String })

const slug = props.slug
const draft = ref('')
const tasks = ref([])

function add() {
  if (!draft.value) return
  tasks.value.push(draft.value)
  draft.value = ''
}
</script>

<style scoped>
form { display:flex; gap:8px; margin:10px 0; }
input { flex:1; padding:8px 10px; border:1px solid #e2e8f0; border-radius:8px; }
button { padding:8px 12px; border-radius:8px; border:1px solid #0f172a; background:#0f172a; color:#fff; }
</style>

4) 小技巧與注意

  • props 取參數
    在父/子頁面都用 propsslug,比直接 useRoute() 更好測試,也利於型別與解耦。

  • 參數變更時的反應
    如果同一個元件會在不同參數間切換(例如從 /planet/mars 直接導航到 /planet/jupiter 而不卸載元件),你可以

import { watch } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
watch(() => route.params.slug, (newSlug) => { /* 重新抓資料 */ })

但我們這裡因為用 props + computed,基本需求已足夠。

  • 路由名稱(name
    推薦都給路由命名,在 <RouterLink :to="{ name:'planet-moons', params:{ slug } }">
    比硬寫字串路徑穩定、可重構。

結論

  • 動態路由:同模板多實體(每顆星球一頁)。
  • 巢狀路由:父頁(星球)內的子分頁(總覽/衛星/研究)。
  • Planet.vue 放內層 <RouterView /> 來顯示子頁。
    到這裡,你的「星際航線系統」就能:/planet/:slug/(overview|moons|research) 自由切換啦!

參考資源
https://vuejs.org/guide
https://www.runoob.com/vue3


上一篇
Day 17:星際航路圖 — Vue Router 實作
系列文
邊學邊做:Vue.js 實戰養成計畫18
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言