單一職責原則(SRP)
在 React 中,組件是構建 UI 的核心單位,而良好的組件設計是保證應用質量和可維護性的關鍵。單一職責原則是一種設計原則,也適用于 React 組件的開發。它強調每個組件應該只關注一個職責,這樣可以提高組件的可維護性、復用性以及降低代碼的復雜度。
什么是單一職責原則
單一職責原則的定義是每個類應該只有一個職責, ? 也就是只做一件事。這個原則是最容易解釋的,因為我們可以簡單地將其理解為“每個功能/模塊/組件都應該只做一件事”。
在 React 組件中,單一職責原則要求將組件的功能分解為更小的部分,每個部分只負責一個特定的職責。這樣做的好處是,當需求發生變化時,只需要修改與該職責相關的部分,而不會影響整個組件。這提高了代碼的可維護性和可測試性。
在所有這些原則中,單一職責原則是最容易遵循的,也是最有影響力的一項,因為它極大提高了代碼的質量。為了確保組件只做一件事,可以這樣:
-
將功能較多的大型組件拆分為較小的組件;
-
將與組件功能無關的代碼提取到單獨的函數中;
-
將有聯系的功能提取到自定義 Hooks 中。
如何應用單一職責原則在 React 中
在 React 中應用單一職責原則主要通過組件的拆分和組合來實現。以下是一些建議:
-
拆分大型組件:當一個組件變得龐大且復雜時,考慮將其拆分為更小的組件,每個組件關注不同的職責。這樣可以提高可維護性和復用性。
-
提取可復用的功能:識別重復使用的功能,將其提取為單獨的組件,并通過 props 進行參數化。這樣可以減少重復代碼,提高復用性。
-
保持組件的簡潔和獨立性:確保每個組件只關注自己的職責,并且不包含與其他職責無關的代碼。這樣可以降低代碼的復雜度和維護成本。
-
使用高階組件(Higher-Order Components):高階組件是一種函數,接受一個組件作為參數,并返回一個新的增強組件。通過使用高階組件,可以將與特定職責相關的邏輯與組件分離,提高代碼的可維護性。
-
使用自定義鉤子(Custom Hooks):自定義鉤子是一種將組件邏輯進行復用的方式。通過將與特定職責相關的邏輯封裝在自定義鉤子中,可以使組件更加簡潔和獨立。
下面來看一個顯示活躍用戶列表的組件:
const?ActiveUsersList?=?()?=>?{const?[users,?setUsers]?=?useState([])useEffect(()?=>?{const?loadUsers?=?async?()?=>?{??const?response?=?await?fetch('/some-api')const?data?=?await?response.json()setUsers(data)}loadUsers()},?[])const?weekAgo?=?new?Date();weekAgo.setDate(weekAgo.getDate()?-?7);return?(<ul>{users.filter(user?=>?!user.isBanned?&&?user.lastActivityAt?>=?weekAgo).map(user?=>?<li?key={user.id}><img?src={user.avatarUrl}?/><p>{user.fullName}</p><small>{user.role}</small></li>)}</ul>????)
}
這個組件雖然代碼不多,但是做了很多事情:獲取數據、過濾數據、渲染數據。來看看如何分解它。
首先,只要同時使用了 ?useState
? 和 ?useEffect
,就可以將它們提取到自定義 Hook 中:
const?useUsers?=?()?=>?{const?[users,?setUsers]?=?useState([])useEffect(()?=>?{const?loadUsers?=?async?()?=>?{??const?response?=?await?fetch('/some-api')const?data?=?await?response.json()setUsers(data)}loadUsers()},?[])return?{?users?}
}const?ActiveUsersList?=?()?=>?{const?{?users?}?=?useUsers()const?weekAgo?=?new?Date()weekAgo.setDate(weekAgo.getDate()?-?7)return?(<ul>{users.filter(user?=>?!user.isBanned?&&?user.lastActivityAt?>=?weekAgo).map(user?=>?<li?key={user.id}><img?src={user.avatarUrl}?/><p>{user.fullName}</p><small>{user.role}</small></li>)}</ul>????)
}
現在,useUsers
?Hook 只關心一件事——從 API 獲取用戶。它使我們的組件代碼更具可讀性。
接下來看一下組件渲染的 JSX。每當我們對對象數組進行遍歷時,都應該注意它為每個數組項生成的 JSX 的復雜性。如果它是一個沒有附加任何事件處理函數的單行代碼,將其保持內聯是完全沒有問題的。但對于更復雜的 JSX,將其提取到單獨的組件中可能是一個更好的主意:
const?UserItem?=?({?user?})?=>?{return?(<li><img?src={user.avatarUrl}?/><p>{user.fullName}</p><small>{user.role}</small></li>)
}const?ActiveUsersList?=?()?=>?{const?{?users?}?=?useUsers()const?weekAgo?=?new?Date()weekAgo.setDate(weekAgo.getDate()?-?7)return?(<ul>{users.filter(user?=>?!user.isBanned?&&?user.lastActivityAt?>=?weekAgo).map(user?=>?<UserItem?key={user.id}?user={user}?/>)}</ul>????)
}
這里將用于呈現用戶信息的邏輯提取到了一個單獨的組件中,從而使我們的組件更小、更可讀。
最后,從 API 獲取到的用戶列表中過濾出所有非活躍用戶的邏輯是相對獨立的,可以在其他部分重用,所以可以將其提取到一個公共函數中:
const?getOnlyActive?=?(users)?=>?{const?weekAgo?=?new?Date()weekAgo.setDate(weekAgo.getDate()?-?7)return?users.filter(user?=>?!user.isBanned?&&?user.lastActivityAt?>=?weekAgo)
}const?ActiveUsersList?=?()?=>?{const?{?users?}?=?useUsers()return?(<ul>{getOnlyActive(users).map(user?=>?<UserItem?key={user.id}?user={user}?/>)}</ul>????)
}
到現在為止,通過上面三步拆解,組件已經變得比較簡單。但是,仔細觀察會發現,這個組件還有優化的空間。目前,組件首先獲取數據,然后需要對數據進行過濾。理想情況下,我們只想獲取數據并渲染它,而不需要任何額外的操作。所以,可以將這個邏輯封裝到一個新的自定義 Hook 中,最終的代碼如下:
//?獲取數據
const?useUsers?=?()?=>?{const?[users,?setUsers]?=?useState([])useEffect(()?=>?{const?loadUsers?=?async?()?=>?{??const?response?=?await?fetch('/some-api')const?data?=?await?response.json()setUsers(data)}loadUsers()},?[])return?{?users?}
}//?列表渲染
const?UserItem?=?({?user?})?=>?{return?(<li><img?src={user.avatarUrl}?/><p>{user.fullName}</p><small>{user.role}</small></li>)
}//?列表過濾
const?getOnlyActive?=?(users)?=>?{const?weekAgo?=?new?Date()weekAgo.setDate(weekAgo.getDate()?-?7)return?users.filter(user?=>?!user.isBanned?&&?user.lastActivityAt?>=?weekAgo)
}const?useActiveUsers?=?()?=>?{const?{?users?}?=?useUsers()const?activeUsers?=?useMemo(()?=>?{return?getOnlyActive(users)},?[users])return?{?activeUsers?}
}const?ActiveUsersList?=?()?=>?{const?{?activeUsers?}?=?useActiveUsers()return?(<ul>{activeUsers.map(user?=>?<UserItem?key={user.id}?user={user}?/>)}</ul>????)
}
在這里,我們創建了useActiveUsers
?Hook 來處理獲取和過濾數據的邏輯,而組件只做了最少的事情——渲染它從 Hook 中獲取的數據。
現在,這個組件只剩下兩個職責:獲取數據和渲染數據,當然我們也可以在組件的父級獲取數據,并通過 props 傳入該組件,這樣只需要渲染組件就可以了。當然,還是要視情況而定。我們可以簡單地將獲取并渲染數據看作是“一件事”。
總結
使用單一職責原則可以獲得以下好處:
-
提高可維護性:將組件分解為小的職責模塊,使其更易于理解和修改。當需要更新或修復特定功能時,可以更快地定位和處理相關代碼。
-
提高復用性:通過將職責分離為獨立的模塊,可以更好地重用組件的不同部分。這樣可以減少重復代碼,提高開發效率。
-
降低代碼耦合:職責分離使得組件之間的依賴關系更清晰。當一個組件只關注一個職責時,它變得更加獨立,減少了與其他組件的耦合性。
通過應用單一職責原則,我們可以優化 React 組件的設計,提高其可維護性和復用性。將組件分解為小的職責模塊,提取可復用的功能,保持組件的簡潔和獨立性,使用高階組件和自定義鉤子等技術,都是實現單一職責原則的有效方法。
總而言之,遵循單一職責原則,我們有效地采用了大量獨立的代碼并使其更加模塊化,模塊化的代碼更容易測試和維護。