今天分享在 Vaultflix 中,我認為一個專案該有的目錄結構以及如何透過目錄結構就能表達出專案中一些潛在的框架約束.
有關由檔案目錄結構去管理整個專案,我認為是一個很直觀的行為,而我最近讀到管理書"葛洛夫:給經理人的第一課",裡面有提到公司組織其實有分功能向以及目標向兩種.
那我回顧以往專案結構,似乎都是先功能向,像是A模組是一個資料夾,然後下面才是目標向,模組提供一個具體的業務邏輯行為,這是一個專案維度出發.
但在這專案中,其實是偏向系統視角的,因為實際上這專案有前端、後端、資料庫、MinIO、Nginx等組建,我這邊就會想那這種全部混合的專案能怎樣好的管理,因為我當初其實沒在這塊特別關注.
也就是說我從第一天開始做這專案,其實就只關注功能結果,這讓我中間有關哪些檔案放哪裡其實都是沒有概念,但這段我認為應該同樣要有一份規格跟原則出來,這樣人類才能跟AI有討論的空間.
我請 Claude 讀整個 repo 做了一份目錄結構診斷,下面是我驗證過的重點。結論一句話:Vaultflix 是「功能向為主、目標向正在自己長出來」的混合體。混合本身沒問題,問題是它是無意識發生的,兩套慣例並存又沒寫下來。
後端 internal/:
VideoRepository 這種大 interface,共 12 個;新路線在使用端定義窄 interface,例如 keyframeVideoRepo、segmentEnsurer。追下去發現 CLAUDE.md 那句「interface 定義在使用端(例如 repository interface 定義在 repository package)」本身就前後矛盾,AI 兩種都照做過。internal/mock 集中放了 16 個 mock,跟被 mock 的 interface 分居兩地,加一個 interface 要動兩個 package。前端 web/src:
PosterThumb.tsx 有兩份,components 與 components/admin 各一份,是「不知道該放哪就複製一份」的典型症狀。根目錄那層倒是沒問題,cmd、internal、migrations、compose 都符合 Go 社群慣例。所以要重新定義的不是最外殼,是 internal 和 web/src 裡面怎麼分。
先講原則,再講樹。葛洛夫講的核心不是「選功能向還是目標向」,而是讓最常需要協調的人坐得最近。翻成目錄就是:外層放最常一起被改的軸。Vaultflix 的 CLAUDE.md 規定一個對話只做一個場景,場景幾乎都是 feature 級的,所以「一起被改」的單位就是 feature。這是目前結構跟工作方式對不齊的根本原因。
兩種分法的取捨:
| 功能向,按層分 | 目標向,按 feature 分 | |
|---|---|---|
| 優點 | 橫向規範好統一,新人知道「HTTP 的東西在哪」 | 改一個 feature 只碰一個目錄,目錄自我描述 |
| 缺點 | 目錄不說明系統做什麼,package 內聚低變雜物間 | 橫切關注點沒地方放,feature 間共用會產生 import cycle |
| 適合 | 小型 CRUD,feature 數少 | feature 多、平行開發、AI agent 分工 |
收斂成的形狀只有一種:外層按 context 或 feature,內層按層或依賴,另留一個嚴禁放業務邏輯的 shared,import 方向單向。
後端目標樹,把現有檔案按 context 歸位:
internal/
identity/ user、auth、rbac、active_user
catalog/ video、media_source、import、tag、favorite、path_safety
playback/ hls、segment、keyframe、watch_session、history、telemetry、play_mode
enrichment/ scraper、avid、actress、suggestion、enrichment、backfill
insight/ analytics、recommendation
platform/ config、websocket hub、minio client、url_cache、response、errors
↑ 嚴禁放業務規則
每個 context 內部不做完整的 DDD 四層,那對這個規模的個人專案是過度設計。用 Go 的扁平寫法:context 的根 package 放 domain type 與 interface,子 package 按依賴分成 postgres、http、mock,main 負責接線。硬規則只有一條:依賴箭頭只往內指,所以 interface 必然定義在使用端,實作在外圈。
前端目標樹:
web/src/
features/
player/ api、components、hooks 自己一包
admin/
auth/
browse/
components/ 真正跨 feature 共用的才放這裡
lib/ 純邏輯,以內容命名,例如 lib/format
types/
tag、favorite、user 這種薄 CRUD 留在原本的分層裡不動。混合是合理終態,重點是把「哪些走哪套」寫下來。
真正能解「不知道該放哪」的是決策順序,不是目錄名。
接下來的順序是規則先於搬遷:先把上面的內容寫成 docs/ARCHITECTURE.md,順手修掉 CLAUDE.md 那句矛盾的 interface 說明,然後統一成使用端定義的窄 interface,mock 跟著使用端走。搬檔案的 PR 會很大而且 git blame 會斷,不急著做。
今天就先以重新樹立 vaultflix 的檔案目錄定義做收尾,明天會接續看我做這項目的中間更多和AI有往來的部份.