是 Ray 不是 Array

整理這些技術筆記真的很花時間,如果你願意 關閉 Adblock 支持我,我會把這份感謝轉換成更多「踩坑轉避坑」的內容給你!ヽ(・∀・)ノ

Advertisement
2025-12-04 Vue

思考 Vue/Nuxt 專案架構:如何選擇適合團隊的目錄結構?

思考 Vue/Nuxt 專案架構:如何選擇適合團隊的目錄結構?

前言

一個專案的目錄結構其實非常的重要,為什麼呢?對於你管理與維護專案來說,良好的結構可以讓你知道什麼東西該放哪、什麼東西該怎麼找,接著搭配 README.md 文件的說明,就可以有效讓其他開發者快速上手你的專案,這也是變相管制程式碼品質的一種方式。

專案結構

首先,這邊我們先來看一個最基本的 Vue 專案結構,這邊我將會以 npm create vue@latest 建立出來的專案為範例,因此相關資訊如下:

  • create-vue 版本:3.18.3
  • Node.js 版本:22.18.0
  • 包含的功能(這邊全選):
    • TypeScript
    • JSX 支援
    • Router(單頁應用程式開發)
    • Pinia(狀態管理)
    • Vitest(單元測試)
    • 端對端測試
    • ESLint(錯誤預防)
    • Prettier(程式碼格式化)
  • 端對端測試框架:Playwright
  • 試驗特性:Oxlint

接著你應該會看到以下專案結構:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
vue-example/
├── .vscode/ # VSCode 編輯器設定
│ ├── extensions.json
│ └── settings.json
├── e2e/ # E2E 測試(Playwright)
│ ├── tsconfig.json
│ └── vue.spec.ts
├── public/ # 靜態資源
│ └── favicon.ico
├── src/ # 原始碼目錄
│ ├── assets/ # 資源文件
│ │ ├── base.css
│ │ ├── logo.svg
│ │ └── main.css
│ ├── components/ # Vue 元件
│ │ ├── __tests__/ # 元件測試
│ │ │ └── HelloWorld.spec.ts
│ │ ├── icons/ # icons 元件
│ │ │ ├── IconCommunity.vue
│ │ │ ├── IconDocumentation.vue
│ │ │ ├── IconEcosystem.vue
│ │ │ ├── IconSupport.vue
│ │ │ └── IconTooling.vue
│ │ ├── HelloWorld.vue
│ │ ├── TheWelcome.vue
│ │ └── WelcomeItem.vue
│ ├── router/ # Vue Router 路由設定
│ │ └── index.ts
│ ├── stores/ # Pinia 狀態管理
│ │ └── counter.ts
│ ├── views/ # 頁面元件
│ │ ├── AboutView.vue
│ │ └── HomeView.vue
│ ├── App.vue # 根元件
│ └── main.ts # 應用入口
├── .editorconfig # 編輯器設定
├── .gitattributes # Git 屬性設定
├── .gitignore # Git 忽略文件
├── .prettierrc.json # Prettier 格式化設定
├── env.d.ts # TypeScript 環境類型定義
├── eslint.config.ts # ESLint 設定
├── index.html # HTML 入口文件
├── package.json # 專案依賴和腳本
├── playwright.config.ts # Playwright E2E 測試設定
├── README.md # 專案說明文件
├── tsconfig.app.json # TypeScript 設定
├── tsconfig.json # 主 TypeScript 設定
├── tsconfig.node.json # Node.js TypeScript 設定
├── tsconfig.vitest.json # Vitest 測試 TypeScript 設定
├── vite.config.ts # Vite 建構工具設定
└── vitest.config.ts # Vitest 測試設定

但上面的資訊量其實有點過於龐大,所以我將它簡化成以下這樣:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
vue-example/
├── .vscode/ # VSCode 編輯器設定
├── e2e/ # E2E 測試(Playwright)
├── public/ # 靜態資源
├── src/ # 原始碼目錄
│ ├── assets/ # 資源文件
│ ├── components/ # Vue 元件
│ ├── router/ # Vue Router 路由設定
│ ├── stores/ # Pinia 狀態管理
│ ├── views/ # 頁面元件
│ ├── App.vue # 根元件
│ └── main.ts # 應用入口
├── index.html # HTML 入口文件
├── package.json # 專案依賴和腳本
├── README.md # 專案說明文件
├── vite.config.ts # Vite 建構工具設定
└── vitest.config.ts # Vitest 測試設定

這幾個資料夾跟檔案會是你最常接觸也是你一開始就會碰到的,所以接下來讓我們介紹一下 Vue 的專案結構有哪些吧!

扁平化結構(Flat Structure)

這個專案非常適合小型專案,也是前面提到的 Vue 專案結構,主要是將所有東西都放在 src/ 目錄底下,像是元件、路由、狀態管理、頁面元件等等,這樣的好處是可以讓你快速找到你要的東西。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
vue-example/
├── .vscode/ # VSCode 編輯器設定
├── e2e/ # E2E 測試(Playwright)
├── public/ # 靜態資源
├── src/ # 原始碼目錄
│ ├── assets/ # 資源文件
│ ├── components/ # Vue 元件
│ ├── router/ # Vue Router 路由設定
│ ├── stores/ # Pinia 狀態管理
│ ├── views/ # 頁面元件
│ ├── App.vue # 根元件
│ └── main.ts # 應用入口
├── index.html # HTML 入口文件
├── package.json # 專案依賴和腳本
├── README.md # 專案說明文件
├── vite.config.ts # Vite 建構工具設定
└── vitest.config.ts # Vitest 測試設定

但這個結構其實是有一些問題的,隨著專案的成長是會慢慢變的很複雜,像是元件你不知道該放在哪個資料夾、關注點分離也不明顯,接著最麻煩就是要做擴展的時候會很困難,因為你不知道該從哪裡下手。

因此扁平化設計在專案成長到一定程度後,通常會需要重新調整結構,通常會建議用於 小型專案 或是 原型設計階段

原子化結構(Atomic Structure)

如果你是一名前端工程師,我相信你對於「Atomic」這單字應該一點都不陌生,畢竟 Tailwind CSS 就是以原子化的方式來設計的,而原子化結構也是一樣的概念,將專案拆分成更小的單位,讓每個單位都能夠獨立運作,這樣的好處是可以讓你更容易維護專案,因為每個單位都是獨立的,你可以更容易找到你要的東西。

只是 Atomic 會有幾個重點層級:

  • Atoms(原子):最小的單位,像是按鈕、輸入框、標題等等,適合單一功能的元件。
  • Molecules(分子):由多個原子組成的單位,像是搜尋欄、表單等等。
  • Organisms(有機體):由多個分子組成的單位,像是導航欄、頁腳等等。
  • Templates(模板):由多個有機體組成的單位,像是頁面佈局等等。
  • Pages(頁面):最終的頁面,包含所有的模板和內容。

你應該有發現一件事情,如果這件事情拿 Tailwind CSS 來做比喻的話,就是從最小的 class 開始組合成更大的元件,最後再組合成頁面。我們可以透過下表來理解它們的對應關係:

層級 (Level) 定義 Tailwind CSS 類比 程式碼範例
Atoms (原子) 最小的單位,無法再拆分 單一 Utility Class bg-blue-500text-centerp-4
Molecules (分子) 由多個原子組成的單一功能元件 組合 Class 的小型元件 按鈕 (<button class="bg-blue-500 text-white...">)
Organisms (有機體) 由分子與原子組成的複雜區塊 複合式 UI 區塊 卡片 (<div class="card p-4 shadow-lg">...</div>)、導航欄
Templates (模板) 由多個有機體組成的頁面佈局 佈局容器 (Layout Container) <div class="container mx-auto">...</div>
Pages (頁面) 填入真實內容的最終頁面 最終呈現的頁面 <HomePage />

是不是也跟 Sass 7-1 Pattern 很像呢?

那麼基於 Atomic Structure 的 Vue 專案結構會長什麼樣子呢?我們來看一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
vue-example/
├── .vscode/ # VSCode 編輯器設定
├── e2e/ # E2E 測試(Playwright)
├── public/ # 靜態資源
├── src/ # 原始碼目錄
│ ├── assets/ # 資源文件
│ ├── components/ # Vue 元件
│ │ ├── atoms/ # 原子元件
│ │ ├── molecules/ # 分子元件
│ │ ├── organisms/ # 有機體元件
│ │ └── templates/ # 模板元件
│ ├── pages/ # 頁面元件
│ ├── router/ # Vue Router 路由設定
│ ├── stores/ # Pinia 狀態管理
│ ├── App.vue # 根元件
│ └── main.ts # 應用入口
├── index.html # HTML 入口文件
├── package.json # 專案依賴和腳本
├── README.md # 專案說明文件
├── vite.config.ts # Vite 建構工具設定
└── vitest.config.ts # Vitest 測試設定

你會發現 components/ 目錄底下多了四個子目錄,分別是 atoms/molecules/organisms/templates/,這樣結構可以讓你做到更好的關注點分離,畢竟每一個都拆分得非常清楚。

如果這樣不知道該怎麼組合的話,底下我也提供 Atoms 組合成 Pages 的範例給你參考:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Pages(頁面)
└── HomePage.vue
├── Main.vue (Template)
│ ├── Header.vue (Organism)
│ │ ├── Logo.vue (Atom)
│ │ └── NavBar.vue (Molecule)
│ │ ├── NavItem.vue (Atom)
│ │ └── Dropdown.vue (Molecule)
│ ├── Content.vue (Organism)
│ │ ├── ArticleList.vue (Molecule)
│ │ │ ├── ArticleItem.vue (Atom)
│ │ │ └── ReadMoreButton.vue (Atom)
│ │ └── Sidebar.vue (Molecule)
│ │ ├── SearchBox.vue (Atom)
│ │ └── CategoryList.vue (Molecule)
│ │ └── CategoryItem.vue (Atom)
│ └── Footer.vue (Organism)
│ ├── FooterLink.vue (Atom)
│ └── SocialMediaIcons.vue (Molecule)
│ └── SocialMediaIcon.vue (Atom)

你會發現整體在擴展性上是非常高的,但是層級管理上相對複雜很多,所以在前期的規劃負擔重一些,不過這樣的結構非常適合中大型專案使用,尤其是製作 UI Framework 的時候非常適合使用這種結構,畢竟製作一個 UI Framework 通常會需要非常多的原子,接著用這一些原子再組合成 Molecules….以此類推。

因此 UI Framework 也會把 Atomic Structure 底下的各個層級釋出給使用者。

模組化結構(Modular Structure)

模組化結構的專案的核心顧名思義,每個模組都是獨立的單位,並不會因為 A 模組需要 B 模組的東西而把 B 模組的東西放到 A 模組底下,而是透過匯入的方式來使用,你可以大幅減少模組之間的耦合度,讓每個模組都能夠獨立運作,而這也是在替微服務架構做準備。

其中最特別的就是 modules/ 這個資料夾,這個資料夾底下會放置所有的模組,每個模組都是一個獨立的資料夾,裡面會包含該模組所需要的所有東西,像是元件、路由、狀態管理等等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
vue-example/
├── .vscode/ # VSCode 編輯器設定
├── e2e/ # E2E 測試(Playwright)
├── public/ # 靜態資源
├── src/ # 原始碼目錄
│ ├── assets/ # 資源文件
│ ├── modules/ # 模組目錄
│ │ ├── moduleA/ # 模組 A
│ │ │ ├── components/ # 模組 A 的元件
│ │ │ ├── router/ # 模組 A 的路由
│ │ │ │ └── index.ts # 模組 A 的路由入口
│ │ │ ├── stores/ # 模組 A 的狀態管理
│ │ │ └── index.ts # 模組 A 的入口
│ │ └── moduleB/ # 模組 B
│ │ ├── components/ # 模組 B 的元件
│ │ ├── router/ # 模組 B 的路由
│ │ │ └── index.ts # 模組 B 的路由入口
│ │ ├── stores/ # 模組 B 的狀態管理
│ │ └── index.ts # 模組 B 的入口
│ ├── router/ # 全域路由設定
│ ├── stores/ # 全域狀態管理
│ ├── App.vue # 根元件
│ └── main.ts # 應用入口
├── index.html # HTML 入口文件
├── package.json # 專案依賴和腳本
├── README.md # 專案說明文件
├── vite.config.ts # Vite 建構工具設定
└── vitest.config.ts # Vitest 測試設定

這時候就會出現一個疑問了,所以 modules/moduleA/router/index.ts 裡面的路由該怎麼跟全域路由做整合呢?這邊我們可以透過動態匯入的方式來達成,像是這樣:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// src/router/index.ts
import { createRouter, createWebHistory } from 'vue-router';
import moduleARoutes from '../modules/moduleA/router';
import moduleBRoutes from '../modules/moduleB/router';

const routes = [
...moduleARoutes,
...moduleBRoutes,
// 其他全域路由
];

const router = createRouter({
history: createWebHistory(),
routes,
});

export default router;

然後 modules/moduleA/router/index.ts 裡面就只需要定義該模組的路由即可:

1
2
3
4
5
6
7
8
9
10
11
// src/modules/moduleA/router/index.ts
import { RouteRecordRaw } from 'vue-router';

const moduleARoutes: RouteRecordRaw[] = [
{
path: '/',
name: 'ModuleAHome',
component: () => import('../components/ModuleAHome.vue'),
}
]
export default moduleARoutes;

這時候你應該發現一件事情了,當有其他專案有相同模組需求的時候,你只需要把 modules/moduleA/ 這個資料夾整個搬過去就可以了,完全不需要擔心其他模組會影響到這個模組。

模組化結構最大問題就是巢狀過深,當模組越來越多的時候,專案結構會變得非常複雜,這時候就需要靠良好的命名規則來維持專案的可讀性。

Nuxt 呢?

其實這邊算是沒特別講到 Nuxt 的專案結構,因為 Nuxt 本身就有一套自己的專案結構,像是 pages/components/layouts/plugins/middleware/ 等等,這些都是 Nuxt 幫你規劃好的。

除此之外,也有 Auto Import 的功能,讓你不需要手動匯入元件、組件、插件等等,這樣可以大幅減少你在專案結構上的負擔。

當然你也可以使用 Nuxt 所提供的『Nuxt Layers』功能來達成模組化結構,這樣就可以讓你在 Nuxt 專案中使用模組化結構。

結論

其實不論選擇哪一個專案結構,最重要的還是要符合你的專案需求,像是專案的規模、團隊人數、開發流程等等,這些都是需要考量的因素,並不是因為你喜歡某一種結構就一定要使用,如果團隊對於 Modular Structure 不熟悉的話,強行使用反而會讓專案變得更複雜。

當然這些專案結構並不是絕對的標準,你可以根據你的需求進行調整,像是結合 Atomic Structure 跟 Modular Structure,或是根據功能來劃分資料夾,又或者自己變體出自己一套適合團隊的專案結構,這些都是可以的。

整理這些技術筆記真的很花時間,如果你願意 關閉 Adblock 支持我,我會把這份感謝轉換成更多「踩坑轉避坑」的內容給你!ヽ(・∀・)ノ

Advertisement

你的支持會直接轉換成更多技術筆記

如果我的筆記讓你少踩一個坑、節省 Debug 的時間,
也許你可以請我喝杯咖啡,讓我繼續當個不是 Array 的 Ray ☕

buymeacoffee | line | portaly

Terminal

分享這篇文章

留言

© 2025 Ray. All rights reserved.

Powered by Ray Theme