React中的Portal组件
这可以在这里看: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._overlayTarget
和this._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组件相关推荐
- 如何在React中从其父组件更改子组件的状态
by Johny Thomas 约翰尼·托马斯(Johny Thomas) 如何在React中从其父组件更改子组件的状态 (How to change the state of a child com ...
- [react] 在react中怎样改变组件状态,以及状态改变的过程是什么?
[react] 在react中怎样改变组件状态,以及状态改变的过程是什么? 使用this.setState改变组件的状态 改变的过程中,React Fiber Reconciler遍历了整个Fiber ...
- [react] 描述下在react中无状态组件和有状态组件的区别是什么?
[react] 描述下在react中无状态组件和有状态组件的区别是什么? 1,无状态组件主要用来定义模板,接收来自父组件props传递过来的数据,使用{props.xxx}的表达式把props塞到模板 ...
- [react] 在react中无状态组件有什么运用场景
[react] 在react中无状态组件有什么运用场景 适用于逻辑简单的纯展示的场景,如资料卡片等 个人简介 我是歌谣,欢迎和大家一起交流前后端知识.放弃很容易, 但坚持一定很酷.欢迎大家一起讨论 主 ...
- react中创建一个组件_如何使用React和MomentJS创建一个Countdown组件
react中创建一个组件 Recently I had to create a Countdown for one of my other projects, and I thought that i ...
- React中的纯组件
React中的纯组件 React提供了一种基于浅比较模式来确定是否应该重新渲染组件的类React.PureComponent,通常只需要继承React.PureComponent就可以定义一个纯组件. ...
- React中的受控组件和非受控组件
一.认识受控组件 在React中,HTML表单的处理方式和普通的DOM元素不太一样:表单元素通常会保存在一些内部的state. 比如下面的HTML表单元素: 这个处理方式是DOM默认处理HTML表单的 ...
- 如何在React中使用功能组件
Have you wondered how to create a component in React? 您是否想过如何在React中创建组件? To answer, it is as simple ...
- React 中的受控组件和非受控组件的区别
React 中受控和非受控的概念通常跟 form 表单组件相关,比如 input ,通过区分与 input 进行数据交互的方式,组件被分成两个不同的派系,受控与非受控. 受控组件 React 中受控组 ...
最新文章
- kbengine0.2.3发布,开源分布式游戏服务端引擎
- 【转】为什么自动车完全不可以犯错误
- hdu4699-Editor【对顶栈】
- 关于虚拟机第二块网卡eth1(仅主机模式)的配置问题
- mysql要将语句反复执行15次_MySQL多表查询疑问
- c 语言编程怎样弄循环语句,C 语言循环语句
- 多线程输出奇数和偶数
- Pyhton爬小说实例解析笔记——爬虫基础
- C语言lowB排序和NB排序
- Pytorch深度学习笔记(02)--损失函数、交叉熵、过拟合与欠拟合
- 虹科资讯| 虹科AR荣获汽车后市场“20佳”维修工具评委会提名奖!
- 三个角度细谈:如何发挥朋友圈广告的威力
- echarts-gl中3d曲面UV参数详解
- 88---Python 以符号的方式给出积分表达式,类似Mathematics
- 王者荣耀——bat批处理文件,自动刷金币版(脱胎于30行Python代码刷金币版),Windows双击即可运行!
- 电子商务:说说转化率
- 0 在c语言中有什么作用,\0在c语言中代表什么?
- iperf 工具使用
- SD卡文件损坏怎么办?SD卡数据恢复用这招
- 磁偏角测试仪TY3300永磁材料磁偏角测量