React 设计模式和场景分析

Kygo on live

这一周连续发表了两篇关于 React 的文章:

其中涉及到 React 组件复用、轮子设计相关话题,并配合相关场景实例进行了分析。这些内容都算是 React 设计模式,一提到 Design Patterns,读者大可不必恐惧,事实上这都是 React 开发应用灵活性的体现。今天这篇文章,我们继续通过一个场景,循序渐进,通过一步步优化设计来进行加深理解。

场景介绍

页面展现

屏幕左侧大面积展现区块内容,点击 continue 按钮,切换为下条内容信息;右侧是一个导航条,指示当前区块展示信息条目。

如果看 Gif 图不过瘾,可以到 CodeSandbox 进行在线了解。

具体代码结构为:

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

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

我们看到,Stepper 组件包含 Progress 组件(左侧导航)以及 Steps 组件。
这样的代码运行良好,但是在复用性和灵活性上有一些问题。比如:

  • 如果我们需要切换 Progress 和 Steps 组件(左右)展示顺序怎么办?
  • 如果我们的 Stepper 需要承载更多的 stages 怎么办?
  • 如果我们需要更改某个 stage 内容怎么办?
  • 如果我们想要切换 stages 顺序该怎么办?

现有代码基础上,这些问题都可以解决。但是需要重新更改组件编写内容。如果某天又新增或者调整了需求,组件内容同样又需要改写。

接下来,我们用另一种方式实现需求,使得代码更加灵活,复用性更强。

重新设计

仔细观察 Stepper 组件:它包含了当前区块 stage,以及一个更改 stage 的方法,渲染了两个子组件。

我们使用 Function as Child Component 手段,将 Stepper 组件重构。(如果对 Function as Child Component 不熟悉,请参考我之前文章 组件复用那些事儿 - React 实现按需加载轮子

如下图:

Function as Child Component 重构

Progress 和 Steps 组件不再直接出现在 Stepper 组件的 render 方法中。我们使用 this.props.children 对 Stepper 组件的所有子组件进行渲染。这样 Stepper 组件渲染的内容更加灵活。

但是仅仅这样的修改是不可能完成需求的,当用户点击 continue 按钮,stage 并不会进行切换。因为 Progress 和 Steps 组件无法再通过 props 感知 stage 和 handleClick 方法。

为了解决这个问题,我们可以手动遍历 Stepper 组件的子节点,并对相应 props 一一注入。如下代码:

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

借助 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, {stage, handleClick: this.handleClick})
})
return (
    <div style={styles.container}>
        {children}
    </div>
    );

这样一来,应用又一次正确运转!

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

同样的手段,我们也可以应用到 Progress 组件当中。这里不再一一展开。

使用 Static Properties

值得一提的是,我们可以使用 Static Properties 增强代码的可读性。Static Properties 允许我们在 class 当中直接对方法进行调用。首先,我们在 Stepper 组件中创建两个 static 方法,并赋值给 Progress 组件和 Steps 组件:

static Progress = Progress;
static Steps = Steps

现在,在 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;

这样的好处体现在不用一次次地 import 进来 Progress 组件和 Steps 组件,它们都将作为 Stepper 的静态属性出现。我个人并不是很喜欢这种做法。

使用 React Transition Group

我们使用 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>
        );
    }
}

我们也可以给 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>
    );
  }
}

重新设计之后,整个应用变得更加灵活,复用性更强。我们可以指定任意个 stages,每一个 stage 文本内容也可以自定义设置,同样 stages 排列顺序等都可以随意搭配。

重构代码以及效果可以访问这里查看。

思考及待续

如果你觉得上述代码完美无懈可击,那显然想简单了。需求是变化多端的,如果我们想在 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>
    );
  }
}

如图,

加入标题

这样一来,Stepper.Steps 组件再也不是 Stepper 组件的直接唯一子节点了,那预期之中的 props 自然又一次无法取得!

问题也不仅仅于此。笔者本人不是很喜欢类似 React.cloneElement 顶层 API,除了偏好以外,也有一个难以规避的问题:在使用 React.cloneElement 扩充 props 时,如果出现 props 命名冲突怎么办?

比如一个 <input> 遇见了命名为 value 的 prop,后果可想而知。

那么问题来了,是否有更优雅高效的方法解决上述问题?或者,是否有更好的方式,实现更灵活的设计?

答案一定是有的,我将会留在下一篇文章进行讲解。

本文源于:How To Master Advanced React Design Patterns,部分内容有改动。

广告时间:
如果你对前端发展,尤其对 React 技术栈感兴趣:我的新书中,也许有你想看到的内容。关注作者 Lucas HC,新书出版将会有送书活动。

Happy Coding!

PS: 作者 Github仓库知乎问答链接 欢迎各种形式交流!

我的其他几篇关于React技术栈的文章:

从setState promise化的探讨 体会React团队设计思想

React 应用设计之道 - curry 化妙用

组件复用那些事儿 - React 实现按需加载轮子

通过实例,学习编写 React 组件的“最佳实践”

React 组件设计和分解思考

从 React 绑定 this,看 JS 语言发展和框架设计

做出Uber移动网页版还不够 极致性能打造才见真章**

React+Redux打造“NEWS EARLY”单页应用 一个项目理解最前沿技术栈真谛

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容