Appearance
AI 協作:路由設定交給 AI
本章的技術主線在上一節已經完成,這一節是可選的協作實例,跳過不影響後續章節。路由表有大量規律性的 path、name 與 component 對應,很適合讓 AI 先搭骨架;但只要一個子路徑多了 /、一個頁面忘了 lazy load,產物就會「看起來完整,實際上不符合需求」。
這次不再修改書店 canonical,而是用一個獨立的部落格案例練習,避免兩套路由表混在一起。
AI 協作:用 vue skill 加速
情境:部落格需要首頁、文章列表、文章內頁、關於與 404。文章內頁是文章列表的巢狀子路由;App 要有首頁、文章與關於導覽。案例不包含登入守衛。
做法:在安裝好 vue-skills 的 AI 工具中(安裝與觸發方式見第一章),下指令 use vue-router-best-practices,再給清楚的檔案與驗收需求:
請產生一個 Vue 3 部落格的路由與導覽。頁面有首頁、文章列表、文章內頁、關於、404;文章內頁是
/posts/:slug,放在文章列表 route 的 children;每筆路由都要有唯一 name,所有 view 使用 route-level lazy loading,404 用 catch-all;提供src/router/routes.js、src/router/index.js、src/main.js、src/App.vue與全部 views。主線使用<script setup>,產出 JavaScript,不使用 TypeScript,不加入 navigation guards。
AI 產出(節錄):整體結構很接近,但留下兩個常見問題:
js
import AboutView from '../views/AboutView.vue' // ❌ 靜態載入
export const routes = [
{
path: '/posts',
component: () => import('../views/PostListView.vue'),
children: [
{
path: '/:slug', // ❌ 以 / 開頭,變成根路徑
component: () => import('../views/PostDetailView.vue'),
},
],
},
{ path: '/about', component: AboutView },
]人工驗證重點:
- 搜尋 route 設定裡的
.vue,每一個都應出現在() => import()裡;不要因首頁或 About 很小,就默默改回 eager import。 - 巢狀子
path應是:slug,不能以/開頭;實際輸入/posts/hello-vue,確認是 PostListView 的內層<RouterView>顯示 PostDetailView。 - 每一筆可導航路由都有唯一
name,App 的 RouterLink 名稱與 route record 完全一致。 - 直接輸入不存在的深層網址,應由
/:pathMatch(.*)*接住;404 必須放在一般路由之後。 - 最後不只看路由表:逐一確認 routes 引用的五個 view 都真的存在,再跑 build。AI 很容易產生 import,卻漏掉其中一個檔案。
修正後的完整範例如下。這一組刻意與書店案例分開,聚焦巢狀路由、lazy loading、命名一致與 404。
修正後的完整範例(部落格)
src/router/routes.js:
js
export const routes = [
{
path: '/',
name: 'home',
component: () => import('../views/HomeView.vue'),
},
{
path: '/posts',
name: 'posts',
component: () => import('../views/PostListView.vue'),
children: [
{
path: ':slug',
name: 'post',
component: () => import('../views/PostDetailView.vue'),
props: true,
},
],
},
{
path: '/about',
name: 'about',
component: () => import('../views/AboutView.vue'),
},
{
path: '/:pathMatch(.*)*',
name: 'not-found',
component: () => import('../views/NotFoundView.vue'),
},
]src/router/index.js:
js
import { createRouter, createWebHistory } from 'vue-router'
import { routes } from './routes.js'
const router = createRouter({
history: createWebHistory(),
routes,
})
export default routersrc/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>
<div class="blog-shell">
<header>
<nav class="blog-nav" aria-label="主要導覽">
<RouterLink :to="{ name: 'home' }">首頁</RouterLink>
<RouterLink :to="{ name: 'posts' }">文章</RouterLink>
<RouterLink :to="{ name: 'about' }">關於</RouterLink>
</nav>
</header>
<main class="blog-main">
<RouterView />
</main>
</div>
</template>
<style scoped>
.blog-shell {
max-width: 44rem;
margin-inline: auto;
padding: 1.5rem;
font-family: system-ui, sans-serif;
}
.blog-nav {
display: flex;
gap: 1rem;
}
.blog-main {
padding-block: 2rem;
}
.router-link-active {
color: #7c3aed;
font-weight: 700;
}
</style>src/views/HomeView.vue:
vue
<template>
<section>
<h1>Vue 手記</h1>
<p>記錄學習 Vue 與前端工程的過程。</p>
</section>
</template>src/views/PostListView.vue:
vue
<script setup>
const posts = [
{ slug: 'hello-vue', title: '重新認識 Vue' },
{ slug: 'router-mindset', title: '讓 URL 成為狀態' },
]
</script>
<template>
<section>
<h1>文章列表</h1>
<ul>
<li v-for="post in posts" :key="post.slug">
<RouterLink :to="{ name: 'post', params: { slug: post.slug } }">
{{ post.title }}
</RouterLink>
</li>
</ul>
<RouterView />
</section>
</template>src/views/PostDetailView.vue:
vue
<script setup>
defineProps({
slug: { type: String, required: true },
})
</script>
<template>
<article>
<h2>文章:{{ slug }}</h2>
<p>這是教學用的最小文章內容。</p>
</article>
</template>src/views/AboutView.vue:
vue
<template>
<section>
<h1>關於本站</h1>
<p>這是一個用 Vue Router 建立的部落格範例。</p>
</section>
</template>src/views/NotFoundView.vue:
vue
<template>
<section>
<h1>找不到頁面</h1>
<RouterLink :to="{ name: 'home' }">回首頁</RouterLink>
</section>
</template>到這裡,我們已經能把應用程式拆成可分享的頁面、讓資料跟著 route params 更新,也能在導航前做決策。下一章會加入 Pinia,讓登入資訊與跨頁面狀態不再由暫時的模組層級 ref 承擔。
