在React中应用SOLID原则的方法

目录
  • 1、单一职责原则(SRP)
  • 2、开放封闭原则(OCP)
  • 3、里氏替换原则(LSP)
  • 4、接口隔离原则(ISP)
  • 5、依赖倒置原则(DIP)
  • 6、小结

在面向对象编程(OOP)中,SOLID 原则是设计模式的基础,它的每个字母代表一种设计原则:

  • 单一职责原则(SRP)
  • 开放封闭原则(OCP)
  • 里氏替换原则(LSP)
  • 接口隔离原则(ISP)
  • 依赖倒置原则(DIP)

下面就来看看每个原则的含义以及如何在 React 中应用 SOLID 原则!

1、单一职责原则(SRP)

单一职责原则的定义是每个类应该只有一个职责, 也就是只做一件事。这个原则是最容易解释的,因为我们可以简单地将其理解为“每个功能/模块/组件都应该只做一件事”。

在所有这些原则中,单一职责原则是最容易遵循的,也是最有影响力的一项,因为它极大提高了代码的质量。为了确保组件只做一件事,可以这样:

  • 将功能较多的大型组件拆分为较小的组件。
  • 将与组件功能无关的代码提取到单独的函数中。
  • 将有联系的功能提取到自定义 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 传入该组件,这样只需要渲染组件就可以了。当然,还是要视情况而定。我们可以简单地将获取并渲染数据看作是“一件事”。

总而言之,遵循单一职责原则,我们有效地采用了大量独立的代码并使其更加模块化,模块化的代码更容易测试和维护。

2、开放封闭原则(OCP)

开放封闭原则指出“ 一个软件实体(类、模块、函数)应该对扩展开放,对修改关闭 ”。开放封闭原则主张以一种允许在不更改源代码的情况下扩展组件的方式来构造组件。

下面来看一个场景,有一个可以在不同页面上使用的 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中,而无需修改组件本身。

遵循开放封闭原则,可以减少组件之间的耦合,使它们更具可扩展性和可重用性。

3、里氏替换原则(LSP)

里氏替换原则可以理解为 对象之间的一种关系,子类型对象应该可以替换为超类型对象 。这个原则严重依赖类继承来定义超类型和子类型关系,但它在 React 中可能不太适用,因为我们几乎不会处理类,更不用说类继承了。虽然远离类继承会不可避免地将这一原则转变为完全不同的东西,但使用继承编写 React 代码会使代码变得糟糕(React 团队不推荐使用继承)。因此,对于这一原则不再过多解释。

4、接口隔离原则(ISP)

根据接口隔离原则的说法,客户端不应该依赖它不需要的接口。为了更好的说明 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>
  )
}

接口隔离原则主张最小化系统组件之间的依赖关系,使它们的耦合度降低,从而提高可重用性。

5、依赖倒置原则(DIP)

依赖倒置原则指出“要依赖于抽象,不要依赖于具体”。换句话说,一个组件不应该直接依赖于另一个组件,而是它们都应该依赖于一些共同的抽象。这里,“组件”是指应用程序的任何部分,可以是 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 原则中反复出现的关键词——从最小化单个组件的职责范围到最小化它们之间的依赖关系等等。

6、小结

通过上面的示例,相信你已经对如何在 React 中使用 SOLID 原则有了一定的了解。应用SOLID 原则使我们的 React 代码更易于维护和健壮。

但是需要注意,虔诚地遵循这些原则可能会造成破坏并导致代码过度设计,因此我们应该学会识别对组件的进一步分解或解耦何时会导致其复杂度增加而几乎没有任何好处。

到此这篇关于如何在React中应用SOLID原则?的文章就介绍到这了,更多相关React SOLID原则内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 教你应用 SOLID 原则整理 React 代码之单一原则

    目录 什么是单一责任原则? 让我们从一个糟糕的例子开始 1. 移动数据处理逻辑 2. 可重用的数据获取钩子 3. 分解 UI 组件 让我们回顾一下我们刚刚做了什么 总结 SOLID 原则的主要是作为关心自己工作的软件专业人员的指导方针,那些以经得起时间考验的设计精美的代码库为荣的人. 今天,我们将从一个糟糕的代码示例开始,应用 SOLID 的第一个原则,看看它如何帮助我们编写小巧.漂亮.干净的并明确责任的 React 组件,. 什么是单一责任原则? 单一责任原则告诉我们的是,每个类或组件应该有一

  • 在React中应用SOLID原则的方法

    目录 1.单一职责原则(SRP) 2.开放封闭原则(OCP) 3.里氏替换原则(LSP) 4.接口隔离原则(ISP) 5.依赖倒置原则(DIP) 6.小结 在面向对象编程(OOP)中,SOLID 原则是设计模式的基础,它的每个字母代表一种设计原则: 单一职责原则(SRP) 开放封闭原则(OCP) 里氏替换原则(LSP) 接口隔离原则(ISP) 依赖倒置原则(DIP) 下面就来看看每个原则的含义以及如何在 React 中应用 SOLID 原则! 1.单一职责原则(SRP) 单一职责原则的定义是每个

  • 如何应用 SOLID 原则在 React 中整理代码之开闭原则

    目录 本系列其他文章 什么是开闭原则? 让我们从一个例子开始 一个糟糕的解决方案 解决方案是什么? 让我们创建单独的用户组件 注意 总结 SOLID 是一套原则.它们主要是关心代码质量和可维护性的软件专业人员的指导方针. React 不是面向对象,但这些原则背后的主要思想可能是有帮助的.在本文中,我将尝试演示如何应用这些原则来编写更好的代码. 在前一篇文章中,我们讨论了单一责任原则.今天,我们将讨论 SOLID 的第二个原则: 开闭原则. 本系列其他文章 如何应用 SOLID 原则在 React

  • React中使用antd组件的方法

    目录 antd 使用antd antd antd 是基于 Ant Design 设计体系的 React UI 组件库,主要用于研发企业级中后台产品. 国内镜像为:https://ant-design.gitee.io/docs/react/introduce-cn速度很快 进入网页点击组件就可以看到网页需要的各种配件,如按钮.导航栏等等,并且配有各种使用方法的API,目前已经更新到4.22版本,原本是只支持react不过后来也支持vue了.3.几版本的文档说明会更加详细antd还可以更改主题颜色

  • React中jquery引用的实现方法

    在React中引用Jquery比较好玩,获取元素的数据更多 1.引入方法举例: import $ from 'jquery'; import { Button } from 'antd'; class testJquery extends React.Component { constructor(props) { super(props); this.selectElement = this.selectElement.bind(this); } render() { return( <div

  • react中使用swiper的具体方法

    正文 最近的react项目需要使用轮播图,自然而然的就想到了swiper,一直想通过npm安装的方式来使用,但是网上找了很多,资料很少,于是就暂时通过在index.html里直接引用swiper的js和css文件的方式来加载,下面来说一下具体的步骤和使用方法. 首先说一下我这里使用的是swiper3x系列.接下来说具体的步骤: 在index.html中引入js和css文件 <!DOCTYPE html> <html lang="en"> <head>

  • 深入理解React中es6创建组件this的方法

    首发于:https://mingjiezhang.github.io/. 在JavaScript中,this对象是运行时基于函数的执行环境(也就是上下文)绑定的. 从react中的demo说起 Facebook最近一次更新react时,将es6中的class加入了组件的创建方式当中.Facebook也推荐组件创建使用通过定义一个继承自 React.Component 的class来定义一个组件类.官方的demo: class LikeButton extends React.Component

  • React中this丢失的四种解决方法

    发现问题 我们在给一个dom元素绑定方法的时候,例如: <input type="text" ref="myinput" accept = "image/*" onChange = {this.selectFile} /> React组件中不能获取refs的值,页面报错提示:Uncaught TypeError: Cannot read property 'refs' of null or undefind 小栗子 import Re

  • React中使用UEditor百度富文本的方法

    前言 本文将介绍笔者在React的项目中使用百度的富文本编辑器Ueditor的过程.注意本文不提供一条龙式的使用方法,只是将使用过程中的一些实现思路进行总结,供以参考.react项目中导入ueditor,会存在各种不正交的问题,需要注意. 引入 首先在ueditor官网下载最新安装包,然后在项目入口的html中导入(导入方式不一,可以采用import的方式,需要自行度娘.但是无论哪种引入方式,只要想自定义功能,不正交问题就难以避免QAQ).不管三七二十一先跑起来再说.. <!DOCTYPE HT

  • 详解React中传入组件的props改变时更新组件的几种实现方法

    我们使用react的时候常常需要在一个组件传入的props更新时重新渲染该组件,常用的方法是在componentWillReceiveProps中将新的props更新到组件的state中(这种state被成为派生状态(Derived State)),从而实现重新渲染.React 16.3中还引入了一个新的钩子函数getDerivedStateFromProps来专门实现这一需求.但无论是用componentWillReceiveProps还是getDerivedStateFromProps都不是

随机推荐