大家好,我叫CUGGZ。在面向对象编程(OOP)中,SOLID原则是设计模式的基础,它的每一个字母代表一个设计原则:单一职责原则(SRP)开闭原则(OCP)里氏替换原则(LSP)接口隔离原则(ISP)依赖倒置原则(DIP)让我们来看看每个原则是什么意思,以及如何在React中应用SOLID原则!1.单一职责原则(SRP)单一职责原则的定义是每个类应该只有一个职责,即只能做一件事。这个原则最容易解释,因为我们可以简单地理解为“每个功能/模块/组件应该只做一件事”。在所有这些原则中,单一职责原则是最容易遵循的,也是最有影响力的,因为它极大地提高了代码的质量。为了保证组件只做一件事,可以这样:将功能较多的大组件拆分成较小的组件。将与组件功能无关的代码提取到单独的函数中。将相关功能提取到自定义Hooks中。让我们看一个显示活跃用户列表的组件:response=awaitfetch('/some-api')constdata=awaitresponse.json()setUsers(data)}loadUsers()},[])constweekAgo=newDate();weekAgo.setDate(weekAgo.getDate()-7);返回(
{users.filter(user=>!user.isBanned&&user.lastActivityAt>=weekAgo).map(user=>{user.fullName}
{user.role})}
)}这个组件虽然代码不多,但是做很多事情:获取数据、过滤数据、渲染数据。让我们看看如何分解它。首先,只要useState和useEffect都用到,就可以提取到自定义的Hook中:constuseUsers=()=>{const[users,setUsers]=useState([])useEffect(()=>{constloadUsers=async()=>{constresponse=awaitfetch('/some-api')constdata=awaitresponse.json()setUsers(data)}loadUsers()},[])return{users}}constActiveUsersList=()=>{const{users}=useUsers()constweekAgo=newDate()weekAgo.setDate(weekAgo.getDate()-7)return(
{users.filter(user=>!user.isBanned&&user.lastActivityAt>=weekAgo).map(user=>{user.fullName}
{user.role})}
)}现在,useUsersHook只关心一件事——从API获取用户。它使我们的组件代码更具可读性。接下来查看组件渲染的JSX。每当我们遍历一个对象数组时,我们应该意识到它为每个数组项生成的JSX的复杂性。如果它是一个没有附加任何事件处理程序的单行代码,那么将它保持内联是完全可以的。但是对于更复杂的JSX,将它提取到一个单独的组件中可能是一个更好的主意:constUserItem=({user})=>{return(
{user.fullName}
{user.role})}constActiveUsersList=()=>{const{users}=useUsers()constweekAgo=newDate()weekAgo.setDate(weekAgo.getDate()-7)return(
{users.filter(user=>!user.isBanned&&user.lastActivityAt>=weekAgo).map(user=>)}
)}这里将渲染用户信息的逻辑提取到一个单独的组件中,使我们的组件更小,更易读。最后,API获取的用户列表中过滤掉所有非活跃用户的逻辑比较独立,可以在其他部分复用,所以可以抽取出一个public函数:constgetOnlyActive=(users)=>{constweekAgo=newDate()weekAgo.setDate(weekAgo.getDate()-7)returnusers.filter(user=>!user.isBanned&&user.lastActivityAt>=weekAgo)}constActiveUsersList=()=>{const{用户}=useUsers()return(
{getOnlyActive(users).map(user=>)}
)}至此,通过拆解以上三步,组件就变得比较简单了。不过仔细一看,这个组件还是有优化空间的。目前组件是先取数据,然后需要过滤数据。理想情况下,我们只想获取数据并渲染它而无需任何额外操作。因此,这个逻辑可以封装成一个新的自定义Hook,最终代码如下://getdataconstuseUsers=()=>{const[users,setUsers]=useState([])useEffect(()=>{constloadUsers=async()=>{constresponse=awaitfetch('/some-api')constdata=awaitresponse.json()setUsers(data)}loadUsers()},[])return{users}}//列表渲染constUserItem=({user})=>{return(
{user.fullName}
{user.role})}//列表过滤器constgetOnlyActive=(users)=>{constweekAgo=newDate()weekAgo.setDate(weekAgo.getDate()-7)returnusers.filter(user=>!user.isBanned&&user.lastActivityAt>=weekAgo)}constuseActiveUsers=()=>{const{users}=useUsers()constactiveUsers=useMemo(()=>{returngetOnlyActive(users)},[users])返回{activeUsers}}constActiveUsersList=()=>{const{activeUsers}=useActiveUsers()return(
{activeUsers.map(user=>)}
)}这里我们创建useActiveUsersHook来处理获取和过滤数据的逻辑,而组件只做最少的事情——渲染它从Hook获取的数据现在,这个组件只有两个职责:获取数据和渲染数据,当然我们也可以在父类中component获取数据,通过props传递给组件,这样组件只需要渲染即可。当然,还是要视情况而定。我们可以简单地认为获取和渲染数据是“一件事”。总而言之,遵循单一职责原则,我们有效地将大量独立的代码进行了模块化,模块化的代码更易于测试和维护。2.开闭原则(OCP)开闭原则指出“一个软件实体(类、模块、函数)应该对扩展开放,对修改关闭”。开闭原则提倡以一种允许在不更改源代码的情况下扩展组件的方式来构建组件。让我们看一个场景。有一个Header组件,可以在不同的页面上使用。根据页面的不同,Header组件的UI应该略有不同:constHeader=()=>{const{pathname}=useRouter()return(
{pathname==='/dashboard'&&创建事件}{pathname==='/'&&转到仪表板}操作>)}constHomePage=()=>(<>
>)constDashboardPage=()=>(<>
>)此处,根据页面的不同,显示指向不同页面组件的链接。现在想一想,如果需要在更多的页面中添加这个Header组件会怎样?每次创建新页面时,都需要引用Header组件并修改其内部实现。这种做法使得Header组件与使用它的上下文紧密耦合,违反了开闭原则。为了解决这个问题,我们可以使用组件组合。Header组件不需要关心它将在内部呈现什么,而是可以将此责任委托给将使用children属性的组件:constHeader=({children})=>(
)constHomePage=()=>(<>
>)constDashboardPage=()=>(<>
>)使用此方法,我们彻底去掉了Header组件内部的变量逻辑。现在您可以使用组合将任何您想要的内容放入Header中,而无需修改组件本身。遵循开闭原则,可以降低组件之间的耦合度,提高可扩展性和复用性。3.LiskovSubstitutionPrinciple(LSP)LiskovSubstitutionPrinciple可以理解为对象之间的一种关系,子类型对象应该可以被超类型对象替换。这个原则在很大程度上依赖于类继承来定义超类型和子类型关系,但它可能不适用于React,因为我们几乎从不处理类,更不用说类继承了。虽然远离类继承不可避免地会将这一原则完全变成其他东西,但使用继承编写React代码会使代码变得糟糕(React团队不推荐继承)。因此,这个原理就不做过多解释了。4.接口隔离原则(ISP)根据接口隔离原则,客户端不应该依赖它不需要的接口。为了更好地说明ISP所针对的问题,让我们看一个呈现视频列表的组件:{items})=>{return(
)}Thumbnail组件的实现如下:typeProps={video:Video}constThumbnail=({video}:Props)=>{return
}Thumbnail组件很小很简单,但是它有一个问题:它期望将完整的视频对象作为道具传入,但只有一个属性(coverUrl)被有效使用。除了视频,我们还需要渲染直播的缩略图,这两种媒体资源会混合在同一个列表中。让我们定义直播流类型LiveStream:typeLiveStream={name:stringpreviewUrl:string}这是更新后的VideoList组件:typeProps={items:Array