Skip to content

AI 協作:路由設定交給 AI

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

本章的技術主線在上一節已經完成,這一節是可選的協作實例,跳過不影響後續章節。路由表有大量規律性的 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.jssrc/router/index.jssrc/main.jssrc/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 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>
  <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 承擔。