React 作為一個庫,不會決定你如何組織專案的結構。這是件好事,因為這樣我們有了充分的自由去嘗試不同的組織方式並且選取最適合我們的方式。但是從另一個角度講,這可能會讓剛剛上手 React 的開發者產生些許困惑。
我將會在本文為大家展示我已經使用過一段時間並且效果不錯的方式,這些方式沒有通過重新造輪子來實現,而是通過將社群中的方案組合和提煉得到。
注意:這裡沒有什麼是絕對正確的!你可以選擇你認為容易理解,並且可以適應或可以改造成適應你的情景的方式。
目錄結構
我最常見到的一個問題就是如何組織檔案和目錄結構。在本文中,我們假設你有一個 create-react-app
生成的最簡單的目錄結構。
create-react-app
為我們生成了一個基礎的專案,包含根目錄還有諸如.gitignore
, package.json
, README.md
, yarn.lock
的檔案。
它還生成了 public
和 src
目錄,src
目錄就是我們存放原始碼的目錄。
看一下下面圖片所描述的結構:
本文我們只會關注 src
目錄,所有在它之外的都會保持不變。
容器和元件
我們可以看到在 src
目錄下有 containers 目錄和 components 目錄:
1 2 3 4 |
src ├─ components └─ containers |
但是這個方式會導致下面這些問題:
- 主觀的規則 你不清楚什麼是容器而什麼是元件,這兩者的差異是主觀定義的。如果在你的團隊裡去推行,讓所有的開發者能夠相同地贊成和評判這兩者是非常困難的。
- 沒有考慮元件的變化 即使你決定了一個元件適用於某種特定的種類,在專案的週期內很容易發生變化,最終迫使你把它從
components
挪到containers
目錄下,反之亦然。 - 允許兩個元件使用同一個名字 元件在應用中的命名應該是宣告式且唯一的,從而避免對相同命名元件的職責產生困惑。但上面的方式為兩個元件可以擁有相同命名開啟了一個缺口,一個可以是容器,另一個可以是展示型元件。
- 效率低下 即使你在實現一個獨立特性時,也不得不經常在 containers 和 components 目錄下來回切換,因為一個獨立特性有兩種不同型別的元件是再正常不過的事情了。
有一種基於這種方式的變種方式,在模組的目錄下保持著兩個目錄的分離。
想象一下在你的應用中有一個 User 模組,在此模組下,你有兩個目錄去分離你的元件:
1 2 3 4 5 |
src └─ User ├─ components └─ containers |
上述方式最小化了在兩個遙遠目錄下不斷切換的的問題,但是同樣增加了很多煩惱。當你的應用有非常多模組的時候,你最終會可能會建立幾十個 containers
和 components
目錄。
所以我們討論如何組織目錄和檔案的時候,和元件是否被拆分為展示型和容器型是無關的。也就是說,我們會把所有的元件都放在 components
目錄下,除了頁面。
即使在目錄上拆分它們是不必要的,瞭解它們之間的差異性依然是有必要的。如果你對這個話題還有疑問,建議閱讀這篇文章:Presentational and Container Components。
拆分和組合程式碼
在 components
目錄下,我們通過模組/特性(module/feature)的結構來組織檔案。
在對使用者進行增刪改查的過程中,我們只會有一個 User 模組。所以我們的目錄結構會像下面這樣:
1 2 3 4 5 6 |
src └─ components └─ User ├─ Form.jsx └─ List.jsx |
每當一個元件會有不止一個檔案的時候,我們會將這個元件和它對應的檔案放在同一個資料夾下,並且使用同一個名字來命名。舉個例子:現在我們有一個 Form.css
檔案包含了 Form.jsx
的樣式,這時我們的目錄結構會像這樣:
1 2 3 4 5 6 7 8 |
src └─ components └─ User ├─ Form │ ├─ Form.jsx │ └─ Form.css └─ List.jsx |
測試用的檔案和被測試的檔案放在一起,在上面這個例子中,
Form.jsx
的測試檔案會放在同一個資料夾下並且命名為Form.spec.jsx
UI 元件
除了通過模組拆分元件,我們還會在 src/components
放置一個 UI
目錄,用於存放所有通用的元件。
UI 元件不屬於任何一個模組,需要足夠通用。它們應該可以直接放在開源庫中,因為它們不包含任何特定應用的業務邏輯。常見的這類元件有:按鈕,輸入框,核取方塊,下拉選擇,模態框,資料視覺化元件等等。
元件命名
以上我們瞭解瞭如何組織目錄結構和如何通過模組來拆分我們的元件,但是還有一個問題:如何命名它們?
這裡我們說的是如何命名我們的 class 或者定義元件的常量。
1 2 3 |
class MyComponent extends Component {} const MyComponent = () => {}; |
元件的命名在應用中應當清晰且唯一,這樣可以讓它們可以輕鬆被找到並且避免可能的困惑。
當應用在執行時發生錯誤或者通過 React 開發者工具除錯時,元件的名字是非常方便易用的,因為錯誤發生的地方往往都伴隨著元件的名字。
我們採用基於路徑的元件命名方式,即根據相對於 components
檔案目錄的相對路徑來命名,如果在此資料夾以外,則使用相對於 src
目錄的路徑。舉個例子,元件的路徑如果是 components/User/List.jsx
,那麼它就被命名為 UserList
。
如果檔名和檔案目錄名相同,我們不需要重複這個名字。也就是說,components/User/Form/Form.jsx
會命名為 UserForm
而不是 UserFormForm
。
這樣的命名方式有以下幾點好處:
便於在專案中搜尋檔案
如果你的編輯器支援模糊搜尋,只需要搜尋 UserForm
就可以讓你找到對應的檔案:
如果你想要在目錄樹中搜尋檔案,可以很容易地通過元件的名字定位到它:
可以避免在引入時重複名稱
遵循這種方式,你可以根據元件的上下文環境來命名檔案。想一下上面的 form 元件,我們知道它是一個 User 模組下的 form 元件,但是既然我們已經把 form 元件放在了 User 模組的目錄下,我們就不需要在 form 元件的檔名上重複 user 這個單詞,使用 Form.jsx
就可以了。
我最初使用 React 的時候喜歡用完整的名字來命名檔案,但是這樣會導致相同的部分重複太多次,同時引入時的路徑太長。來看看這兩種方式的區別:
1 2 3 |
import ScreensUserForm from './screens/User/UserForm'; // vs import ScreensUserForm from './screens/User/Form'; |
在上面的例子中,我們看不出來明顯的優勢。但是應用複雜度上升一點時就能夠看到區別了。我們來看看下面這個我實際專案中的例子:
1 2 3 |
import MediaPlanViewChannel from '/MediaPlan/MediaPlanView/MediaPlanViewChannel.jsx'; // vs import MediaPlanViewChannel from './MediaPlan/View/Channel'; |
現在想象一下一個檔名中重複五到十次。
出於這樣的原因,我們認為根據元件檔案的上下文環境以及它的相對路徑來命名是更好的方式。
頁面(Screen)
如果要對一個使用者做增刪改查的操作,我們需要有使用者列表頁面,建立新使用者的頁面以及編輯已有使用者的頁面。
在應用中,通過使用元件相互組合的結果,就是一個頁面。理想狀態下,頁面應該不包含任何邏輯,而僅僅是一個函式式元件。
我們以 src
目錄為根目錄,將不同頁面分散在不同資料夾中。因為它們是根據路由定義而不是模組來劃分成組的。
1 2 3 4 5 6 7 |
src ├─ components └─ screens └─ User ├─ Form.jsx └─ List.jsx |
假設我們專案中在使用 react-router,我們在 screens 目錄下放置 Root.jsx 檔案,並且在其中定義我們應用所有的路由。
Root.jsx 的程式碼可能像下面這樣:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
import React, { Component } from 'react'; import { Router } from 'react-router'; import { Redirect, Route, Switch } from 'react-router-dom'; import ScreensUserForm from './User/Form'; import ScreensUserList from './User/List'; const ScreensRoot = () => ( <Router> <Switch> <Route path="/user/list" component={ScreensUserList} /> <Route path="/user/create" component={ScreensUserForm} /> <Route path="/user/:id" component={ScreensUserForm} /> </Switch> </Router> ); export default ScreensRoot; |
注意我們將所有頁面都放在同一個目錄下,這個目錄以路由名稱命名。嘗試為每個父級路由建立一個目錄,在這個目錄中組織子路由。在這個示例中,我們建立了 User 目錄並且將 List 頁面和 Form 頁面放在裡面。這種方式使你看一眼 url 就能夠輕鬆定位當前路由渲染的頁面。
像上面的例子中的建立和編輯一個使用者的路由一樣,一個頁面可能會被兩個不同的路由渲染使用。
你可能注意到了所有的元件都包含 Screen 作為名稱的字首。當元件在元件目錄外使用時,我們需要使用它們相對於 src 目錄的路徑來命名。位於 src/screens/User/List.jsx
的元件應該被命名為 ScreensUserList。
包括 Root.jsx 在內,我們的目錄結構如下:
1 2 3 4 5 6 7 8 |
src ├─ components └─ screens ├─ User │ ├─ Form.jsx │ └─ List.jsx └─ Root.jsx |
別忘了在 index.js 中引入作為應用根元件的 Root.jsx 。
如果你對一個頁面長什麼樣子還有疑問,看看下面的示例,它就是使用者表單的頁面。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
import React from 'react'; import UserForm from '../../components/User/Form/Form'; const ScreensUserForm = ({ match: { params } }) => ( <div> <h1> {`${!params.id ? 'Create' : 'Update'}`} User </h1> <UserForm id={params.id} /> </div> ); export default ScreensUserForm; |
最終,我們應用的目錄結構會像下面這樣:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
src ├─ components │ ├─ User │ │ ├─ Form │ │ │ ├─ Form.jsx │ │ │ └─ Form.css │ │ └─ List.jsx │ └─ UI │ └─ screens ├─ User │ ├─ Form.jsx │ └─ List.jsx └─ Root.jsx |
回顧要點
- 展示型和容器元件放在 src/components 目錄下
- 通過模組/特性(module/feature)的方式組織元件
- 基礎的 UI 元件放在 src/components/UI 目錄下
- 保持頁面簡單,使用最簡潔的結構和程式碼
- 通過路由定義組織頁面。對於 /user/list 路由地址來說,我們會有一個頁面在 /src/screens/User/List.jsx。
- 元件由相對 components 或 src 的路徑命名,就是說,處於 src/components/User/List.jsx 位置的元件會被命名為 UserList。處於 src/screens/User/List.jsx 位置的元件會被命名為 ScreensUserList
- 元件和目錄同名時,不要在使用元件的時候重複這個名字。考慮這樣一個場景,處於 src/components/User/List/List.jsx 位置的元件會被命名為 UserList 而不是 UserListList。