用 Feature First 的方式管理前端專案複雜度

BDEEFE發表於2019-04-12

什麼是複雜度

軟體系統變得複雜的三個成因是規模、結構與變化。

針對三個成因,我們可以通過以下方式簡化系統:

  • 分而治之,控制規模
  • 保持結構的清晰與一致
  • 擁抱變化

依據這些原則,我們該如何組織檔案結構,管理好前端專案複雜度?

前端專案中,組織檔案結構的兩種常見方式

File-Type First (FTF)

『按檔案型別組織』,這也是前端專案中最普遍的組織方式。例如:

src
├── index.js
├── components
│   ├── index.js
│   ├── footer
│   ├── header
│   ├── posts
│   └── users
│       ├── UserDetail.js
│       └── UserList.js
├── containers
│   ├── home
│   ├── posts
│   │   └── index.js
│   └── users
│       └── index.js
├── actions
│   ├── posts.js
│   └── users.js
├── reducers
│   ├── posts.js
│   └── users.js
└── sagas
    ├── posts.js
    └── users.js
複製程式碼

Feature First

『按功能特性組織』。例如:

src
├── components
│   ├── footer
│   ├── header
│   └── index.js
├── features
│   ├── posts
│   │   ├── action.js
│   │   ├── components
│   │   │   └── index.js
│   │   ├── containers
│   │   │   └── index.js
│   │   ├── reducers.js
│   │   └── sagas.js
│   └── users
│       ├── action.js
│       ├── components
│       │   └── index.js
│       ├── containers
│       │   └── index.js
│       ├── reducers.js
│       └── sagas.js
└── index.js
複製程式碼

『關注點分離』的差異

兩種檔案組織方式都是在做『關注點分離』,不同的是對『關注點』的理解。

  • File-Type First 的 『關注點』是技術和手段。
  • Feature First 的『關注點』是功能和目標。

『語言』分離和『元件』分離

用 Feature First 的方式管理前端專案複雜度
(圖片來源:A feature based approach to React development

『功能特性』分離

用 Feature First 的方式管理前端專案複雜度
(圖片來源:Why React developers should modularize their applications?

你應該採用哪種方式?

File-Type First 的特點是:簡潔、直接,符合開發者的直覺。而 Feature First 在管理大規模專案的複雜度上更有優勢。

深入 Feature First 的檔案組織方式

Feature First 檔案組織方式的優勢:

  1. 程式碼易於查詢、定位 程式碼的組織方式反映了產品結構,與產品需求相對應。

  2. 程式碼更易於維護 每個 Feature 隔離,當修改一個 Feature 中的程式碼修改、重構時,不會影響其它 Feature。 多特性並行開發時,更大程度上避免 merge 時產生的衝突。

  3. 啟用 Feature Flags 機制

Feature Flag 是一種通過配置開功能特性的技術,無須重新部署程式碼。

像類似 A/B Testing 的需求,也可以借用 Feature Flags 實現。

程式碼示例:

// features.json
{
  ...,
  portal: true,
  users: true,
  posts: false
}

// index.js
export function isFeatureEnabled(feature) {
  return features[feature] || false;
}
複製程式碼

將 Feature First 進行到底

如果某個 Feature 比較複雜,可以將其進行一步細分,形成 Feature 的巢狀結構。 例如:將 features/users 細分為

  • features/users/features/detailView
  • features/users/features/listView
src
├── features
│   └── users
│       ├── components
│       │   ├── Table.js
│       │   └── index.js
│       └── features
│           ├── index.js
│           ├── detailView
│           │   └── components
│           │       └── Detail.js
│           └── listView
│               └── components
│                   └── List.js
└── index.js
複製程式碼

如何組織應用內的共享元件

有的元件跨 Feature 複用,有的元件同 Feature 內跨 Page 複用。 為了保證良好的可維護性,共享元件的組織應該遵循明確的規則。

下面是一個供參考的方案。(components 目錄下的 index.js 只負責 export 元件,不實現具體功能)

src
├── components
│   ├── Notification.js
│   └── index.js
├── features
│   ├── posts
│   │   └── components
│   │       └── index.js
│   └── users
│       ├── components
│       │   ├── Table.js
│       │   └── index.js
│       └── features
│           ├── index.js
│           ├── detailView
│           │   └── components
│           │       └── Detail.js
│           └── listView
│               └── components
│                   ├── List.js
│                   ├── Title.js
│                   └── components
│                       ├── index.js
│                       ├── Pagination.js
│                       └── components
│                           └── index.js
└── index.js
複製程式碼

請思考一下,在上述專案結構中, src/features/users/pages/listView/components/List.js 可以共享使用的元件有哪些?

接下來,我們一一來看:

  • src/components 內放置的是應用範圍內的共享元件,所以,List 可以使用其中的所有元件。
  • src/features/posts/components 跟 List 屬於不同 Feature,所以,無法使用其中元件。
  • src/features/users/pages/detailView/components,雖然跟 List 屬於同一個 Feature,如果允許從listViewdetailView的『同層引用』,也會增加 Feature 內的檔案引用複雜度。所以,這類引用也是要被禁止的。
  • src/features/users/pages/listView/components/components (與 List 同級的components 目錄),List 的子元件就放在同級的 components 目錄中,所以,允許訪問。
  • src/features/users/pages/listView/components/components/component (與 List 同級的 components 目錄的子級 components),『跨層引用』也會增加複雜度,所以,也不允許此類引用。

把上述情況,歸結成一句話就是:

除了同目錄檔案,元件只能引用其所在檔案各級路徑下的 components 目錄。

src/features/user/pages/listView/components/List.js 按照上述規則展開:

  • src/features/user/pages/listView/components/components
  • src/features/user/pages/listView/components
  • src/features/user/pages/components
  • src/features/user/components
  • src/features/components
  • src/components

看到這,可能找個了一個熟悉的身影。 是的,它跟 CommonJS 中的模組查詢規則很類似。

components 目錄裡放什麼?components 目錄的嚴格意義是,放置僅供同級元件複用的子元件。例如:上述與 List 同級的 components 目錄,應該存放僅供 List、Title 複用的子元件。

超越 Feature 的專案複雜度

新的邏輯層次

在人才管理、財務管理等企業級應用中,僅有 App 和 Feature 已經不能如期地對應上產品結構了。

這時,我們需要一個新的邏輯層次,如:Solution。 一個 Solution 包含若干個 App,每個 App 有多個 Feature。

有了 Solution 新的邏輯層次,根據內聚性,可以把原來的 Feature 分拆到不同的 App 中。 相比於 Feature,App 間的耦合性更小,甚至可以當作獨立的部署單元。

從多模組(multi-module) 到 多包(multi-package)

專案的規模大了,依賴管理上的挑戰也出現了。 Feature 之間要減少依賴, App 之間更要進一步隔離, 跨 App 複用的模組,就不能簡單地 import 了。

為了減少 App 之間的耦合,需要將複用單元需要封裝成 package, 然後,各個 App 在 package.json 中宣告依賴。 同樣,在 Solution 的眼中,各個 App 也是 package。

通過從 multi-module 到 multi-package 的轉變,耦合減小了, 但給開發也帶了新的成本,這些問題可藉助於 Lerna 等工具解決。

使用 Lerna 可以解決依賴和版本管理的問題,除此之外,還要做好 package 的分層設計。

多包管理中的分層設計

App 是一個 package,App 依賴的模組也是一個 package, 但是,兩類 package 是不同『質』的。

為了讓專案結構更清晰、易理解,我們需要區分這些 package,進行分層設計。例如:

packages
├── solutions
│   ├── login.sln
│   ├── finance.sln
│   └── people.sln
├── apps
│   ├── portal.app
│   ├── financial-management.app
│   ├── recruting.app
│   └── global-search.app
├── biz-libs
│   ├── workflow-engine
│   ├── rendering-engine
│   └── network
├── base-libs
│   ├── ui-components
│   └── animations
└── vendor-libs
    ├── router
    └── state-manager
複製程式碼

總結

與 File-Type First 的檔案組織方式相比, Feature First 在提升大規模專案的可維護性上有著明顯的優勢。

  • 更易於查詢、定位程式碼檔案;
  • 重構、修改程式碼時,更好地控制影響範圍;
  • 更利於使用 Feature Flags;

在組織應用內的共享元件時,可以遵循跟 CommonJS 類似的模組查詢方式:元件只引用其所在檔案各級路徑下的 components 目錄

在企業級應用等大規模專案中,可以通過引入新的邏輯層次和多包管理,進一步控制專案複雜度。

參考資料


文章作者:孔常柱

BDEEFE 在全國各地長期招聘優秀的前端工程師,招聘需求瞭解下?

相關文章