本文 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.vue、UserProfile.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.txt、favicon.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 抽成 Base 或 Common 組件 |
直接在組件裡 import store(未使用 setup) |
會失去 tree‑shaking 效益 | 使用 Pinia 的 defineStore + setup 方式,或在 main.js 只掛載一次 |
忘記在 vite.config.js 設定別名 |
路徑寫成 ../../../,易出錯 |
在 vite.config.js 加入 resolve.alias,統一使用 @/ |
在 public 放大量圖片 |
無法利用 hash 版本管理,導致快取問題 | 大型或會變動的圖片放在 src/assets,讓 Vite 處理哈希 |
最佳實踐:
- 單一職責:每個資料夾只負責一類型的資源(元件、路由、狀態、共用函式)。
- 命名規則:檔案與資料夾使用一致的大小寫風格;組件檔名與匯出名稱保持相同。
- 模組化路由:大型專案把路由分割成多個檔案(例如
router/modules/user.js),再在router/index.js合併。 - 使用 TypeScript(若團隊允許):在
src內使用.ts、.tsx、.vue結合,提升 IDE 補全與型別安全。 - 自動化檢查:加入 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/product、composables/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 的旅程中,寫出乾淨、可維護且具備良好擴充性的程式碼!