此前,我使用了react-router库来完成单页应用的路由,从而实现组件之间的切换能力。然而,默认页面的切换是非常生硬的,为了让页面切换更加缓和与舒适,通常的方案就是过渡动画。

这里我调研了2种实现方案,它们都能够为react-router实现路由切换时的过渡效果,第1种是react官方自带的ReactCSSTransitionGroup(官方,推荐),第2种则是react-router-transition(非官方)。

下面,我会基于ReactCSSTransitionGroup来分析页面过渡的简单原理以及编程细节,而react-router-transition则大同小异,因此不做赘述。

ReactCSSTransitionGroup

安装

这个库是react官方自带的,它实现于react/lib/ReactCSSTransitionGroup.js。

你可以通过import直接导入这个文件,或者通过命令来安装一个便捷的别名包(仅仅是指向react/lib/ReactCSSTransitionGroup.js):

  • npm install –save react-addons-css-transition-group

原理

ReactCSSTransitionGroup也是一个react组件,我们将在react-router的路由容器组件中引用它,让它替我们在路由切换的时候实现页面间的过渡动画。

首先看一下我的路由配置:

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ReactDOM.render(
    (
        <Provider store={store}>
            <Router history={history}>
                <Route path="/" component={Container}>
                    <IndexRoute component={MsgListPage} />
                    <Route path="msg-list-page" component={MsgListPage}/>
                    <Route path="msg-detail-page/:msgId" component={MsgDetailPage}/>
                    <Route path="msg-create-page" component={MsgCreatePage}/>
                    <Route path="menu-page" component={MenuPage}/>
                </Route>
            </Router>
        </Provider>
    ),
    document.getElementById('reactRoot')
);

一个很简单的路由配置,所有子路由的父容器都是Container组件,路由切换时react-router会将代表子路由的组件(例如MsgListPage)填充到Container的props.children孩子属性中。

既然Container组件是容纳子路由组件的容器,那么可以想到当子路由切换时:Conainter的props.children经历了从老的组件变为了新的组件的过程,如果可以在这个过程中稍作手脚是有机会实现新老组件的平滑过渡的。

先来看一下当前Container当前实现:

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import React from "react";
 
export default class Container extends React.Component {
    constructor(props, context) {
        super(props, context);
    }
 
    componentWillMount() {
        document.body.style.margin = "0px";
        // 这是防止页面被拖拽
        document.body.addEventListener('touchmove', (ev) => {
            ev.preventDefault();
        });
    }
 
    render() {
        return (
            <div id="reactContainer">
                {
                    this.props.children
                }
            </div>
        );
    }
}

它将子路由组件(也就是this.props.children)直接填充了进来,这样实现虽然能够完成路由切换,但是它没有任何的过渡效果。

下面利用ReactCSSTransitionGroup实现过渡效果,代码变成了这样:

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import React from "react";
import ReactCSSTransitionGroup from "react-addons-css-transition-group";
import style from "./Container.css";
 
export default class Container extends React.Component {
    constructor(props, context) {
        super(props, context);
    }
 
    componentWillMount() {
        document.body.style.margin = "0px";
        // 这是防止页面被拖拽
        document.body.addEventListener('touchmove', (ev) => {
            ev.preventDefault();
        });
    }
 
    render() {
        return (
            <ReactCSSTransitionGroup
                transitionName="transitionWrapper"
                component="div"
                className={style.transitionWrapper}
                transitionEnterTimeout={300}
                transitionLeaveTimeout={300}>
                <div key={this.props.location.pathname}
                     style={{position:"absolute", width: "100%"}}>
                    {
                        this.props.children
                    }
                </div>
            </ReactCSSTransitionGroup>
        );
    }
 
}

我们直接套用了ReactCSSTransitionGroup组件,并将子路由组件(this.props.children)包裹在其内部,这样做的目的是:当子路由组件切换时,ReactCSSTransitionGroup可以拦截其内部新老组件的交替过程,从而实现老组件消逝,新组件出现的过渡视觉。

说了那么多,不如看一下切换路由的瞬间DOM树的样子,更加便于理解:

外层div是ReactCSSTransitionGroup引入的父<div>,它内部是有2个子<div>是这段代码引入的:

 
1
2
<div key={this.props.location.pathname}
                     style={{position:"absolute", width: "100%"}}>

默认同一时刻应该只有1个路由组件,那么<div>为什么会出现2个呢?因为ReactCSSTransitionGroup拦截了子路由切换的过程,它在组件替换前将前1个子组件备份了起来,在替换后将新老2个子组件一起填充到父<div>中并开始执行过渡动画,当动画结束后它将老组件移除只保留下新组件:

为什么子<div>要有一个key属性呢?因为ReactCSSTransitionGroup在过渡期间同时维护新老组件需要一个唯一标识加以区分,因为location.pathname代表当前访问的完整路径(包括_k=…),所以用它最合适不过。

CSS动画

至于动画是怎么实现的?第一张图片里你应该可以看到,它为2个子<div>添加了对应的class,一个是enter进入的意思,另外一个是leave离开的意思,我们只需要定义对应的css实现transition动画既可(注意<ReactCSSTransitionGroup>的transitionName属性定义了下述class的前缀):

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
:global(.transitionWrapper-enter) {
    opacity: 0.01;
    transition: opacity 30000ms ease-in;
}
 
:global(.transitionWrapper-enter.transitionWrapper-enter-active) {
    opacity: 1;
}
 
:global(.transitionWrapper-leave) {
    opacity: 1;
    transition: opacity 30000ms ease-in;
}
 
:global(.transitionWrapper-leave.transitionWrapper-leave-active) {
    opacity: 0;
}
 
.transitionWrapper {
    position: relative;
}

这里,:global(classname)的用法是css-loader插件提供的,默认所有css都是通过css-loader局部编译的,从而保证跨组件css名字不冲突。

然而ReactCSSTransitionGroup组件不支持我们控制这些动画class的命名规则,因此我们只能使用全局css,通过:global修饰的class或者id都不会被编码,而是在整个app全局生效,这一块知识可以在这里补充学习

这里我基于transition实现透明度opacity的动画,新组件逐渐显现而老组件逐渐淡化,动画方面可以自行学习。这里重点提一下:

 
1
.transitionWrapper-enter.transitionWrapper-enter-active

我们通常见过2种css表达:

  • .class1 .class2,中间是一个空格,表示class1孩子里的class2元素都应用某css规则。
  • .class1,.class2,中间是一个逗号,表示class1和class2都应用某css规则。

这里.class1.class2是连续写的,表示同时满足class1和class2的元素应用css规则。

为什么不好用?

很多朋友用ReactCSSTransitionGroup发现路由切换动画异常,不符合预期的效果,怎么调试都不行,其实本质都是对原理不够了解。

问题关键在于CSS控制有问题,如果你理解了上述ReactCSSTransitionGroup实现的原理,那么你应该知道新老组件同时出现的时候属于过渡阶段,它们顺序堆积在父<div>中(默认<div>是从上而下堆砌的)。

为了实现过渡效果,理所应当让2个组件重叠在屏幕中央,然后一个淡入一个淡出。因此这就要求子组件要绝对定位(position:absolute),因此你可以看到我给transitionWrapper应用了position:relative,并给<div key=…>应用了position:absolute,width:100%,就是这个道理。

为什么报错?

如果你发现console里有这样的报错:

Warning: setState(…): Cannot update during an existing state transition (such as within render or another component’s constructor). Render methods should be a pure function of props and state; constructor side-effects are an anti-pattern, but can be moved to componentWillMount.

那么说明你在组件的render或者constructor里调用了setState方法,这些应该移到componentWillMount中执行。

我用的是react-redux,之前的某些组件在构造函数里调用了action触发了state修改也被警告了,因此我将初始化组件用的action调用挪到了componentWillMount中,问题迎刃而解。

体验

代码:https://github.com/owenliang/react

扫码访问:

基于ReactCSSTransitionGroup实现react-router过渡动画的更多相关文章

  1. 基于 React 实现一个 Transition 过渡动画组件

    过渡动画使 UI 更富有表现力并且易于使用.如何使用 React 快速的实现一个 Transition 过渡动画组件? 基本实现 实现一个基础的 CSS 过渡动画组件,通过切换 CSS 样式实现简单的 ...

  2. react过渡动画效果的实现,react-transition-group

    本文介绍react相关的过渡动画效果的实现 有点类似vue的transition组件,主要用于组件mount和unmount之前切换时应用动画效果 安装 cnpm install react-tran ...

  3. 12 react 基础 的 css 过渡动画 及 动画效果 及 使用 react-transition-group 实现动画

    一. 过渡动画 # index.js import React from 'react';import ReactDOM from 'react-dom';import App from './app ...

  4. React Router教程

    React Router教程 React项目的可用的路由库是React-Router,当然这也是官方支持的.它也分为: react-router 核心组件 react-router-dom 应用于浏览 ...

  5. React Router API文档

    React Router API文档 一.<BrowserRouter> 使用HTML5历史记录API(pushState,replaceState和popstate事件)的<Rou ...

  6. React Router学习

    React Router教程 本教程引用马伦老师的的教程 React项目的可用的路由库是React-Router,当然这也是官方支持的.它也分为: react-router 核心组件 react-ro ...

  7. React+React Router+React-Transition-Group实现页面左右滑动+滚动位置记忆

    2018年12月17日更新: 修复在qq浏览器下执行pop跳转时页面错位问题 本文的代码已封装为npm包发布:react-slide-animation-router 在React Router中,想 ...

  8. 最新的chart 聊天功能( webpack2 + react + router + redux + scss + nodejs + express + mysql + es6/7)

    请表明转载链接: 我是一个喜欢捣腾的人,没事总喜欢学点新东西,可能现在用不到,但是不保证下一刻用不到. 我一直从事的是依赖angular.js 的web开发,但是我怎么能一直用它呢?看看最近火的一塌糊 ...

  9. React router动态加载组件-适配器模式的应用

    前言 本文讲述怎么实现动态加载组件,并借此阐述适配器模式. 一.普通路由例子 import Center from 'page/center'; import Data from 'page/data ...

随机推荐

  1. 图解HTTP

    1.返回结果的HTTP状态码 a. 2xx 成功: 200 ok 204 No Content  206 Partial Content b. 3XX重定向:301 Moved Permanently ...

  2. php sprintf 函数的用法

    sprintf() 函数把格式化的字符串写入变量中. arg1.arg2.++ 参数将被插入到主字符串中的百分号(%)符号处.该函数是逐步执行的.在第一个 % 符号处,插入 arg1,在第二个 % 符 ...

  3. Asia Hong Kong Regional Contest 2016

    A. Colourful Graph 可以在$2n$步之内实现交换任意两个点的颜色,然后就可以构造出方案. #include <bits/stdc++.h> using namespace ...

  4. 从零开始山寨Caffe·陆:IO系统(一)

    你说你学过操作系统这门课?写个无Bug的生产者和消费者模型试试! ——你真的学好了操作系统这门课嘛? 在第壹章,展示过这样图: 其中,左半部分构成了新版Caffe最恼人.最庞大的IO系统. 也是历来最 ...

  5. Uboot+Linux启动时间优化

    动机 设备启动时间往往是项目立项时的一项重要技术指标.快速的启动时间意味着设备宕机时间的缩短,系统的快速恢复,也能改善用户使用时的体验感受,是一项重要的市场竞争力. 准备 优化启动时间之前,我们首先要 ...

  6. JS typeof与instanceof的区别

    typeof 与 instanceof 通常是用来判断一个变量的类型,二者有如下区别: typeof: 判断一个变量的类型,返回值是字符串形式,返回结果有如下几种: number,boolean,st ...

  7. python实现简单爬虫功能

    在我们日常上网浏览网页的时候,经常会看到一些好看的图片,我们就希望把这些图片保存下载,或者用户用来做桌面壁纸,或者用来做设计的素材. 我们最常规的做法就是通过鼠标右键,选择另存为.但有些图片鼠标右键的 ...

  8. ubuntu16.04 install flash

    今天安装了一下flash,发现不同版本的Ubuntu复制的位置不同,此处介绍Ubuntu16.04的安装方式 1.首先下载flash,下载tar.gz的 2.解压缩到当前目录 3,打开terminal ...

  9. arm,iptables: No chain/target/match by that name.

    最近由于项目需要,需要打开防火墙功能. 公司有 arm linux 3.0x86 linux 3.2x86 linux 2.4 的三个嵌入式.都需要打开防火墙功能. 执行“whereis iptabl ...

  10. java分享第十四天(TestNG Assert详解)

     TestNG Assert 详解org.testng.Assert 用来校验接口测试的结果,那么它提供哪些方法呢? 中心为Assert测试类,一级节点为方法例如assertEquals,二级结点为参 ...