场景介绍
@H_502_0@ @H_502_0@屏幕左侧大面积展现区块内容,点击 continue 按钮,切换为下条内容信息;右侧是一个导航条,指示当前区块展示信息条目。 @H_502_0@如果看 Gif 图不过瘾,可以到 CodeSandbox 进行在线了解。 @H_502_0@具体代码结构为:class App extends Component { render() { return ( <Stepper stage={1}/> ); } }@H_502_0@Stepper 组件 'stage' prop 表示默认开始第几个区块,同时具用同名 'stage' 状态。stage 在这里表示左侧一个个内容区块。
handleClick 方法对 this.stata.stage 进行切换。
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> ); } }@H_502_0@我们看到,Stepper 组件包含 Progress 组件(左侧导航)以及 Steps 组件。
这样的代码运行良好,但是在复用性和灵活性上有一些问题。比如:
- 如果我们需要切换 Progress 和 Steps 组件(左右)展示顺序怎么办?
- 如果我们的 Stepper 需要承载更多的 stages 怎么办?
- 如果我们需要更改某个 stage 内容怎么办?
- 如果我们想要切换 stages 顺序该怎么办?
重新设计
@H_502_0@仔细观察 Stepper 组件:它包含了当前区块 stage,以及一个更改 stage 的方法,渲染了两个子组件。 @H_502_0@我们使用 Function as Child Component 手段,将 Stepper 组件重构。(如果对 Function as Child Component 不熟悉,请参考我之前文章 组件复用那些事儿 - React 实现按需加载轮子) @H_502_0@如下图: @H_502_0@ @H_502_0@Progress 和 Steps 组件不再直接出现在 Stepper 组件的 render 方法中。我们使用 this.props.children 对 Stepper 组件的所有子组件进行渲染。这样 Stepper 组件渲染的内容更加灵活。 @H_502_0@但是仅仅这样的修改是不可能完成需求的,当用户点击 continue 按钮,stage 并不会进行切换。因为 Progress 和 Steps 组件无法再通过 props 感知 stage 和 handleClick 方法。 @H_502_0@为了解决这个问题,我们可以手动遍历 Stepper 组件的子节点,并对相应 props 一一注入。如下代码:const children = React.Children.map(this.props.children,child => { return React.cloneElement(child,{stage,handleClick: this.handleClick}) })@H_502_0@借助 React.Children.map 进行子节点遍历,并通过 React.cloneElement 方法对子组件进行拷贝,这个方法通过第二个参数,具有添加额外 props 的能力。Stepper 组件的 render 方法只需要具体应用:
const { stage } = this.state; const children = React.Children.map(this.props.children,child => { return React.cloneElement(child,handleClick: this.handleClick}) }) return ( <div style={styles.container}> {children} </div> );@H_502_0@这样一来,应用又一次正确运转!
class App extends Component { render() { return ( <div> <Stepper stage={1}> <Progress /> <Steps /> </Stepper> </div> ); } }@H_502_0@同样的手段,我们也可以应用到 Progress 组件当中。这里不再一一展开。
使用 Static Properties
@H_502_0@值得一提的是,我们可以使用 Static Properties 增强代码的可读性。Static Properties 允许我们在 class 当中直接对方法进行调用。首先,我们在 Stepper 组件中创建两个 static 方法,并赋值给 Progress 组件和 Steps 组件:static Progress = Progress; static Steps = Steps@H_502_0@现在,在 App.js 中我们可以直接:
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;@H_502_0@这样的好处体现在不用一次次地 import 进来 Progress 组件和 Steps 组件,它们都将作为 Stepper 的静态属性出现。我个人并不是很喜欢这种做法。
使用 React Transition Group
@H_502_0@我们使用 React Transition Group 对 Steps 组件内容添加过渡动画。只有当 props.num 与 this.props.stage 相等时,区块内容设置为可见: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> ); } }@H_502_0@我们也可以给 Steps 组件添加任意个内容:
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> ); } }@H_502_0@重新设计之后,整个应用变得更加灵活,复用性更强。我们可以指定任意个 stages,每一个 stage 文本内容也可以自定义设置,同样 stages 排列顺序等都可以随意搭配。 @H_502_0@重构代码以及效果可以访问这里查看。
思考及待续
@H_502_0@如果你觉得上述代码完美无懈可击,那显然想简单了。需求是变化多端的,如果我们想在 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> ); } }@H_502_0@如图, @H_502_0@ @H_502_0@这样一来,Stepper.Steps 组件再也不是 Stepper 组件的直接唯一子节点了,那预期之中的 props 自然又一次无法取得! @H_502_0@问题也不仅仅于此。笔者本人不是很喜欢类似 React.cloneElement 顶层 API,除了偏好以外,也有一个难以规避的问题:在使用 React.cloneElement 扩充 props 时,如果出现 props 命名冲突怎么办? @H_502_0@比如一个 <input> 遇见了命名为 value 的 prop,后果可想而知。 @H_502_0@那么问题来了,是否有更优雅高效的方法解决上述问题?或者,是否有更好的方式,实现更灵活的设计? @H_502_0@答案一定是有的,我将会留在下一篇文章进行讲解。 @H_502_0@本文源于:How To Master Advanced React Design Patterns,部分内容有改动。 @H_502_0@广告时间:
如果你对前端发展,尤其对 React 技术栈感兴趣:我的新书中,也许有你想看到的内容。关注作者 Lucas HC,新书出版将会有送书活动。 @H_502_0@Happy Coding! @H_502_0@PS: 作者Github仓库和知乎问答链接欢迎各种形式交流! @H_502_0@我的其他几篇关于React技术栈的文章: @H_502_0@从setState promise化的探讨 体会React团队设计思想 @H_502_0@React 应用设计之道 - curry 化妙用 @H_502_0@组件复用那些事儿 - React 实现按需加载轮子 @H_502_0@通过实例,学习编写 React 组件的“最佳实践” @H_502_0@React 组件设计和分解思考 @H_502_0@从 React 绑定 this,看 JS 语言发展和框架设计 @H_502_0@做出Uber移动网页版还不够 极致性能打造才见真章** @H_502_0@React+Redux打造“NEWS EARLY”单页应用 一个项目理解最前沿技术栈真谛 原文链接:https://www.f2er.com/react/301649.html