howenhuo

2018-07-06 13:53

如何掌握高级的React设计模式: 复合组件【译】

本文作者:IMWeb howenhuo 原文出处:IMWeb社区 未经同意,禁止转载

原文链接:How To Master Advanced React Design Patterns: Compound Components

为了庆祝 React 16.3 的正式发布,我决定分享我最近使用的一些技术,这些技术彻底改变了我创建 React 组件的方法。因此,我能够设计出完全可重用的组件,并且可以在许多不同的环境中灵活地使用这些组件。

单击此处查看本系列的第2部分:Context API

上面的 sandbox 是一个简洁的 Stepper 组件的初始代码,我将使用它来展示其中的一些技术。 就目前而言,这个组件完全正常工作,并且完全按照设计目的进行,但它缺乏灵活性。

import React, { Component } from 'react';
import Stepper from "./Stepper"
class App extends Component {
  render() {
    return (
      <Stepper stage={1}/>
    );
  }
}
export default App;

如上可视,Stepper 组件的灵活性在 stage 属性处终止;我们只能修改 stage 的值来确定 Stepper 组件进行到哪一步。

  • 如果我需要将进度块放在右侧怎么办?
  • 如果我需要一个类似的追加额外 stageStepper 怎么办?
  • 如果我需要更改 stage 的内容怎么办?
  • 如果我想改变 stage 的顺序怎么办?

就目前而言,我要实现这些变化的唯一方法是完全重写组件,以相同的方式重写一个类似的组件。 但是,如果将来又要进行其他更改,那该组件又一次需要重写。 因此,让我们尝试不同的方法来重写组件,使其具有灵活性和可重用性,以应变将来任何的配置。

在本系列的第一部分中,我们将探讨一种名为“复合组件”的设计模式

使用复合组件设计模式

首先,让我们来看看 Stepper 组件。

class Stepper extends Component {
  state = {
    stage: this.props.stage
  }
  static defaultProps = {
    stage: 1
  }
  handleClick = () => {
    this.setState({ stage: this.state.stage + 1 })
  }
  render() {
    const { stage } = this.state;
    return (
      <div style={styles.container}>
        <Progress stage={stage}/>
        <Steps handleClick={this.handleClick} stage={stage}/>
      </div>
    );
  }
}
export default Stepper;

Stepper 组件有一个存储当前 stage 的状态对象,一个增加 stage 属性值的方法,以及一个 render 方法,它返回包含2个子组件的div。

目前,我们明确地将 ProgressSteps 组件直接放在 Stepper 组件中。 为了减少这种静态写法,我们可以使用 props 对象动态插入子组件。

Stepper.js 文件中使用 props.children 对象替换 ProgressSteps 组件,并将它们放在 App.js中的 Stepper 组件内。

只需这简单的改变就给我们带来很大的收益。现在我们可以选择组件树中的哪个组件先渲染; 我们可以选择进度块是在左侧还是右侧。

但这种方法有一个问题: ProgressSteps 组件不能再访问 stagehandleClick 属性了。 为了让每个子组件获取它们需要的属性,我们需要手动遍历每个子组件并向其注入这些属性。 我们可以使用 react API 提供的一些辅助方法来实现。 两个方法是: Children.map()cloneElement()

const children = React.Children.map(this.props.children, child => {
  return React.cloneElement(child, {stage, handleClick: this.handleClick})
})

Children.map() 类似于 Array.map() 方法。但请务必使用Children.map(),因为 children.props 具有不透明的数据结构,使得 Array.map() 方法不适合此用例。

cloneElement 如名称一样,它克隆这些子组件并可以注入额外的属性,最后返回新的组件。

// Render method of Stepper.js
const { stage } = this.state;
const children = React.Children.map(this.props.children, child => {
  return React.cloneElement(child, {stage, handleClick: this.handleClick})
});
return (
  <div style={styles.container}>
    {children}
  </div>
);

现在我们可以将 ProgressStage 作为子项添加到 Stepper 组件中,运行效果不变。但此时我们可以决定每个组件的位置,甚至可以在左右两侧同时设置进度块。

import React, { Component } from 'react';
import Stepper from "./Stepper"
import Progress from './components/Progress';
import Steps from './components/Steps';
class App extends Component {
  render() {
    return (
      <div>
        <Stepper stage={1}>
          <Progress />
          <Steps />
        </Stepper>
      </div>
    );
  }
}
export default App;

静态属性

另外一种能够提高可读性和易用性的技术就是使用类的静态属性。它允许我们直接在类上调用方法。

这是什么意思? 让我们来看看…。

首先,我们在 Stepper 组件中创建两个静态方法,并将 ProgressSteps 组件赋值给它们:

static Progress = Progress
static Steps = Steps

那么现在,我们不需要再到处引入 ProgressSteps 组件,而是直接从 Stepper 组件中引用它们:

import React, { Component } from 'react';
import Stepper from "./Stepper"
class App extends Component {
  render() {
    return (
      <Stepper stage={1}>
        <Stepper.Progress />
        <Stepper.Steps />
      </Stepper>
    );
  }
}
export default App;

到目前为止,我们已经创建了一个简单可读且灵活的API。那么是不是可以对 Progress 组件使用相同的技术呢? 让我们来看看......

export default class Progress extends Component {
  render(){
    const {stage} = this.props
    return(
      <div style={styles.progressContainer}>
        <Stage stage={stage} num={1} />
        <Stage stage={stage} num={2} />
        <Stage stage={stage} num={3} />
        <Stage stage={stage} num={4} />
      </div>
    )
  }
}

您也许已经注意到 Progress 组件与之前的 Stepper 组件存在同样的问题。所以我们用 props.children 对象来替换这 4 个 Stage 组件并遍历子项添加所需的属性,然后在 Stepper 类中添加一个 Stage 静态方法,供外部直接引用 Stage

export default class Progress extends Component {
  render(){
    const {stage} = this.props
    const children = React.Children.map(this.props.children, child => {
      return React.cloneElement(child, {stage})
    })
    return(
      <div style={styles.progressContainer}>
        {children}
      </div>
    )
  }
}

完成上述步骤后,我们可以在任意位置动态添加任意数量的 Stage 组件:

import React, { Component } from 'react';
import Stepper from "./Stepper"
class App extends Component {
  render() {
    return (
      <Stepper stage={1}>
        <Stepper.Progress>
          <Stepper.Stage num={1} />
          <Stepper.Stage num={2} />
          <Stepper.Stage num={3} />
          <Stepper.Stage num={4} />
        </Stepper.Progress>
        <Stepper.Steps />
      </Stepper>
    );
  }
}
export default App;

接下来我们可以对 Steps 组件做同样的改进,但这个有点不同,因为每个子项都要被 React's Transition GroupTransition 组件包裹。同样是使用 Children.map() 遍历,但只有 Steps 组件的 stage 属性与子组件的 num 属性匹配时才展示该子组件。 即它们匹配时,子组件会被包裹在 Transition 组件中(ReactTransitionGroup文档解释了此目的)并在屏幕上渲染。

class Steps extends Component {
  render() {
    const { stage, handleClick } = this.props
    const children = React.Children.map(this.props.children, child => {
      console.log(child.props)
      return (
        stage === child.props.num &&
        <Transition appear={true} timeout={300} onEntering={entering} onExiting={exiting}>
          {child}
        </Transition>
      )
    })
    return (
      <div style={styles.stagesContainer}>
        <div style={styles.stages}>
          <TransitionGroup>
            {children}
          </TransitionGroup>
        </div>
        <div style={styles.stageButton}>
          <Button disabled={stage === 4} click={handleClick}>Continue</Button>
        </div>
      </div>
    );
  }
}

export default Steps;

通过在 Stepper 组件上添加相应的静态方法,我们可以按照我们想要的顺序添加任意数量的Step组件。

import React, { Component } from 'react';
import Stepper from "./Stepper"
class App extends Component {
  render() {
    return (
      <Stepper stage={1}>
        <Stepper.Progress>
          <Stepper.Stage num={1} />
          <Stepper.Stage num={2} />
          <Stepper.Stage num={3} />
        </Stepper.Progress>
        <Stepper.Steps>
          <Stepper.Step num={1} text={"Stage 1"}/>
          <Stepper.Step num={2} text={"Stage 2"}/>
          <Stepper.Step num={3} text={"Stage 3"}/>
          <Stepper.Step num={4} text={"Stage 4"}/>
        </Stepper.Steps>
      </Stepper>
    );
  }
}
export default App;

我们用一种方式就创建了非常灵活可重用的组件。现在我们可以选择多少个 stage,每个 stage 的文本和顺序,以及我们可以决定进度条在左侧还是右侧。

虽然改进了很多,但在灵活性上我们仍然受到限制! 如果我们想在 Steps 上方添加标题怎么办?

class App extends Component {
  render() {
    return (
        <Stepper stage={1}>
          <Stepper.Progress>
            <Stepper.Stage num={1} />
            <Stepper.Stage num={2} />
            <Stepper.Stage num={3} />
          </Stepper.Progress>
          <div>
            <div>Title</div>
            <Stepper.Steps>
              <Stepper.Step num={1} text={"Stage 1"}/>
              <Stepper.Step num={2} text={"Stage 2"}/>
              <Stepper.Step num={3} text={"Stage 3"}/>
              <Stepper.Step num={4} text={"Complete!"}/>
            </Stepper.Steps>
          </div>
        </Stepper>
    );
  }
}
export default App;

上面这样做会破坏我们应用程序的结构,因为 Stepper.Steps 组件不再是 Stepper 组件的直接子组件,所以它无法访问 stage 属性了。

这就是为什么 React 16.3 的发布非常重要! 到目前为止,React’s context API 还处于试验阶段,但现在它已经正式发布了!

在本系列的第2部分中,我将探讨如何实现 context API 以便能够在组件树中的任何位置传递属性,这样无论 Stepper.Steps 组件位于何处,它始终都能够访问 stage 属性。

0条评论

    您需要 注册 一个IMWeb账号或者 才能进行评论。