專案資料夾架構指南實作建議 定義 By Type 與 By Feature 流派以優化可維護性

me
林彥成
2022-09-30 | 6 min.
文章目錄
  1. 1. 什麼是專案資料夾架構?
    1. 1.1. 屬性分類 (By Type):適用於小型專案的簡單結構
    2. 1.2. 功能分類 (By Feature):大專案的高內聚力架構
  2. 2. React 專案架構:元件與資料流的組織策略
    1. 2.1. 前端專案依屬性分類
      1. 2.1.1. Side Effect 處理
    2. 2.2. 前端專案依功能分類
    3. 2.3. 簡化架構的函式庫與框架
  3. 3. 後端專案架構:Controller、Route 與 Model 的收納之道
    1. 3.1. 後端專案依屬性分類
    2. 3.2. 後端專案依功能分類
  4. 4. FAQ:專案架構常見問題
    1. 4.1. Q1:什麼時候應該從 By Type 遷移到 By Feature?
    2. 4.2. Q2:有些元件是「全局共用」的(如 Button, Input),應該放在哪裡?
    3. 4.3. Q3:後端專案如果使用了功能分類,該如何處理關聯查詢(Joins)?

什麼是專案資料夾架構?

專案資料夾架構 (Project Folder Structure) 是指在軟體開發中,如何有邏輯地組織原始碼檔案、資源與配置的設計模式。其核心目的在於提升「可維護性」與「開發者體驗 (DX)」。目前主流分為兩大流派:1. 屬性分類 (By Type):將相同性質的檔案(如所有 CSS 或所有 API 邏輯)集中存放,適合小型、邏輯單純的專案;2. 功能分類 (By Feature):將特定業務功能所需的所有檔案(如登入功能相關的組件、邏輯與樣式)放在同一目錄下 (Co-location),能顯著降低模組間的耦合度,適合中大型及需要快速規模化的專案。選擇高品質的收納方式,能讓團隊成員在「尋寶」程式碼時更直觀,是專業專案管理的第一步。


今天談談 專案資料夾架構 的結構,在還沒開始寫任何程式前,可以思考的是檔案要怎麼收納。良好的 專案管理 始於清晰的檔案組織。

不知道各位情場高手都怎麼收納前男友或前女友們的禮物呢?是按照物品種類來分類?還是依照對象來分類?或是直接丟掉?隨著數量上升後管理的難度也會上升,小編覺得程式碼也跟收納一樣分成兩種流派:

  1. 按照屬性分類 (By Type)
  2. 按照功能分類 (By Feature)

接下來就讓我們看看這兩種 專案資料夾架構 的差別吧!


屬性分類 (By Type):適用於小型專案的簡單結構

會看屬性分得多細,若是分類比較粗一點,當衣服的數量變多就會變成很難找到整套運動服該如何正確搭配。屬性分類專案資料夾架構 中,就是將相同類型的檔案(如所有的 Action、所有的 Component)放在同一個資料夾下。

依專案來說,小編認為小專案較適合這種 屬性分類 配置,比較適合拿來學習新知識時使用。

屬性分類用衣服的概念來看就是分類成

  • 全部的內衣
  • 全部的內褲
  • 全部的褲子
  • 全部的上衣
  • 全部的外套

功能分類 (By Feature):大專案的高內聚力架構

當衣服變多的時候也知道怎麼依照功能找到全部相關的配套。功能分類 則是將與特定功能相關的所有檔案(Action, Component, Reducer 等)都放在同一個資料夾中。

依專案來說,小編認為大專案較適合這種 功能分類 配置,每個功能都會有自己屬於的資料夾,能大幅提升 專案管理 的效率。

功能分類就會是

  • 慢跑外套、上衣、褲子、內衣、內褲
  • 登山外套、上衣、褲子、內衣、內褲
  • 約會外套、上衣、褲子、內衣、內褲
  • 上班外套、上衣、褲子、內衣、內褲
  • 居家外套、上衣、褲子、內衣、內褲
  • 休閒外套、上衣、褲子、內衣、內褲

依專案來說,小編認為大專案較適合這種配置,每個功能都會有自己屬於的資料夾,對菜鳥工程師來說則是快樂小天地。

React 專案架構:元件與資料流的組織策略

在進行 React 專案架構 設計時,一般會有元件、API、資料流、樣式相關分類。

React 是 component-based 的前端 UI 函式庫,通常會配合其他函式庫使用:

  • react-router: 處理 SPA 的路由
  • redux: 做統一的狀態管理,協助元件之間的溝通
    • redux-thunk or redux-saga: 處理 AJAX 的 side effect

前端專案依屬性分類

比較小的專案如果不包含 Redux 可以簡單分類

1
2
3
4
5
6
7
8
9
10
api/
├─ ProfileAPI.js
└─ UserAPI.js
components/
├─ Footer.js
├─ Footer.css
├─ Profile.js
├─ ProfileHeader.js
└─ ProfileHeader.css

搭配 Redux 後,主要就是增加元件之間需要溝通工具,會有以下特性:

  • 單向資料流
  • 公共的狀態儲存 (Store)

專案架構也會受到這種概念去分類:

  • actions: 觸發狀態改變用的 function
  • pages: 頁面容器元件
  • containers: 有連接 Store
  • components: 沒有連接 Store
  • reducers: 收到 action 後的資料邏輯

所以專案資料夾架構依照屬性去分類就會長成下面這個樣子,在完成任務的過程中需要在多個不同的資料夾中來回,覺得較適合小型專案。

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
└── src
├── actions
│ ├── typeOneActions.js
│ └── typeTwoActions.js
├── api
│ ├── apiHandler.js
│ ├── typeOneApi.js
│ └── typeTwoApi.js
├── components
│ ├── TypeOneComponent.jsx
│ ├── TypeOneListComponent.jsx
│ ├── TypeTwoComponent.jsx
│ ├── TypeTwoPageComponent.jsx
│ └── HomePageComponent.jsx
├── containers
│ ├── TypeOneContainer.js
│ └── TypeTwoPageContainer.js
├── pages
│ └── HomePageContainer.js
├── index.js
├── reducers
│ ├── typeOneReducer.js
│ └── typeTwoReducer.js
├── routes.js
├── store.js
└── utils

之前弄的小專案當例子,如果有加上 redux 的話,專案架構大概會長成下面的樣子,在比較簡單的專案中可以更簡化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
├── .storybook
├── src/
│ ├── constants/
│ ├── utils/
│ ├── pages/
│ ├── containers/ # containers 放置與 Redux 連接的相關元件,單元測試檔案為 `元件名稱.test.js`
│ ├── components/ # components 放置相關元件,單元測試檔案為 `元件名稱.test.js`
│ │ └── Root.js # 路由根目錄
│ ├── hooks/ # hooks 相關
│ ├── middleware/ # 資料處理相關
│ │ ├── API.js # axios 的 instance
│ │ └── redux-api.js # redux-api
│ ├── styles/ # 樣式檔們
│ ├── index.js # 程式入口
│ ├── serviceWorker.js
│ └── setupTests.js # 測試相關設定
├── .eslintrc # ESLint 設定檔
├── .prettierrc # prettierrc 工具設定
├── .travis.yml # 持續發佈工具
├── package-lock.json
├── package.json
└── README.md

Side Effect 處理

平常不太會去特別注意的名詞,我們用這個來分類 function 或元件:

  • Pure function or UI 元件
    • 每次送進去固定的輸入,出來就會是固定的結果
    • 相同的 props, 產生相同的 component,會放在 components 資料夾
    • 不寫商業邏輯
    • 不加入有 side effect 的 code
  • 有 side effect 的 function or 元件
    • 因為與狀態相關所以就會放在 containers 資料夾
    • AJAX,相關程式在 react hooks 中就會放在 useEffect,class component 就會放在 componentDidMount
    • AJAX 跟 redux,side effect 交給 redux-thunk 或是 redux-saga 去管理

最近開始接觸單元測試後,也深刻感覺到 side effect 越少,測試也越好寫,所以我想如果刻意去寫出好測試的元件,最後也會讓專案的結構變得更好偵錯與維護。

前端專案依功能分類

對新加入專案的工程師來說,最多的時間就是花在找尋相關的程式,其實可以透過小訣竅可以去優化前端開發者體驗,那在資料夾的架構方面,其實很簡單的透過把相近功能的程式碼放在一起 (Co-located) 就可以大量省去尋寶的時間,也能夠更方便的管理。

1
2
3
4
5
6
7
├── Login/
│ ├── SocialButton/
│ │ ├── LineButton.js # 社群登入的 Line 按鈕
│ │ └── FacebookButton.js # 社群登入的 FB 按鈕
│ ├── Modal/
│ │ └── ModalLogin.js # 登入的 Modal
│ └── index.js # 登入邏輯與主要 Layout

比較大的專案,就會把相關功能的元件都放在同個資料夾中降低相依性、增加內聚性,這樣在維護和修改時可以確定只要照顧好這個資料夾中的檔案即可。

使用 ReduxToolKits 後按照功能搭配分類,覺得較適合大型專案,依照功能就會分成

  • typeOne
  • typeTwo
  • typeThree

每個功能所需要的 action、components、containers、reducers 都會放在一起,所以在開發時,每個工程師都可以在獨立的資料夾中完成該次的任務。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
src
└── features
├── typeOne
│ ├── TypeOne.js
│ ├── TypeOne.styles.scss
│ └── typeOneSlice.js
├── typeTwo
│ ├── TypeTwo.js
│ ├── TypeTwo.styles.scss
│ └── typeTwoSlice.js
└── typeThree
├── TypeThree.js
├── TypeThree.styles.scss
└── typeThreeSlice.js

元件如果複雜一些也是依照功能區分,覺得較適合大一點的專案。

1
2
3
4
5
6
7
8
.
└── admin
└── UserCard
├── layouts
│ ├── UserCard.mobile.tsx
│ └── UserCard.desktop.tsx
└── UserCard.tsx

簡化架構的函式庫與框架

對於專案或是後端收到的資料,若沒有太多需要客製化的部分,可用底下函式庫與框架簡化:

  • storybook: 用故事書元件的角度設計,元件通常就能夠獨立且好測試
  • 將 Redux 常用的寫法封裝來減少寫太多重複的程式碼,簡化架構
    • Redux Toolkit
      • RTK Query
      • createSlice
  • axios,Promise based HTTP client for the browser and node.js
    • 前後端共構
    • 使用客製化 instance 的方式可以將收跟送的邏輯統一實作,減少重複的程式碼
  • Next.js,是一套以 React.js 生態系打造出來的完整的框架,幫我們配置了
    • AMP
    • 路由
    • CSS-in-JS
    • 靜態頁面輸出
    • 伺服器渲染

後端專案架構:Controller、Route 與 Model 的收納之道

通常 Mongoose 會搭配一個後端的框架來做使用,像是 Express 或是 Koa 等等。在設計 後端專案架構 時,同樣有屬性分類與功能分類兩種選擇。

後端專案依屬性分類

依照屬性性,controller、routes、models 集中放置在各自的分類資料夾,覺得較適合小型專案。

1
2
3
4
5
6
7
8
9
├── src/
│ ├── controller/ # 資料庫 CRUD
│ ├── routes/ # API 設定
│ ├── models/ # Schema 定義
│ └── index.js # 程式入口
├── .eslintrc # ESLint 設定檔
├── package-lock.json
├── package.json
└── README.md

後端專案依功能分類

依照資料功能去做區分,把同個資料表的 model、controller、route 集中存放,覺得較適合大型專案。

1
2
3
4
5
6
7
8
9
10
11
├── src/
│ ├── entities/ # 按照資料表去區分
│ │ └── user/ # 使用者表
│ │ ├── model.js # Schema 定義
│ │ ├── controller.js # 資料庫 CRUD
│ │ └── route.js # API 設定
│ └── index.js # 入口
├── .eslintrc # ESLint 設定檔
├── package-lock.json
├── package.json
└── README.md

FAQ:專案架構常見問題

Q1:什麼時候應該從 By Type 遷移到 By Feature?

A:當您發現「修改一個簡單功能需要打開五個不同目錄下的檔案」或者是「src 下的 components 資料夾已經累積超過 30 個檔案」時,就是遷移的最佳時機。By Feature 能讓您在一個目錄內完成所有相關修改,大幅提升開發流暢度。

Q2:有些元件是「全局共用」的(如 Button, Input),應該放在哪裡?

A:在 By Feature 架構中,通常會建立一個 src/componentssrc/common 資料夾,專門存放跨功能使用的原子元件(Atomic Components)。如果某個元件只在特定功能中使用,則應遵循「就近存放」原則,放在該功能的目錄下。

Q3:後端專案如果使用了功能分類,該如何處理關聯查詢(Joins)?

A:雖然 Model 定義在各自的功能目錄中,但您可以在一個專門的 service 層或者是跨模組的 controller 中引用多個 Model 進行處理。功能分類強調的是「擁有權 (Ownership)」而非絕對的隔離,確保核心定義清晰才是收納的關鍵。



喜歡這篇文章,請幫忙拍拍手喔 🤣