react+redux实战(四)----实现遮罩动画与多组件间通信

上一篇文章: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通过状态(数据流)来改变视图的特点。

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

推荐阅读更多精彩内容