这可以在这里看:http://leozdgao.me/reactzhong-de-portalzu-jian/

几个月前遇到了写模态窗(modal)的需求,当初其实没什么思路,不知道怎么用更React的方式实现模态窗,于是去学习了下ReactBootstrap的源代码,发现了一个Portal组件,通过这个Portal的概念实现了React式的模态窗,诸如tooltip或者是notification等组件也是同样的道理。最近在看React-conf的视频时又听到Ryan提到,最近重新回去看ReactBootstrap的源代码,发现其实变化挺大的,原先Portal的部分已经被抽象出了另一个库react-overlays,于是准备总结下这个部分。

模态窗的实现思路

模态窗扮演这相当于桌面应用的MessageBox的角色,各个浏览器对这个部分的支持有些缺陷(这里指alert或confirm这些),比如每个浏览器实现效果有差异、用户可以禁止其显示,还有最重要的是没有办法灵活控制。

于是我们自己来实现,要扮演一个MessageBox,那么我们希望一个modal应该是永远被置于顶层的,而又由于Stacking Context的关系(在此不赘述),我们会将modal直接append在body元素下,设置一个属于它的z-index区间什么的。

回到React,在React中要实现一个模态窗,可以是这样的:

handleClick () {$('.modal').modalShow() // 假设这是一个jquery的modal插件
}

我不知道是不是存在这样一个jquery插件(应该是有的,不过我jquery用的不多),不过大家应该明白我的意思,利用React对其他库的友好来曲线救国。

另外的一种方式是实现一个Modal类,通过Modal.show()这样的方法调用,这个方法会负责将模态窗render在它应该出现的地方,这个思路我一开始也有想到,不过自己其实更倾向于尝试声明式的React组件实现。

那么实现React式的模态窗会遇到什么问题呢?比如有一个Container组件(承载页面结构和业务逻辑的组件),在页面的逻辑中会有一个modal弹出来,那么我们希望声明式的写法是这样的:

<div><button>Show</button>{/* portals */}<Modal isShowed={this.state.modalShowed}><p>Modal showed</p></Modal>
</div>

这里存在的问题就是Stacking Context,对于一个通用组件而言没有办法保证上下文的样式,于是就要讲讲这个Portal组件。

什么是Portal组件

所以我们需要的一个通用组件,它做如下的事情:

  • 可以声明式的写在一个组件中

  • 并不真正render在被声明的地方

  • 支持过渡动画

那么,像modal、tooltip、notification等组件都是可以基于这个组件的。我们叫这个组件为Portal。

Portal这个东西我不知道怎么给它一个合适的中文名,最初是在ReactBootstrap的项目里看到,之后React-conf又提到,那么相信应该是一个通用的概念了,由于这个组件并不真正render在它被声明的地方,姑且就翻译为『传送门』吧......

实现一个Portal组件

首先,由于它并不真正render在被声明的地方,那么:

render () {return null
}

恩,是的,没有办法在render方法里做文章,直接让它返回null即可,它会在被声明处留下一个noscript标签,无所谓了。

那么真正的render是在哪里进行的呢?我们先准备下_renderOverlay这个方法:

_renderOverlay() {let overlay = !this.props.children ? null : React.Children.only(this.props.children)if (overlay !== null) {this._mountOverlayTarget()// Save reference for future access.this._overlayInstance = React.render(overlay, this._overlayTarget)} else {// Unrender if the component is null for transitions to nullthis._unrenderOverlay()this._unmountOverlayTarget()}
}

我们把Portal的唯一子组件作为是要一个遮罩物(overlay),要承载这个遮罩物,我们需要一个DOM容器,于是我们在_mountOverlayTarget 方法里创建一个div,也就是this._overlayTarget,于是调用React.render方法将组件挂载到这个div节点上,并将保持对该实例的引用this._overlayInstance

通常情况下,对于React组件来说,不直接操作DOM,而且React.render方法我们通常都是在入口点调用一次,其他时候基本不用,然而对于Portal组件来说,这两点都是必要的。

相应的unrender的部分,比较简单,分别释放this._overlayTargetthis._overlayInstance

_unmountOverlayTarget() {if (this._overlayTarget) {this.getContainerDOMNode().removeChild(this._overlayTarget)this._overlayTarget = null}
}_unrenderOverlay() {if (this._overlayTarget) {React.unmountComponentAtNode(this._overlayTarget)this._overlayInstance = null}
}

好了,那么我们需要在何处调用_renderOverlay呢,很容易想到:

componentDidMount () {this._renderOverlay()
}componentDidUpdate () {this._renderOverlay()
}

然后记得要擦屁股:

componentWillUnmount() {this._unrenderOverlay();this._unmountOverlayTarget();
}

为了增加Portal的灵活性,可以给它传一个container属性,用来指定『传送门』的位置(默认为body元素)。

实现上其实基本上就是这样了,这里要简单提一下,之前就ReactBootstrap对Portal组件的实现而言,把isShowed的逻辑给加在Portal里,增加了一些实现的复杂度,这个项目好像重构过一波,现在的实现中isShowed的逻辑被移出去了,Portal仅用于充当『传送门』的角色,那么以Modal为例:

render () {if (this.props.isShowed) {return (<Portal><div><div className='modal'>{this.props.children}</div><div className='backdrop'></div></div></Portal>)}else return null
}

感觉这样的设计确实比之前更科学,而这个部分也被单独抽象到了react-overlays中。

过渡动画

并不想在Portal组件里再额外加入动画相关的逻辑了,于是准备再封装一层,加上对过渡动画的支持。

提供几个思路,一个是通过操作classname,这里以模态窗为例,先上代码:

componentWillReceiveProps (nextProps) {const { show } = nextPropsif (!show && this.props.show && this.props.closeTimeout) { // ready to closethis.setState({ delaying: true, closing: true, opened: false })setTimeout(() => {this.setState({ delaying: false, closing: false })}, this.props.closeTimeout)}
}componentDidUpdate (prevProps, prevState) {const { show } = prevPropsif (!show && this.props.show) { // first showsetTimeout(() => { // need do it in next loopthis.setState({ opened: true })})}
}

分别在合适的时机加上相应的class即可,对于show这个动作来说没什么问题,但对于close而言,显然我们需要等到transition的过渡时间结束后才真正unrender我们的组件,于是我们给它一个可传入的属性叫closeTimeout,并在组件内具有一个this.state.delaying这个状态,那么我们的render逻辑应该是这样的:

if (this.props.show || this.state.delaying) {return (<Portal><div className={classnames([ 'modal',{ opened: this.state.opened,closing: this.state.closing }])}>{this.props.children}</div></Portal>)
}
else {return null
}

再灵活一点就是自定义opened和closing的classname了,这里不赘述。

这是一种方法,不过动画的部分不怎么React式,是的,React动画又是另一块内容了,这里不会详述,因为似乎还不怎么成熟,不过还是给出一些可供参考的库吧:

  • Timeout Transition Group

  • react-motion

简单地贴点代码:

render () {return (<Portal><TimeoutTransitionGroupenterTimeout={200}leaveTimeout={250}transitionName='modal-anim'>{this.props.isShowed ? (<div className='modal'>{this.props.children}</div>) : null}</TimeoutTransitionGroup></Portal>)
}

当然这里的是否需要transition、timeout以及transitionName都应该是可配置的,作为示例代码就简单点写了。

最后

推荐大家看看react-overlays,可以直接使用里面的Portal实现还有一些其他有用的通用组件,文档在这里。或者其实有一个单独的react-modal的实现也可以直接用。

好了,结束了。

React中的Portal组件相关推荐

  1. 如何在React中从其父组件更改子组件的状态

    by Johny Thomas 约翰尼·托马斯(Johny Thomas) 如何在React中从其父组件更改子组件的状态 (How to change the state of a child com ...

  2. [react] 在react中怎样改变组件状态,以及状态改变的过程是什么?

    [react] 在react中怎样改变组件状态,以及状态改变的过程是什么? 使用this.setState改变组件的状态 改变的过程中,React Fiber Reconciler遍历了整个Fiber ...

  3. [react] 描述下在react中无状态组件和有状态组件的区别是什么?

    [react] 描述下在react中无状态组件和有状态组件的区别是什么? 1,无状态组件主要用来定义模板,接收来自父组件props传递过来的数据,使用{props.xxx}的表达式把props塞到模板 ...

  4. [react] 在react中无状态组件有什么运用场景

    [react] 在react中无状态组件有什么运用场景 适用于逻辑简单的纯展示的场景,如资料卡片等 个人简介 我是歌谣,欢迎和大家一起交流前后端知识.放弃很容易, 但坚持一定很酷.欢迎大家一起讨论 主 ...

  5. react中创建一个组件_如何使用React和MomentJS创建一个Countdown组件

    react中创建一个组件 Recently I had to create a Countdown for one of my other projects, and I thought that i ...

  6. React中的纯组件

    React中的纯组件 React提供了一种基于浅比较模式来确定是否应该重新渲染组件的类React.PureComponent,通常只需要继承React.PureComponent就可以定义一个纯组件. ...

  7. React中的受控组件和非受控组件

    一.认识受控组件 在React中,HTML表单的处理方式和普通的DOM元素不太一样:表单元素通常会保存在一些内部的state. 比如下面的HTML表单元素: 这个处理方式是DOM默认处理HTML表单的 ...

  8. 如何在React中使用功能组件

    Have you wondered how to create a component in React? 您是否想过如何在React中创建组件? To answer, it is as simple ...

  9. React 中的受控组件和非受控组件的区别

    React 中受控和非受控的概念通常跟 form 表单组件相关,比如 input ,通过区分与 input 进行数据交互的方式,组件被分成两个不同的派系,受控与非受控. 受控组件 React 中受控组 ...

最新文章

  1. kbengine0.2.3发布,开源分布式游戏服务端引擎
  2. 【转】为什么自动车完全不可以犯错误
  3. hdu4699-Editor【对顶栈】
  4. 关于虚拟机第二块网卡eth1(仅主机模式)的配置问题
  5. mysql要将语句反复执行15次_MySQL多表查询疑问
  6. c 语言编程怎样弄循环语句,C 语言循环语句
  7. 多线程输出奇数和偶数
  8. Pyhton爬小说实例解析笔记——爬虫基础
  9. C语言lowB排序和NB排序
  10. Pytorch深度学习笔记(02)--损失函数、交叉熵、过拟合与欠拟合
  11. 虹科资讯| 虹科AR荣获汽车后市场“20佳”维修工具评委会提名奖!
  12. 三个角度细谈:如何发挥朋友圈广告的威力
  13. echarts-gl中3d曲面UV参数详解
  14. 88---Python 以符号的方式给出积分表达式,类似Mathematics
  15. 王者荣耀——bat批处理文件,自动刷金币版(脱胎于30行Python代码刷金币版),Windows双击即可运行!
  16. 电子商务:说说转化率
  17. 0 在c语言中有什么作用,\0在c语言中代表什么?
  18. iperf 工具使用
  19. SD卡文件损坏怎么办?SD卡数据恢复用这招
  20. 磁偏角测试仪TY3300永磁材料磁偏角测量

热门文章

  1. 如何在新版的gitbook上写自己的书
  2. c#之多线程之为所欲为
  3. jQuery动态五星评分
  4. mysql的Innodb为什么使用B+树
  5. Netty技术细节源码分析-Recycler对象池原理分析
  6. Redis面试常问3 如何实现分布式锁 记住Redis的原子性
  7. 收获,不止SQL优化——抓住SQL的本质--第十章
  8. 设计模式--责任链模式--Java实现
  9. Go bufio.Reader 结构+源码详解
  10. rcp异步多参数实例