昨天我們先把開發環境準備好,也看懂了 package.json 和套件是怎麼讓專案跑起來的。
而回到剛開始做 BuJo 的時候,我很快就遇到另一個問題:
功能寫完之後,這個檔案到底該放在哪個資料夾才是對的?
打開 Project 一看,裡面有好多不同名字的檔案和資料夾:
project/
├─ src/
│ ├─ components/
│ ├─ stores/
│ ├─ services/
│ └─ router/
├─ public/
└─ ...
有些是建立專案時就出現的,有些又像是後來自己建立的。
當時我其實很疑惑:
這些資料夾是原本就規定好的嗎?如果不是,一個專案到底是依照什麼邏輯,決定哪些檔案該放在哪裡?
所以今天就從 專案結構(Project Structure) 開始,把這張地圖從外到內拆開來看。
接下來先用一個 Vue 專案可能會看到的結構來理解。
把整個專案拉遠來看,大致可以看到不同用途的區域:
project/
├─ src/ ← 主要程式碼
├─ public/ ← 靜態資源
├─ tests/ ← 測試(也可能採其他安排)
├─ package.json ← 套件、scripts 等專案資訊
├─ vite.config.js ← Vite 設定
└─ ...
這些不同用途的檔案與資料夾,一起構成了整個 專案結構(Project Structure)。
專案結構處理的是一個比較上層的問題:
整個專案裡,不同性質的東西應該放在哪個區域?
再往下一層看,開發功能時,我們最常動到的,通常就是集中在 src/ 裡的這些程式碼。
當 src/ 再被打開:
src/
├─ components/
├─ stores/
├─ services/
├─ composables/
├─ router/
└─ ...
問題就又往下一層了:
同樣都是程式碼,為什麼還要分成這麼多資料夾?
接下來,就來看這些程式碼到底可以依照什麼方式分類、組織。
先不要急著背每個名字。
以 Vue 專案常見的情況來看,可以先按照「為什麼會出現」理解:
常見的應用程式區域
├─ components/ → 畫面元件
├─ assets/ → 圖片、CSS、字型等資源
└─ router/ → 路由設定(有 Routing 時)
依技術選擇
├─ stores/ → 使用狀態管理時
├─ composables/ → 抽出可重複使用的 Composition API 邏輯時
└─ services/ → 團隊選擇集中 API/外部服務邏輯時
依產品需求
└─ locales/ → 需要 i18n 多語系時
再翻成白話:
| 資料夾 | 通常拿來放什麼 |
|---|---|
components/ |
Vue 元件,也就是畫面上的 UI 單位 |
assets/ |
圖片、CSS、字型等資源 |
router/ |
URL 對到哪個頁面,以及路由規則 |
stores/ |
需要集中管理、讓多個元件使用的狀態 |
composables/ |
可重複使用的 Vue Composition API 邏輯 |
services/ |
API、HTTP Client 或外部服務相關邏輯 |
locales/ |
多語系翻譯內容 |
utils/ |
比較通用的小工具函式 |
所以這些並不是:
「Vue 已經幫我準備好的標準櫃子。」
有些會受到使用的框架、套件或開發工具影響,有些則是開發者依照專案需求自己建立的。
那問題就來了:
工程師到底依據什麼,決定哪些程式碼要分開、哪些又適合放在一起?
這裡會碰到一個重要的軟體工程原則:
關注點分離(Separation of Concerns, SoC)。
可以先把它理解成:
不同責任的程式碼,不要毫無邊界地全部揉在一起。
例如:
介面顯示
路由
狀態管理
API 溝通
這些程式碼負責的事情不同,所以通常會依照各自的責任整理到不同區域。
但要注意,分的是責任,不是單純把不同類型的檔案全部拆開。
Vue 官方在介紹 單一檔案元件(Single-File Component, SFC) 時也特別提醒:
關注點分離(SoC)不等於按照檔案類型把程式碼拆開。
一個 .vue 檔裡可以同時有:
template
script
style
因為它們雖然形式不同,卻都在共同描述同一個元件。
所以 SoC 真正要處理的是:
哪些責任應該靠近,哪些責任應該分開。
知道「為什麼要分」之後,接下來才進到實際的程式碼組織方式。
先看兩個常見方向,以及實務上也會使用的混合方式:
程式碼組織方式
│
├─ 按類型組織(by-type)
│ └─ 按技術類型/角色分類
│
├─ 按功能組織(by-feature)
│ └─ 按產品功能分類
│
└─ 混合式組織(Hybrid)
└─ 混合使用不同的組織方式
這些不是 Vue 規定的三種標準架構,而是不同的程式碼組織思路。
例如:
src/
├─ components/
│ ├─ ActivityPage.vue
│ └─ FriendsPage.vue
│
├─ stores/
│ ├─ activityStore.js
│ └─ friendStore.js
│
└─ services/
├─ activityApi.js
└─ friendApi.js
它的分類邏輯是:
畫面元件 → components/
共用狀態 → stores/
服務邏輯 → services/
也就是先問:
這個檔案是什麼類型?
同一種類型的程式碼,就集中放在同一個資料夾裡。
在專案規模還不大的時候,這種方式通常很直覺。
但如果換一個角度,不是要找「所有 Store」或「所有 Component」,而是:
我要修改 Activity 功能。
按照剛剛的 by-type,就可能需要分別到:
components/
stores/
services/
把所有和 Activity 有關的程式碼找出來。
另一種做法,就是先按照產品功能分類:
src/
└─ features/
├─ activity/
│ ├─ components/
│ ├─ store/
│ └─ services/
│
└─ friends/
├─ components/
├─ store/
└─ services/
這就是 按功能組織(by-feature)。
它先問的是:
這段程式碼屬於哪一個功能?
所以兩者最簡單的差異可以記成:
by-type
→ 先看「它是什麼類型?」
by-feature
→ 先看「它屬於哪個功能?」
by-type 和 by-feature 不一定只能二選一。
例如:
src/
├─ features/
│ ├─ activity/
│ ├─ friends/
│ └─ auth/
│
├─ shared/
│ ├─ ui/
│ └─ utils/
│
├─ router/
└─ assets/
跟特定功能綁得比較緊的程式碼,可以放在各自的 features/ 裡。
真的會被多個功能共用的東西,再抽到 shared/。
這就是一種 混合式組織(Hybrid)。
它不是另一套固定模板,而是依照專案實際需求,把不同的組織方式搭配使用。
所以真正的問題並不是:
「by-type 和 by-feature 到底哪個比較專業?」
而是:
「現在這個專案結構,還適合目前的程式碼規模和開發方式嗎?」
專案剛開始時:
components/
stores/
services/
可能已經非常清楚。
但隨著功能越來越多,也可能開始出現:
| 現象 | 可能代表 |
|---|---|
| 修改一個功能要跨很多資料夾 | 相關程式碼可能離得太遠 |
| 同一個資料夾塞進大量互不相關的檔案 | 原本的分類開始不好找 |
| 新增檔案時常常不知道該放哪 | 責任邊界開始變得模糊 |
這時候才可能需要重新整理,逐步改成 by-feature,或採用混合式組織。
所以 專案結構(Project Structure) 並不是專案建立第一天決定一次,之後就永遠不動。
它也可能隨著程式碼規模、功能和團隊需求一起演化。
BuJo 前端目前的 src/ 大致是:
src/
├─ assets/
├─ components/
├─ composables/
├─ locales/
├─ router/
├─ services/
├─ stores/
├─ utils/
├─ App.vue
└─ main.js
從最上層來看,components/、composables/、services/、stores/ 主要是按照程式碼的技術角色分類,所以整體上屬於 按類型組織(by-type) 的思路。
再往資料夾裡面看,也會依照實際用途繼續細分,例如:
components/
└─ ui/
也就是先有一個主要的分類方式,再根據實際需要往下整理。
這也比較接近真實專案的樣子:不一定每一層都要套上一個新的架構名稱,而是讓目前的結構維持清楚、好找、好維護。
前端在整理程式碼時,會思考畫面、狀態、路由、API 等責任該怎麼分。
換到後端,問題則會變成:
誰負責接收請求?
誰負責處理業務邏輯?
誰負責和資料庫或外部服務溝通?
這些不同責任,也會反映在資料夾結構上。
先用一個常見的後端結構來看:
src/
├─ routes/
├─ controllers/
├─ services/
├─ middleware/
├─ lib/
└─ __tests__/
| 資料夾 | 通常負責什麼 |
|---|---|
routes/ |
定義 HTTP 路由,決定請求要交給哪個處理程式 |
controllers/ |
接收請求、取得參數、呼叫後續邏輯並組成回應 |
services/ |
處理業務邏輯或外部服務整合 |
middleware/ |
在主要處理流程前後執行共用邏輯,例如驗證、流量限制、錯誤處理 |
lib/ |
共用的底層工具或第三方函式庫封裝 |
__tests__/ |
測試 |
這背後同樣會用到 關注點分離(Separation of Concerns, SoC)。
例如後端常見會看到這類責任層次:
Route
↓
Controller
↓
Service
↓
Database / ORM
這種按照責任層次來組織的方向,也常會談到 按層組織(by-layer)。
不過這不是所有後端專案都必須照抄的固定架構,而是其中一種常見的責任切分方式。
所以前端和後端真正共通的,不是資料夾名稱,而是:
都需要把不同責任的程式碼分到合理的邊界裡。
以前我會把資料夾結構想成單純的檔案整理:
這個是元件,就放進 components/;
那個是狀態管理,就放進 stores/。
但現在才發現,真正重要的不是把檔案收得多整齊,而是替不同責任畫出清楚的邊界。
這樣之後要找程式碼時,會比較知道該從哪裡開始;修改功能時,也比較不容易牽動一堆原本不相關的地方。
而且這張地圖也不是一開始決定好就永遠不變。
隨著專案規模變大、功能變多,原本好用的分類方式也可能需要重新調整。
所以資料夾結構真正解決的,不只是「檔案該放哪」,而是讓整個專案之後更容易理解、修改和維護。
下一個問題就是:
每一次有人進來改過什麼,我們要怎麼留下紀錄?
下一篇,就來正式拆 Git。