React 两个基础的组件设计模式

「组件设计的模式」是React 技术发开者的工具之外的专业技能

今年(2020)我围绕着一个主题花了很多时间,它就是所谓的架构师(程序结构设计师),有了相当多的结论,其中除了对这个专业(基础技术和职责)有了更深的认识外,还在其基础上,上升到对整个软件项目开发中四个专业(功能,结构,开发和管理)有了新认识。然而,实际的项目实践中,我们大部分的工作,它性质其实都是开发——将需求转为实际模块代码,而不是结构设计。因为优秀的结构设计已经集成在各种库和应用框架之中,我们拿来即用,除非我有能耐,可以对像React这样的库或前端框架进行调优,为社区作出贡献,否则,作为一名日常的开发者,他的大部分价值,都体现在技术开发

所以,如果有一个发展方向的问题,基础方向(至少当下)是前端技术开发者,而不应该是所谓的架构师,那可以是一更远的方向,以当下的方向为基础。

作为技术开发者,它的专业任务可概括为,**对由架构设计师提供的结构设计案进行分析,结合某种前端 SPA理论,设计出某种具体的代码实现方案。**作为React 技术发开者,他可选的技术,是React 各种API,例如Hook函数组件和类组件API。

我们可以从「任务」的方向发现React 技术发开者的需要「专业技能」。其中一种任务是「复合的交互UI(Vx)」。对一个经验丰富的React 开发者,他应该知道日常中开发任务,是「针对某个交互功能进行组件分割」。最困难的设计任务,是选择合适的组件,以满足项目代码可维护性可扩展性等质量要求。

「组件设计的模式」是React 技术发开者的专业技能,本文介始了最基本两个组件设计模式。本文略译自《Component Composition in React 》。

Introduction

交互应用程序(Web UI app)是一种事务数据处理程序,「代码逻辑重复率」是比较高的。所以「代码复用」的是一项很重要技术任务,使用React开发当然不例外。

刚接触React的开发者,面对「代码复用任务」常常想到使用「类继承技术」,然而在大部分情况下,使用React官方提供的「组件组合模型」更为优雅,贴合UI结构实际。

开发用户界面,一个典型的复用例子就是标签页(tabs)组件:多个相似「标签」,每个标签有不同的「标签内容」。本文通过创建标签页组件,演示如何使用「React 组件组合模型」注1实现复用任务,其中演示三种复用模式:专业组件(specialized components),容器组件(container components),和它们的混合模式。

Tabs

一个最简单的标签页(tabs)组件代码(JSX部分)大概如下这个样子:

<ul className="nav nav-tabs">
  <li className="nav-item">
    <button
      className={`nav-link${selectedTabIndex === 0 ? " active" : ""}`}
      onClick={() => setSelectedTabIndex(0)}
    >
      {"Tab 1"}
    </button>
  </li>
  <li className="nav-item">
    <button
      className={`nav-link${selectedTabIndex === 1 ? " active" : ""}`}
      onClick={() => setSelectedTabIndex(1)}
    >
      {"Tab 2"}
    </button>
  </li>
</ul>
<div className="tab-content">
  {selectedTabIndex === 0 && (
    <>
      <h2>{"Tab 1"}</h2>
      <p>{"Some content for the first tab"}</p>
    </>)}
  {selectedTabIndex === 1 && (
    <>
      <h2>{"Tab 2"}</h2>
      <p>{"Some content for the second tab"}</p>
    </>)}
</div>

最顶层的UL是 标签头,DIV是对应的 标签页内容。当开发完后出现新需求,要「增加新标签」,或「修改标签形式」时,怎么办?由于这是一个单体的组件,我们只能手动拷贝标签代码,和一个一个修改标签的新形式,非常的不理想。

为了改善开发,我们可以将「单体的标签页组件」分解成更小粒度的组件,再通过 组合模型 来组建完整的标签页功能,最基本的组合复用模式有:

  • 第一,专业组件(specialized components);
  • 第二,容器组件(container components);
  • 第三,混合以上二;

专业复用模式与data props

专业组件 是 「最简单最基础的」 组件复用开发模式,你可认为任何一个组件都符合 专业组件模式,因为它们都是由抽象的React 函数或类组件实现的。专业组件的技术特征是,它是通用抽象的组件模板,接受一些 props 进行具体派生,或特例化。

例如,标签页组件(tabs)可分割为 标签头专业组件(TabSpecialized),和 标签内容专业组件(TabContentSpecialized)。

TabSpecialized

以下是标签头专业组件 的代码:

const TabSpecialized = props => (
  <li className="nav-item">
    <button 
      className={`nav-link${props.selected ? " active" : ""}`} 
      onClick={props.onSelect}>
      {props.text}
    </button>
  </li>);

组件抽象了三个props (两个data props,一个func props): text, selected, and onSelect 。它们的意义也很直显。使用(通过JSX实例化它们)时:

<TabSpecialized
  text="Tab 3"
  selected={selectedTabIndex === 2}
  onSelect={() => setSelectedTabIndex(2)}
/>

分割出专业组件,代码都集中在一处,上面的两个问题——增加新标签和修正新形式——都得到很好的解决。

TabContentSpecialized

标签内容专业组件同理:

const TabContentSpecialized = props => (
  <>
    <h2>{props.header}</h2>
    <p>{props.paragraph}</p>
  </>);

两个data props:header and paragraph 。使用:

<TabContentSpecialized
  header="Tab 3" 
  paragraph="Some content for the third tab"
/>

容器复用模式与children props

很多常用UI功能组件,它的形式相对固定,专业组件模式是够用的;然而,固定形式不适用于所有的交互功能。例如「标签头」是典型形式稳定的专业组件,而「标签内容」,则不同了。

上面的「标签内容组件」 只有两个data props,这是为了示例而已,在具体的开发中,标签内容可能会有更多的渲染内容需要抽象定制,此情也可以选择将它们一一抽象为data props,但这样做可能会使组件结构变得很复杂。此时最好使用 容器组件模式。

React components 有一个特殊的prop API,就是 children。相对于data props 和 func props,children 是名字唯一的object props。children 是指JSX定义内嵌部分——子组件。

有了children props,我们可将组件不稳定部分“挖空”,像做成一个容器,实例复用再具体填特定的内容。例如,我们这里的「标签内容组件」,可这样给「内容」“挖”一个占位:

const TabContentContainer = props => (
  <div className="tab-content">
    {props.children}
  <div />);

然后,实例化时再填上内容:

<TabContentContainer>
  <div className="tab4-content">
    <img src={logo} className="App-logo" alt="logo" />
    <p>This tab can contain anything.</p>
  </div>
</TabContentContainer>

可见容器模式的复用原理与专业模式是类似的,只是复用抽取的单元不同,专业组件抽取的是静态形式数据,容器组件抽象的组件内部结构。

由于React 不限定props可传递的内容,故children 是名字唯一的object props,我还可以自定义不同用途的object props,看官方例子:

https://reactjs.org/docs/composition-vs-inheritance.html#containment

混合两种复用模式

在实际开发中,一个组件既可是专业组件(抽象出一些形式属性),同时也是一个容器组件(抽象出局部结构)。例如本文实例的「标签头」可结合两种模式实现更丰富的功能:

const Tab = props => (
  <li className="nav-item">
    <button 
      className={`nav-link${props.selected ? " active" : ""}`} 
      onClick={props.onSelect}
    >
      {props.text}
      {props.children}
    </button>
  </li>);

这个「标签头」组件,除了有原来一些 data props,还添加了一个children props可添加额外结构内容定制,例如图片。这样,它就结合了两种组件组合模式了。

Conclusion

React的基础API,包括函数组件和类组件,是实现交互UI的基础,也是代码复用的基础,因为函数组件和类组件都是参数的抽象的对象模板。由于交互UI本身的结构化特性,React 巧妙的利用其及JS的灵活性,提供了诸如children这种object props,支持「组件组合模型」,最大化开发复杂组件的灵活性。

代码看这里。 https://github.com/ChrisDobby/tabs-composed

我的小结@2023

1 本文标题是 “组件模式”,更确切 是 “组件模式”的基础——React 提供的 组件API,包括在技术上的 函数组件 和 类组件

2 “组件模式”更准确 是指 组件设计模式,像CP模式

3 本文介绍了 React 提供的 组件API的三种用法 data, func, 和child prop。特别注意,func 和 child 都是 object prop,而 使用 JSX,child prop更直观表达了 父子组件 之间嵌套关系

4


  1. 组合模型(composition model)不是 API,而是对基础API的某种使用,一种设计模式。
裸男
Nakeman.cn 2023 Build by Gatsby and Tailwind, Deploy on Netlify.