β

【译】如何构建React组件?

JDC | 京东设计中心 5 阅读

原文: https://reallifeprogramming.com/how-to-structure-components-in-react-54fc43e71546

编程是一项非常复杂的工程,尤其要编写干净整洁的代码更为困难。我们需要考虑很多问题—变量命名、函数作用域、异常处理、安全保障、性能监控等等。在编程中,变量命名唯一还是一件比较困难的事情,我倾向于编写松散耦合且高度聚合的组件。如果从面向对象或者函数式编程的角度来说,也会遇到同样的问题。构建体系是我认为最难的事情,因为它将影响到整个项目。想要成为能熟练设计软件架构的人,需要经过多年磨砺和积累。(也许没有人能完全掌握它— 在如此快速发展的行业中,总有人走在前面,也总有一个方法可以提高软件设计)。

我非常喜欢使用React,因为我觉得它最大优点就是足够简单。 在简单和容易之间还是存在区别 的,我的意思是React也很简单。当然你需要些时间来了解它,当你掌握其核心内容后,其他的事都是水到渠成的了。下文将介绍比较难的部分。

耦合&内聚

这些指标(耦合&内聚)或多或少的给我们改变编程习惯带了挑战。它们经常被运用在类形式的面向对象编程中。我们也将参考并且运用同样的规则在编写React组件上。

耦合指元素之间的相互连接和依赖关系。如果你改变一个元素需要同步的更新另外一个元素,我们称此为紧密耦合。而松散耦合指的是改变一个元素时,并不需要改变另外一个元素。举个例子,显示银行转账金额功能。如果展示金额依赖于汇率计算,那么内部转换结构更改时,展示的代码也会被更新。如果我们设计基于一个元素接口的,松散耦合的系统,这样元素的改变并不会影响视图层展示。很明显,松散耦合的组件更易于管理和控制。

内聚则是组件是否只为一件事情负责。这个指标是依循单一原则和unix原则:专注一件事情并且做好这件事。如果账户余额格式化展示需要计算相关汇率和检查是否有查阅历史权限,那么这个包含很多功能职责,而这些功能并不相互依赖。也许,权限处理和汇率应该是不同的组件。另一方面,如果有多个组件,一个用于整数部分,一个用于小数部分,另外一个用于货币展示,程序员想展示余额,他们则需要找到所有组件进行组装。其中的挑战则是创造高度内聚的组件。

构建组件

创建组件有很多种方法。 我们希望在合理程度下组件是可以被重用的 。我们也希望构建的小组件可以被用在更大的组件中。理想情况下,我们想构建松散耦合和高度聚合的组件,这样我们的系统更有利于管理和扩展。在React组件中props 类似函数中的参数,它们也可以看做是无状态功能的组件。我们需要思考在组件中如何定义props和组件如何被重用。

接下来,我们以费用管理系统为背景,分析费用详细的格式,来介绍如果构建组件:

type Expense {
      description: string
      category: string
      amount: number
      doneAt: moment
    }

根据模型,会有以下几种对费用格式的程序建模方式:

下面分别讨论使用上述传递方式的优缺点。不过需要时刻关注使用以上任何方式是要看使用场景和依赖系统的。这也是我们所做的,建立适当的抽象场景。

无props

这是最简单的解决办法,往往就是构建一个写静态数据的组件。

const ExpenseDetails = () => (
      <div className='expense-details'>
         <div>Category: <span>Food</span></div>
         <div>Description: <span>Lunch</span></div>
         <div>Amount: <span>10.15</span></div>
         <div>Date: <span>2017-10-12</span></div>
      </div>
    )

不传递props,就不会给我们带来任何灵活性,而且组件也只能用于单一的场景。在费用明细的例子中,我们可以看到,最初组件是需要接受一些props。不过在某些场景下,没有任何props也是一个好的解决方式。首先,我们可以使用一些组件,其props的内容是一些不会轻易更改的内容,比如:商标,logo或者公司信息等。

const Logo = () => (
      <div className='logo'>
       <img src="https://jdc.jd.com/logo.png" alt='DayOne logo'/>
      </div>
    )

编写尽可能小的组件使得系统更加容易维护。保持信息只保存在一处而且只需要在一处进行修改。不要在多处写重复的代码。

传递expense对象

在费用明细确定情况下,我们需要给组件传递数据。首先,我们需要传递一个expense对象。

const ExpenseDetails = ({ expense }) => (
      <div className='expense-details'>
         <div>Category: <span>{expense.category}</span></div>
         <div>Description: <span>{expense.description}</span></div>
         <div>Amount: <span>{expense.amount}</span></div>
         <div>Date: <span>{expense.doneAt}</span></div>
      </div>
    )

传递费用对象给费用明细的组件是非常有意义的。费用明细的格式是高度一致的,它显示费用的数据。无论什么时候需要改变格式,这是唯一可以修改的地方。改变费用明细的格式也不会对费用对象自身带来什么副作用。

这个组件是和费用对象紧密耦合,这是一个坏的事情吗?当然不是,但是我们必须意识到这是如何影响我们系统的。 传递一个对象作为props,费用明细的组件将会依赖费用内部结构。当我们改变费用的内部结构时候,我们将需要更改费用明细组件。当然,我们只需要在一处进行修改。

这样的设计如何适应未来的改变呢? 如果我们增加、改变或者删除一个字段,我们将只需改变一个组件。如果我们需要增加一个其他的日历格式化展示?我们可以为日历格式化增加一个新的prop。

const ExpenseDetails = ({ expense, dateFormat }) => (
      <div className='expense-details'>
         <div>Category: <span>{expense.category}</span></div>
         <div>Description: <span>{expense.description}</span></div>
         <div>Amount: <span>{expense.amount}</span></div>
         <div>Date: <span>{expense.doneAt.format(dateFormat)}</span></div>
      </div>
    )

我们开始增加属性来使组件更加灵活。如果只有几个选项,那么一切都是很ok的。系统业务开始扩展后问题就来了,在不同的场景下我们需要维护大量的props。

const ExpenseDetails = ({ expense, dateFormat, withCurrency, currencyFormat, isOverdue, isPaid ... })

增加props可以使得组件重用性更好,但你可能设计了多重功能职责的组件。这种规则也同样在函数写法中运用。可以用多个参数来创建一个函数,当参数的数目超过3个或者4个时候,意味着这个函数在做很多事情了,也许这时候应该将函数拆成更小的函数来的更加简单。

随着组件props的增加,我们将其拆分成定义明确的组件,比如:OverdueExpenseDetails, PaidExpenseDetails等。

只传递必要的属性

为了减少对象自身的内容,我们可以只传递必要的属性值。

const ExpenseDetails = ({ category, description, amount, date }) => (
      <div className='expense-details'>
         <div>Category: <span>{category}</span></div>
         <div>Description: <span>{description}</span></div>
         <div>Amount: <span>{amount}</span></div>
         <div>Date: <span>{date}</span></div>
      </div>
    )

我们分别传递属性,这样我们将一部分工作责任转移给了组件使用者。如果费用的内部结构发生变化,他将不会影响费用明细的格式化。但可能影响每个使用组件的地方,因为我们可能需要修改props。当我们以独立的属性传递props时候,一个组件将更加抽象了。

只传递需要的字段对未来设计改动是如何影响的?增加、更新或者删除字段将不会很容易。无论何时我们要增加一个字段,我们不仅仅要改变费用细节的实现,也需要改变每个使用组件的地方。另外一个方面,支持多种日历格式化几乎是现成的,我们可以传递日历作为prop,也可以传递格式化后的日历。

<ExpenseDetails 
      category={expense.category} 
      description={expense.description}
      amount={expense.amount}
      date={expense.doneAt.format('YYYY-MM-DD')}
    />

决定如何展示特定的字段应该在掌握在具体使用组件的人手中,这将不是费用明细组件关心的内容。

传递map或者array的属性

为了达到组件抽象化,我们可以传递一个map的属性值。

const ExpenseDetails = ({ expense }) => (
      <div class='expense-details'>
      {
        _.reduce(expense, (acc, value, key) => {
          acc.push(<div>{key}<span>{value}</span></div>)
        }, [])
      }
      </div>
    )

使用组件的人控制费用明细的格式化,传递给组件的对象格式则必须正确。

const expense = {
      "Category": "Food",
      "Description": "Lunch",
      "Amount": 10.15,
      "Date": 2017-10-12
    }

这个方案有很多缺陷,我们很难控制组件展示的样式,并且展示顺序也没有指定。因此,如果我们需要某种顺序的话,可以采用array代替map来解决这个问题。但是仍然还有缺陷。

传递map 和array作为props 不与费用耦合,也根本与它不一致。增加和删除新属性虽然只改变了prop,但是我们无法控制组件本身的格式。如果我们只改变类别的格式化,这不是一个可行的办法。(确切地说,总有一个办法来解决,例如,传递另外一个格式化后的props。这个解决方案似乎不再简单了。)

传递一个格式化的子对象

我们也可以只通过直接传对一个子对象,这样就能考虑更少的组件内需要如何展示。

const ExpenseDetails = ({ children }) => (
      <div class='expense-details'>
        { children }
      </div>
    )

在这种情况下,费用明细只是一个提供结构和样式的容器。展示所有信息则是使用组件的人必须提供的。

<ExpenseDetails>
      <div>Category: <span>{expense.category}</span></div>
      <div>Description: <span>{expense.description}</span></div>
      <div>Amount: <span>{expense.amount}</span></div>
      <div>Date: <span>{expense.doneAt}</span></div>
    </ExpenseDetails>

在费用明细这个案例中,我们需要重复许多工作,因此这也许不是一个好的解决方案。尽管如此,灵活性则是巨大的,因为有可能有很多不同的格式化操作。增删改只需要改变使用组件时候传入的值。日期格式也是一样的,我们虽然失去了功能内聚的特点,但这也是我们不得不付出的代价。

环境为王

正如你所看到,我们讨论了它们的不同优缺点和可能性。哪一个最好呢,这取决于:

没有一个万能的解决方案,一个方案也并不能适用所有场景。我们如何构建组件对于系统的维护和系统可扩展方面有着深远的影响。这完全依赖于组件所使用的环境。非常幸运的是,我们有很多可使用的方案。组件是功能的抽象集合,它既能构建小系统也能构建大系统。所以这仅仅只是一个选择问题。

作者:JDC | 京东设计中心
京东设计中心
原文地址:【译】如何构建React组件?, 感谢原作者分享。

发表评论