Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
imwebteam opened this issue Sep 6, 2018 · 0 comments
Open

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

imwebteam opened this issue Sep 6, 2018 · 0 comments
Labels

Comments

@imwebteam
Copy link
Contributor

imwebteam commented Sep 6, 2018

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

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

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

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

image4

上面的 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 对象动态插入子组件。

image6

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 的文本和顺序,以及我们可以决定进度条在左侧还是右侧。
image8

虽然改进了很多,但在灵活性上我们仍然受到限制! 如果我们想在 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 属性。

@imwebteam imwebteam added the React label Sep 6, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant