本文译自Presentational and Container Components

我发现在写React应用时,有个设计模式非常有用。如果你有React的经验,估计你已经知道了——它就是容器组件这篇文章讲得很清楚了,但我想再补充几点。

有个办法能让组件更易懂、更容易复用,那就是将组件分成两类。我称之为容器组件展示组件,但也有人叫胖组件和瘦组件(Fat and Skinny)、智能组件和非智能组件(Smart and Dumb)、状态组件和纯组件(Stateful and Pure)、画面和组件(Screen and Component)等等。尽管它们并不完全相同,但基本的理念是一致的。

我所说的展示组件如下:

  • 关心组件的外观
  • 内部可能包含展示组件和容器组件,并且通常有自己的DOM标签和自己的样式。
  • 通常可以用this.props.children包含其他组件。
  • 不依赖于app的其他部分,如Flux action或store等。
  • 不需要指定如何加载或操作数据。
  • 通过props明确地接收数据、发起回调。
  • 绝大部分情况下自己没有状态(即使有,也通常是UI状态,而不是数据)
  • 可以写成函数型组件,除非需要状态、生命周期钩子函数,或者需要性能优化等
  • 例如:PageSidebarStoryUserInfoList

容器组件如下:

  • 关心组件如何工作
  • 可以包含展示组件和容器组件,但一般不会有自己的DOM标签(除了一些wrapping div之外),绝对不会有样式
  • 为展示组件或其他容器组件提供数据
  • 调用Flux action并将其作为回调函数提供给展示组件
  • 通常是有状态的(stateful),因为通常被作为数据源使用
  • 通常不是手写,而是由高阶组件生成,如React Redux的connect(),Relay的createContainer(),Flux Utils的Container.create()
  • 例如:UserPageFollowersSidebarStoryContainerFollowedUserList

为清晰期间,我会把它们放在不同的目录下。

好处

  • 更好的任务分离。用这种方式写组件可以加深对app和UI的理解。
  • 更好的可复用性。同一个展示组件可以搭配不同的状态数据源使用,并且这样构成的容器组件还可以再次复用。
  • 展示组件实际上是app的“调色板”。你可以把所有组件都放在一个页面上,让设计师去尝试各种组合,而无需顾忌app的逻辑,还可以在这个页面上运行截图回归测试(screenshot regression test)。
  • 强制将“布局组件”如SidebarPageContextMenu等分离出来并用this.props.children包含,无需在多个容器组件中重写同样的标签和布局。

记住,容器不一定要构造DOM。从UI的角度来说,它们只需要提供组合的边界。

何时应该使用容器组件?

我建议先从展示组件开始构建app。最终你会发现某些中间层组件接收的属性(props)太多了。你会发现,一些组件从来不会使用传给它们的属性,而仅仅是将其传递给下层组件,而且每次下层组件需要更多数据时,你都得重写这些中间层组件。这时就该引入容器组件了。这样,数据和行为属性可以传给底层的叶组件,而不会给组件树中层那些无关的组件带来压力。

这是个持续的重构过程,用不着一开始就这么做。经过多次试验后,你会培养出一种直觉,知道何时应该抽取容器组件,就像知道何时该抽取公用函数一样。我在Egghead上的免费Redux教程或许能帮你!

其他分类法

必须明白,展示组件和容器组件并不是根据技术来分类,而是根据它们的使用目的分类的。

与之对比,还有一些相关(但并不完全相同!)的技术分类法:

  • 有状态无状态。一些组件使用React的setState(),另一些组件不使用。尽管一般情况下容器组件是有状态的,而展示组件无状态,但并不是铁则。展示组件也可以有状态,容器组件也可以无状态。
  • 函数。从React 0.14开始,组件可以定义为类,也可以定义为函数。函数型组件更简单,但有些功能只有类组件才能提供。尽管以后这些限制可能会解决,但至少目前是这样。建议多多使用函数型组件,因为它们更易懂,除非需要状态、生命周期钩子或性能优化等只有类组件才能提供的功能。
  • 纯组件不纯组件。所谓纯组件的意思是,在同样的属性(props)和状态(state)下它一定能返回同样的结果。纯组件可以定义为类,也可以定义为函数;可以有状态,也可以无状态。纯组件的另一个重点是,它不依赖于属性或状态的深层操作,因此通过shouldComponentUpdate()钩子中的浅层比较即可带来渲染性能的优化。目前,只有类组件才能定义shouldComponentUpdate(),也许以后会改变吧。

上述每个分类都可以是展示组件,也可以是容器组件。据我的经验,展示组件更倾向于无状态、纯的函数型组件,而容器组件更倾向于有状态、不纯的类组件。但这仅仅是观察结果,并不是规则,在某些情况下,完全相反的情况反而更合适。

不要把展示组件和容器组件的分隔当做规则。有时其实无所谓,甚至很难分隔。如果觉得某个组件难以确定应该是展示组件还是容器组件,很可能是时机还未到。放松点不要着急!

示例

Michael Chan的这个gist是个很不错的例子。

扩展阅读