本文 AI 產出,尚未審核

Vue 3 基礎概念:Vue 3 專案結構

簡介

在學習 Vue 3 時,了解專案的目錄結構是最先要跨過的門檻。良好的檔案組織不只讓程式碼更易讀,也能在團隊合作、功能擴充與維護時降低衝突與錯誤的機會。
Vue CLI、Vite、Nuxt 等工具各自提供預設的模板,但這些模板僅是起點,真正的專案結構應該根據 業務需求團隊規模開發流程 進行調整。

本文將從 Vue 3 標準專案 的目錄說明開始,逐層解析每個資料夾的職責,並提供實作範例與最佳實踐,協助你快速建立可維護、可擴充的 Vue 應用程式。


核心概念

1. 預設的 Vue 3 專案根目錄

以下圖示以 Vite 建立的 Vue 3 專案為例(npm init vite@latest my-vue-app --template vue):

my-vue-app/
│
├─ public/                # 靜態資源,直接映射至根路徑
│   └─ favicon.svg
│
├─ src/                   # 核心程式碼
│   ├─ assets/            # 圖片、樣式等資源
│   ├─ components/        # 可重用的 UI 元件
│   ├─ composables/       # Vue 3 Composition API 的可抽取邏輯
│   ├─ router/            # Vue Router 設定
│   ├─ store/             # Pinia(或 Vuex)狀態管理
│   ├─ views/             # 路由對應的頁面 (Page Component)
│   ├─ App.vue            # 根組件
│   └─ main.js            # 入口檔案
│
├─ index.html             # HTML 模板
├─ vite.config.js         # Vite 設定
└─ package.json

重點src 內的每個子目錄皆代表一個職責領域,保持 單一職責原則(SRP)是避免程式碼膨脹的關鍵。


2. src/components vs src/views

  • components可重用、不直接對應路由的 UI 單元,例如按鈕、表格、對話框。
  • views頁面級別的組件,每個檔案通常會對應一條路由,例如 HomeView.vueUserProfile.vue

範例:建立一個可重用的 BaseButton.vue

// src/components/BaseButton.vue
<template>
  <button
    :class="['base-button', variant]"
    @click="$emit('click', $event)"
  >
    <slot />
  </button>
</template>

<script setup>
defineProps({
  /** 按鈕樣式變體,可為 'primary'、'secondary'、'danger' */
  variant: {
    type: String,
    default: 'primary'
  }
});
</script>

<style scoped>
.base-button {
  padding: 0.5rem 1rem;
  border: none;
  cursor: pointer;
}
.primary   { background:#42b983; color:#fff; }
.secondary { background:#ccc;   color:#333; }
.danger    { background:#e74c3c; color:#fff; }
</style>

使用方式:

<!-- src/views/HomeView.vue -->
<template>
  <BaseButton variant="danger" @click="handleDelete">
    刪除資料
  </BaseButton>
</template>

<script setup>
import BaseButton from '@/components/BaseButton.vue';
function handleDelete() {
  console.log('執行刪除');
}
</script>

3. Composition API 與 src/composables

Composition API 鼓勵將 邏輯抽離成可重用的函式(俗稱 composable)。將這類函式統一放在 composables 目錄,能讓專案結構更清晰。

範例:useFetch.js – 取得遠端資料的通用函式

// src/composables/useFetch.js
import { ref, onMounted } from 'vue';
import axios from 'axios';

/**
 * @param {string} url - 要請求的 API 位址
 * @returns {object} data、error、loading 三個 reactive 變數
 */
export function useFetch(url) {
  const data = ref(null);
  const error = ref(null);
  const loading = ref(true);

  const fetchData = async () => {
    loading.value = true;
    try {
      const response = await axios.get(url);
      data.value = response.data;
    } catch (e) {
      error.value = e;
    } finally {
      loading.value = false;
    }
  };

  onMounted(fetchData);

  return { data, error, loading, refetch: fetchData };
}

在組件中使用:

<!-- src/views/PostsView.vue -->
<template>
  <div v-if="loading">載入中…</div>
  <ul v-else-if="posts">
    <li v-for="post in posts" :key="post.id">{{ post.title }}</li>
  </ul>
  <div v-else>發生錯誤:{{ error.message }}</div>
</template>

<script setup>
import { useFetch } from '@/composables/useFetch';
const { data: posts, error, loading } = useFetch('https://jsonplaceholder.typicode.com/posts');
</script>

4. 路由設定 (src/router)

Vue Router 4(配合 Vue 3)建議使用 模組化路由,每個功能區塊可以自行管理自己的子路由。

範例:src/router/index.js

// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router';
import HomeView from '@/views/HomeView.vue';
import UserLayout from '@/views/users/UserLayout.vue';
import UserList from '@/views/users/UserList.vue';
import UserDetail from '@/views/users/UserDetail.vue';

const routes = [
  { path: '/', name: 'Home', component: HomeView },

  // 使用巢狀路由管理使用者相關頁面
  {
    path: '/users',
    component: UserLayout,
    children: [
      { path: '', name: 'UserList', component: UserList },
      { path: ':id', name: 'UserDetail', component: UserDetail, props: true }
    ]
  },

  // 捕獲 404
  { path: '/:pathMatch(.*)*', redirect: '/' }
];

export default createRouter({
  history: createWebHistory(),
  routes,
});

技巧@/ 為 Vite 預設的別名,指向 src/,可避免相對路徑過長。


5. 狀態管理 (src/store)

Vue 3 官方推薦使用 Pinia,它的 API 與 Vue 的響應式系統天然相容。以下示範一個簡易的 user store。

// src/store/user.js
import { defineStore } from 'pinia';
import { ref } from 'vue';
import axios from 'axios';

export const useUserStore = defineStore('user', () => {
  const user = ref(null);
  const token = ref(localStorage.getItem('token') || '');

  const login = async (credentials) => {
    const { data } = await axios.post('/api/login', credentials);
    token.value = data.token;
    localStorage.setItem('token', data.token);
    await fetchProfile();
  };

  const fetchProfile = async () => {
    if (!token.value) return;
    const { data } = await axios.get('/api/me', {
      headers: { Authorization: `Bearer ${token.value}` }
    });
    user.value = data;
  };

  const logout = () => {
    token.value = '';
    user.value = null;
    localStorage.removeItem('token');
  };

  return { user, token, login, fetchProfile, logout };
});

在根組件掛載 Pinia:

// src/main.js
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue';
import router from '@/router';

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

6. 靜態資源 (public vs src/assets)

  • public/:放置 不會被 Webpack/Vite 處理 的檔案,例如 robots.txtfavicon.ico。直接以根路徑引用(/favicon.ico)。
  • src/assets/:會經過編譯、哈希命名,適合放置 需要被模組化引用 的圖片、字型或 SCSS。

範例:在組件中引用 src/assets/logo.png

<template>
  <img :src="logoUrl" alt="Logo" />
</template>

<script setup>
import logoUrl from '@/assets/logo.png';
</script>

常見陷阱與最佳實踐

陷阱 說明 解決方案
檔案命名不一致 大寫/小寫混雜、使用中文會導致跨平台衝突 統一使用 kebab-case(如 user-profile.vue)或 PascalCase(如 UserProfile.vue
過度扁平化目錄 所有元件堆在 components/,難以找尋 依功能模組化子資料夾,例如 components/forms/, components/layout/
把所有邏輯寫在單一組件 造成巨型 .vue 檔,維護成本高 把可重用邏輯抽成 composable,UI 抽成 BaseCommon 組件
直接在組件裡 import store(未使用 setup 會失去 tree‑shaking 效益 使用 PiniadefineStore + setup 方式,或在 main.js 只掛載一次
忘記在 vite.config.js 設定別名 路徑寫成 ../../../,易出錯 vite.config.js 加入 resolve.alias,統一使用 @/
public 放大量圖片 無法利用 hash 版本管理,導致快取問題 大型或會變動的圖片放在 src/assets,讓 Vite 處理哈希

最佳實踐

  1. 單一職責:每個資料夾只負責一類型的資源(元件、路由、狀態、共用函式)。
  2. 命名規則:檔案與資料夾使用一致的大小寫風格;組件檔名與匯出名稱保持相同。
  3. 模組化路由:大型專案把路由分割成多個檔案(例如 router/modules/user.js),再在 router/index.js 合併。
  4. 使用 TypeScript(若團隊允許):在 src 內使用 .ts.tsx.vue 結合,提升 IDE 補全與型別安全。
  5. 自動化檢查:加入 ESLint + Prettier,確保程式碼風格一致,避免因格式問題產生的 merge 衝突。

實際應用場景

1. 電子商務平台(大型專案)

  • 目錄
    src/
    ├─ components/
    │   ├─ product/
    │   ├─ cart/
    │   └─ ui/
    ├─ composables/
    │   ├─ useCart.js
    │   └─ usePayment.js
    ├─ router/
    │   ├─ index.js
    │   └─ modules/
    │       ├─ product.js
    │       └─ checkout.js
    ├─ store/
    │   ├─ cart.js
    │   └─ user.js
    └─ views/
        ├─ HomeView.vue
        ├─ ProductList.vue
        └─ Checkout.vue
    
  • 好處:商品相關 UI、商店車、結帳流程各自獨立,開發團隊可同時在 components/productcomposables/useCart 上平行作業,減少相互依賴。

2. 內部管理系統(中小型)

  • 目錄
    src/
    ├─ components/
    │   ├─ BaseButton.vue
    │   └─ Modal.vue
    ├─ composables/
    │   └─ useFetch.js
    ├─ router/
    │   └─ index.js
    ├─ store/
    │   └─ auth.js
    └─ views/
        ├─ Dashboard.vue
        └─ Settings.vue
    
  • 好處:只需少量共用元件與簡單的 useFetch,結構保持輕量,快速上線且易於維護。

3. 行動端單頁應用(SPA)

  • 使用 Vite + Vue Router懶載入(code‑splitting):
// src/router/index.js
const routes = [
  {
    path: '/',
    component: () => import('@/views/HomeView.vue')
  },
  {
    path: '/profile',
    component: () => import('@/views/ProfileView.vue')
  }
];
  • 透過 路由分段(route level code‑splitting)減少首次載入大小,提升行動裝置的使用體驗。

總結

Vue 3 的專案結構不只是檔案放哪裡那麼簡單,它直接影響 開發效率、維護成本與團隊協作

  • src/ 為核心程式碼,依 components / views / composables / router / store 分層管理。
  • Composition API + composables 讓邏輯抽離、可重用;Pinia 則提供簡潔且類型安全的狀態管理。
  • 透過 模組化路由統一別名嚴謹命名規則,可避免常見的陷阱。

掌握這套結構後,你可以輕鬆擴展功能、快速定位問題,從小型個人專案一路成長到企業級應用。祝你在 Vue 3 的旅程中,寫出乾淨、可維護且具備良好擴充性的程式碼!