Skip to content

使用 react 的高级设计模式 — 复合组件 #7

Description

@penouc

使用 react 的高级设计模式 — 复合组件

为了庆祝 react 16.3 的正式发布,我准备分享一下我最近在使用 react 组件上的一些新的实践,使用这个新的技术我可以设计一些完全可以复用的组件,并且可以将这些组件灵活的运用在各个复杂的环境中。

Edit stepper

上面的这个 sandbox 描述了如何初始化一个简单且小巧的 stepper 组件,我将会通过这个组件展示一些技巧。如上链接所示,这个组件运行十分的良好,实现了我需要的功能,但是有一个缺点,就是不够灵活。

import React, { Component } from 'react';
import Stepper from "./Stepper"

class App extends Component {
  render() {
	return (
		<Stepper stage={1}/>
	);
  }
}

export default App;

Stepper 需要传入一个属性,就是 开始的页面

如你所见,这个 Stepper 组件可以灵活的传入 stage 属性;我们只能传入 stage 的值来改变它从哪一步进入。
那么如果我想要直接从右侧定位到相应的步骤应该怎么做呢?
如果我需要添加额外的 stages 应该怎么弄?
或者我需要修改 stages 中的内容呢?
或者我需要改变 stages 的先后顺序?

就像上面这些问题提到的,如果我要满足上面说的这些条件,是不是只有重写一个组件才能实现。但是重写也需要额外的成本。然后,如果在未来需要实现更多的功能,这个功能组件可能还需要重写。所以,我们尝试使用另外一种写法,来让这个组件更加灵活适应未来的各种灵活的复用和灵活变动。

在这个的第一部分,我们将会讨论的这个设计模式叫做『复合组件』

使用复合组件设计模式

首先,我们看一下「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」组件是一个只包含一个 state 对象的基础组件,这个 state 对象中保存了当前的 stage, 一个增加当前 stage 属性的方法,还有 render 方法里面返回包含两个子组件的的 div 。

现在,我们明确地将 「Progress」和 「Steps」组件直接放入到「Stepper」组件中。为了减少静态属性的直接写入,我们使用动态的插入 props 对象到子组件中。

减少了直接写入后,我们能够在 Stepper.js 中使用 props.children 对象并且 Propgress 和 Steps 组件放入到 Stepper 组件中。

仅仅只是这一个改变就给我们带来了很大的收益。我们能够选择在树中的那个组件优先渲染;也可以选择把进度条放到左边还是右边。

但是这么实现有一个最大的问题。Progress 和 Steps 组件不能够再访问 stage 和 handleClick 属性了。为了使每一个组件都能够使用他们需要的 props,我们需要对所有的 child 进行遍历,然后将这些 props 插入到每个组件中。我们能通过这个 react 原生提供的 API 做不少这样的辅助方法。这个 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 就像它名字一样,它是通过 clone 这些子组件然后向下传递。至关重要的是,它也可以被用来插入额外的数据。

// 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>
	);

现在我们能够加入 ProgressState 这两个组件作为 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;

静态属性

另外一个用来提高可读性和易用性的技术点就是使用 class 的静态属性,这个让我们可以在 class 中直接调用方法。

这是什么意思呢?接下来我们看一下

首先我们需要在 Stepper 这个组件中创建静态方法,并且将 Progress 和 Steps 组件赋值给这两个静态方法。

static Progress = Progress
static Steps = Steps

所以现在,和之前需要在很多地方引入 Progress 和 Steps 组件不同,我们只需要在 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>
	)
  }
}

你也许已经注意到,这个组件非常的死板,跟之前的 Stepper 组件非常类似。让我们把这 4 个 Stage 组件通过使用 children.props 来替换,然后循环替换上我们需要的 props 并且给它添加静态方法,让外部可以直接在 Stepper 中调用。

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 Group 这个组件包裹。同样的循环也需要被实现,但是这个组件只有 stage prop 和组件的索引值相同的时候才能显示。如果是匹配的,它会被包裹在 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;

我们已经创建了足够多的灵活的可复用的组件,现在可以选择展示多少个 Stages ,他们展示什么,以及如何排序,还有进度条模块是在页面的左边还是右边。

Edit stepper

但是其实我们还是被限制的,如果现在需要给每一个 Steps 块添加一个 title 的块应该怎么做呢?

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;

这样这个应用的结构就分崩离析了, 因为 Steps 这个组件并不是 Stepper 组件的直接子组件,所以它其实没有权限访问 Stage 属性。
这就是 React 16.3 版本发布后解决的大问题! 在此之前,React’s context API 都是 react 的实验特性,但是现在正式发布了。
在第二部分,我将会讨论如何我们才能够实现使用 context API 来传递 props 到组件树的任何位置,所以不管 Stepper.steps 这个组件的位置,他将可以在任何位置都能够取到 stage prop。

Metadata

Metadata

Assignees

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions