上一篇文章:react+redux实战(三)----异步交互
已经实现了点赞的交互功能,本文就来实现点击评论按钮弹出评论框功能。
提出问题
对于评论页面,着重有以下两点需求需要考虑:
- 弹出效果。
触发方式为点击新闻详情页的多个评论按钮,评论弹出框为全屏,从页面底部飞入飞出效果,如下图:
在页面飞入的过程中,为了交互效果的美观,需要有遮罩层实现背景色的变化(这个各位应该能想到吧,静态图确实好难表述)。以前我们使用jquery的思路是 :在该评论组件中,开放show和close方法,show()方法就是遮罩不透明度变化并配合页面的飞入效果,hide方法实现遮罩层不透明度消失和页面的飞出效果。需要弹出评论框时就全局找到该组件实例,然后调用其方法。其中以show方法为例,基本流程是给body添加遮罩层,将该组件置于遮罩的子级,然后修改组件的class使其通过transform从页面底部移动出来。同样的,react组件化开发时,我们该如何给body添加遮罩层?又如何将该组件渲染到遮罩层中,而不受该组件引用地点的限制?
- 通信问题。
文章详情页中,评论部分如下:
有三种类型的评论按钮都可以触发弹出评论框:
(1)最下边相对视窗绝对定位的功能条,该组件和新闻内容、整个评论组件为兄弟元素;
(2)Comment组件(也就是一级评论)中的评论按钮
(3)Reply组件(即二级评论,是Comment组件的子组件)中的评论按钮
这三中评论按钮组成了祖父-父-子的关系,我们的Comment-popup组件无论放在哪儿 ,跟他们都无法用简单的父子通信方式(父-->子,通过props;子-->父,通过回调函数)实现,那么该使用什么方式实现通信呢?
解决思路
首先考虑react组件的使用方法:react推崇通过数据流也就是状态的转移来自动改变视图,因此写代码时我们不会像jquery那样在自己的组件中去实现show或者close方法,而是要从父组件中接收props从而改变自己的显示/隐藏状态。当清楚react是靠状态改变视图这一点后,以下的思路就比较清晰了。
在新闻详情页article_show中有多个组件ArticleDetail、RelatedComments、CommentPopup和Toolbar,因为Toolbar和RelatedComments中都有按钮可以触发评论框的弹出,所以也将CommentPopup组件放到了该层级。在article_show 页面中给评论框一个open参数作为开关
//article_show页面
//评论框状态默认为关
constructor(props){
super(props);
this.state={
open:false
}
}
//通过setState方法实现开关
show(){
this.setState({
open:true
})
}
close(){
this.setState({
open:false
})
}
并将该参数作为props传递到CommentPopup组件内部,在组件中通过componentWillReceiveProps方法在接收到新的props时判断参数来决定是开还是关,然后做相应的处理,组件内部的实现我们后边会详细介绍。
//article_show页面中的render方法
//其余组件隐去了不必要的属性传递
render(){
<ArticleDetail/>
<RelatedComments/>
<Toolbar/>
<CommentPopup open={this.state.open}/>
}
CommentPopup组件状态的改变需要从article_show页面接收open属性的变化,也就是所有触发评论按钮的响应都需要回到article_show页面来统一处理,那么在该场景下,发布-订阅模式将是最好的选择。
当RelatedComments或者Toolbar中的评论按钮被点击时,就触发显示评论弹出框的事件,订阅了该事件的article_show页面就会收到通知,然后调用show方法,改变open的属性值,然后视图自动刷新。比如在RelateComments组件中,在按钮的点击动作上绑定如下触发事件
//RelatedComments组件中的按钮来触发弹出评论框的动作
//发布订阅模式的具体实现代码有很多,eventProxy只是我应用的一种
clickMessage(){
eventProxy.trigger('Comment::Popup','msg');
}
在article_show页,componentDidMount()方法中订阅该事件
componentDidMount(){
eventProxy.on('Comment::Popup',(msg)=>{
console.log(msg);
//调用使评论组件出现的方法
this.show();
})
}
通过以上部分介绍的发布订阅模式,即可以比较思路清晰的实现不同组件中的评论按钮都能调用评论框的弹出。
弹窗内部的具体实现
上面已经提到,在article_show页面中会将open参数作为属性传递给CommentPopup组件内部
<CommentPopup open={this.state.open}/>
在该组件中使用componentWillReceiveProps()方法来判断弹窗需要打开还是关闭。componentWillReceiveProps会在每一次有属性值传入时被调用
'use strict'
import React from 'react';
import ReactDOM from 'react-dom';
import Utils from '../common/utils.js';
import eventProxy from '../common/eventProxy.js';
require('./index.less');
class CommentPopup extends React.Component{
constructor(props){
super(props);
}
cancelComment(){
//触发自身组件的关闭事件
eventProxy.trigger('Comment::Hide','hide');
}
//主要利用该方法判断属性值的变化
componentWillReceiveProps(nextProps){
//如果当前open为false,而且下一个状态的open为true,则显示弹窗
if (nextProps.open&&!this.props.open) {
//如果还没有给body(在react中不要直接将组件渲染到body上,选择用最外层的div.react-container)追加遮罩层,则新建
if (document.getElementsByClassName('overlay').length==0) {
// 创建 DOM
this.node = document.createElement('div');
// 给上 ClassName
this.node.className = 'overlay';
// 给 body 加上刚才的 带有 className 的 div
document.getElementsByClassName('react-container')[0].appendChild(this.node);
let modal=(
<div className="com-comment-popup hide">
<div className="comment-popup-hd">
<div className="left"><a href="javascript:void(0)" className="link close-popup" onClick={this.cancelComment.bind(this)}>取消</a></div>
<div className="center">我有意见</div>
<div className="right"><a href="#" className="link submit">发布</a></div>
</div>
<div className="comment-popup-bd">
<form action="/post_comment" method="post">
<textarea name="content" placeholder="我有意见"></textarea>
</form>
</div>
</div>
);
let overlayer=document.getElementsByClassName('overlay')[0];
//利用ReactDOM提供的render方法,将该组件渲染到遮罩中
ReactDOM.render(modal,overlayer);
}
//给遮罩层添加class使其显示
Utils.addClass(document.getElementsByClassName('overlay')[0],'overlay-visible');
//为提高动画效果,使遮罩颜色有变化后,弹窗再执行飞入动作
setTimeout(function(){
Utils.removeClass(document.getElementsByClassName('com-comment-popup')[0],'hide');
},50)
}
//如果当前open状态为true,下一个状态open为false,则隐藏遮罩、关闭弹窗
if (this.props.open && !nextProps.open) {
Utils.addClass(document.getElementsByClassName('com-comment-popup')[0],'hide');
setTimeout(function(){
Utils.removeClass(document.getElementsByClassName('overlay')[0],'overlay-visible');
},50);
}
}
//因为该组件需要被渲染到遮罩层中,而不是常规渲染到该组件被引用的地方,render()方法中返回null即可。
render(){
return null;
}
}
export default CommentPopup;
动画部分主要靠对遮罩层和弹窗修改class实现。以下是相关的css代码
.overlay{
position: fixed;
top: 0;
height: 100%;
width: 100%;
z-index: 999;
background-color: rgba(0,0,0,0.5);
transition-duration:0.4s;
visibility: hidden;
opacity: 0;
}
.overlay-visible{
visibility: visible;
opacity: 1;
}
.com-comment-popup{
display: block;
width: 100%;
height: 100%;
transition:transform 0.5s ease-out;
}
.hide{
transform:translate3d(0,100%,0);
}
其中,对遮罩通过visibility属性的切换实现显示/隐藏,因为visibilty:hidden之后,其子组件也就是评论框也会不可见,并且重要的是,遮罩层代码的存在也不会影响对正常文档流的操作,正式基于这点,我们才能在关闭评论框时,保留相关的DOM结构,这样再次点击评论按钮时就不用再次新建DOM,也不用每次都销毁该DOM,关于这一点,网上的很多例子做的都并不好,jquery中好的思想我们还是可以保留继续使用的。
写在最后
从最开始的毫无头绪到最后功能的基本实现,最重要的经验就是对需求功能的一点点详细划分,然后各个击破。多个有着复杂关系的组件间的通信使用发布订阅模式可以实现组件间的有效解耦,这也是react官方在Flux架构中推荐使用的方法。其次组件化的思想也一直在不断的加深和理解,我们设计一个组件时,肯定要考虑其接口设计(或生命周期等方法),因此解决复杂功能时先理清大的思路,小细节在各个组件中去实现就行。对这种弹出式组件,需要渲染的地方有了变化,所以又用到了ReactDOM的render()方法,这个在写常规组件时比较少用到。最后,就是componentWillReceiveProps方法的使用,在react组件生命周期的介绍中一般也很少会介绍到,但是在系统的状态管理中,该方法却是不可缺少的一环,通过它我们可以真真切切的感受到react通过状态(数据流)来改变视图的特点。