
前言
一個專案的目錄結構其實非常的重要,為什麼呢?對於你管理與維護專案來說,良好的結構可以讓你知道什麼東西該放哪、什麼東西該怎麼找,接著搭配 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-500、text-center、p-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
| 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
| 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