您好,登錄后才能下訂單哦!
本篇內容介紹了“在React中怎么應用SOLID原則”的有關知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領大家學習一下如何處理這些情況吧!希望大家仔細閱讀,能夠學有所成!
單一職責原則的定義是每個類應該只有一個職責, 也就是只做一件事。這個原則是最容易解釋的,因為我們可以簡單地將其理解為“每個功能/模塊/組件都應該只做一件事”。
在所有這些原則中,單一職責原則是最容易遵循的,也是最有影響力的一項,因為它極大提高了代碼的質量。為了確保組件只做一件事,可以這樣:
將功能較多的大型組件拆分為較小的組件。
將與組件功能無關的代碼提取到單獨的函數中。
將有聯系的功能提取到自定義 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 傳入該組件,這樣只需要渲染組件就可以了。當然,還是要視情況而定。我們可以簡單地將獲取并渲染數據看作是“一件事”。
總而言之,遵循單一職責原則,我們有效地采用了大量獨立的代碼并使其更加模塊化,模塊化的代碼更容易測試和維護。
開放封閉原則指出“ 一個軟件實體(類、模塊、函數)應該對擴展開放,對修改關閉 ”。開放封閉原則主張以一種允許在不更改源代碼的情況下擴展組件的方式來構造組件。
下面來看一個場景,有一個可以在不同頁面上使用的 Header 組件,根據所在頁面的不同,Header 組件的 UI 應該有略微的不同:
const Header = () => { const { pathname } = useRouter() return ( <header> <Logo /> <Actions> {pathname === '/dashboard' && <Link to="/events/new">Create event</Link>} {pathname === '/' && <Link to="/dashboard">Go to dashboard</Link>} </Actions> </header> ) } const HomePage = () => ( <> <Header /> <OtherHomeStuff /> </> ) const DashboardPage = () => ( <> <Header /> <OtherDashboardStuff /> </> )
這里,根據所在頁面的不同,呈現指向不同頁面組件的鏈接。那現在考慮一下,如果需要將這個Header組件添加到更多的頁面中會發生什么呢?每次創建新頁面時,都需要引用 Header 組件,并修改其內部實現。這種方式使得 Header 組件與使用它的上下文緊密耦合,并且違背了開放封閉原則。
為了解決這個問題,我們可以使用組件組合。Header 組件不需要關心它將在內部渲染什么,相反,它可以將此責任委托給將使用 children 屬性的組件:
const Header = ({ children }) => ( <header> <Logo /> <Actions> {children} </Actions> </header> ) const HomePage = () => ( <> <Header> <Link to="/dashboard">Go to dashboard</Link> </Header> <OtherHomeStuff /> </> ) const DashboardPage = () => ( <> <Header> <Link to="/events/new">Create event</Link> </Header> <OtherDashboardStuff /> </> )
使用這種方法,我們完全刪除了 Header 組件內部的變量邏輯。現在可以使用組合將想要的任何內容放在Header中,而無需修改組件本身。
遵循開放封閉原則,可以減少組件之間的耦合,使它們更具可擴展性和可重用性。
里氏替換原則可以理解為 對象之間的一種關系,子類型對象應該可以替換為超類型對象 。這個原則嚴重依賴類繼承來定義超類型和子類型關系,但它在 React 中可能不太適用,因為我們幾乎不會處理類,更不用說類繼承了。雖然遠離類繼承會不可避免地將這一原則轉變為完全不同的東西,但使用繼承編寫 React 代碼會使代碼變得糟糕(React 團隊不推薦使用繼承)。因此,對于這一原則不再過多解釋。
根據接口隔離原則的說法,客戶端不應該依賴它不需要的接口。為了更好的說明 ISP 所針對的問題,來看一個呈現視頻列表的組件:
type Video = { title: string duration: number coverUrl: string } type Props = { items: Array<Video> } const VideoList = ({ items }) => { return ( <ul> {items.map(item => <Thumbnail key={item.title} video={item} /> )} </ul> ) }
Thumbnail 組件的實現如下:
type Props = { video: Video } const Thumbnail = ({ video }: Props) => { return <img src={video.coverUrl} /> }
Thumbnail 組件非常小并且很簡單,但它有一個問題:它希望將完整的視頻對象作為 props 傳入,但是僅有效地使用其屬性之一(coverUrl)。
除了視頻,我們還需要渲染直播的縮略圖,這兩種媒體資源會混合在同一個列表中。
下面來定義直播的類型 LiveStream :
type LiveStream = { name: string previewUrl: string }
這是更新后的 VideoList 組件:
type Props = { items: Array<Video | LiveStream> } const VideoList = ({ items }) => { return ( <ul> {items.map(item => { if ('coverUrl' in item) { return <Thumbnail video={item} /> } else { // 直播組件,該怎么寫? } })} </ul> ) }
這時就發現一個問題,我們可以輕松的區分視頻和直播對象,但是不能將后者傳遞給Thumbnail組件,因為Video和LiveStream類型不兼容。它們包含了不同的屬性來保存縮略圖:視頻對象調用coverUrl,直播對象調用previewUrl。這就是使組件依賴了比實際更多的props的原因所在。
下面來重構 Thumbnail 組件以確保它僅依賴于它需要的props:
type Props = { coverUrl: string } const Thumbnail = ({ coverUrl }: Props) => { return <img src={coverUrl} /> }
通過這樣修改,現在我們可以使用它來渲染視頻和直播的對略圖:
type Props = { items: Array<Video | LiveStream> } const VideoList = ({ items }) => { return ( <ul> {items.map(item => { if ('coverUrl' in item) { return <Thumbnail coverUrl={item.coverUrl} /> } else { return <Thumbnail coverUrl={item.previewUrl} /> } })} </ul> ) }
當然,這段代碼還可以簡化一下:
type Props = { items: Array<Video | LiveStream> } const VideoList = ({ items }) => { return ( <ul> {items.map(item => ( <Thumbnail coverUrl={'coverUrl' in item ? item.coverUrl : item.previewUrl} /> ))} </ul> ) }
接口隔離原則主張最小化系統組件之間的依賴關系,使它們的耦合度降低,從而提高可重用性。
依賴倒置原則指出“要依賴于抽象,不要依賴于具體”。換句話說,一個組件不應該直接依賴于另一個組件,而是它們都應該依賴于一些共同的抽象。這里,“組件”是指應用程序的任何部分,可以是 React 組件、函數、模塊或第三方庫。這個原則可能很難理解,下面來看一個具體的例子。
有一個 LoginForm 組件,它在提交表單時將用戶憑據發送到某些 API:
import api from '~/common/api' const LoginForm = () => { const [email, setEmail] = useState('') const [password, setPassword] = useState('') const handleSubmit = async (evt) => { evt.preventDefault() await api.login(email, password) } return ( <form onSubmit={handleSubmit}> <input type="email" value={email} onChange={e => setEmail(e.target.value)} /> <input type="password" value={password} onChange={e => setPassword(e.target.value)} /> <button type="submit">Log in</button> </form> ) }
在這段代碼中,LoginForm 組件直接引用了 api 模塊,因此它們之間存在緊密耦合。這種依賴關系就會導致一個組件的更改會影響其他組件。依賴倒置原則就提倡打破這種耦合,下面來看看如何實現這一點。
首先,從 LoginForm 組件中刪除對 api 模塊的直接引用,而是允許通過 props 傳入所需的回調函數:
type Props = { onSubmit: (email: string, password: string) => Promise<void> } const LoginForm = ({ onSubmit }: Props) => { const [email, setEmail] = useState('') const [password, setPassword] = useState('') const handleSubmit = async (evt) => { evt.preventDefault() await onSubmit(email, password) } return ( <form onSubmit={handleSubmit}> <input type="email" value={email} onChange={e => setEmail(e.target.value)} /> <input type="password" value={password} onChange={e => setPassword(e.target.value)} /> <button type="submit">Log in</button> </form> ) }
通過這樣修改,LoginForm 組件不再依賴于 api 模塊。向 API 提交憑證的邏輯是通過 onSubmit 回調函數抽象出來的,現在由父組件負責提供該邏輯的具體實現。
為此,創建了一個 ConnectedLoginForm 組件來將表單提交邏輯委托給 api 模塊:
import api from '~/common/api' const ConnectedLoginForm = () => { const handleSubmit = async (email, password) => { await api.login(email, password) } return ( <LoginForm onSubmit={handleSubmit} /> ) }
ConnectedLoginForm 組件充當 api 和 LoginForm 之間的粘合劑,而它們本身保持完全獨立。這樣就可以對這兩個組件進行單獨的修改和維護,而不必擔心修改會影響其他組件。
依賴倒置原則旨在最小化應用程序不同組件之間的耦合。你可能已經注意到,最小化是所有 SOLID 原則中反復出現的關鍵詞——從最小化單個組件的職責范圍到最小化它們之間的依賴關系等等。
“在React中怎么應用SOLID原則”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業相關的知識可以關注億速云網站,小編將為大家輸出更多高質量的實用文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。