此前,我使用了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. Unity性能优化之 Draw Call原理<转>

    Unity(或者说基本所有图形引擎)生成一帧画面的处理过程大致可以这样简化描述:引擎首先经过简单的可见性测试,确定摄像机可以看到的物体,然后把这些物体的顶点(包括本地位置.法线.UV等),索引(顶点如 ...

  2. python基本图像操作与处理

    # -*- coding: utf-8 -*- from PIL import Image from pylab import * #添加中文支持 from matplotlib.font_manag ...

  3. Leetcode Kth Smallest Element in a BST

    Given a binary search tree, write a function kthSmallest to find the kth smallest element in it. Not ...

  4. (RMQ版)LCA注意要点

    inline int lca(int x,int y){ if(x>y) swap(x,y); ]][x]]<h[rmq[log[y-x+]][y-near[y-x+]+]])? rmq[ ...

  5. BZOJ2506: calc

    Description            给一个长度为n的非负整数序列A1,A2,…,An.现有m个询问,每次询问给出l,r,p,k,问满足l<=i<=r且Ai mod p = k的值 ...

  6. canvas图片处理

    1.灰度 .299 * r + .587 * g + .114 * b; 2.连环画效果 R = |g – b + g + r| * r / 256 G = |b – g + b + r| * r / ...

  7. pythonchallenge 解谜 Level 1

    得到第一关地址后可以进行第一关的解析了. 看起来好神秘的样子.但是也就是把字母 k 变成 m , o 变成 q ,e 变成 g.将字母对应的ASCII的值+2就行了. #-*- coding:utf- ...

  8. android 之httpclient方式提交数据

    HttpClient: 今天实战下httpclient请求网络json数据,解析json数据返回信息,显示在textview, 起因:学校查询饭卡余额,每次都要访问校园网(内网),才可以查询,然后才是 ...

  9. laravel中如何防止直接访问.env文件

    .env文件含有数据库账号密码等敏感数据,在laravel5.2中,在本地访问127.0.0.1/laravel/.env可直接访问到.env. 为避免.env被直接访问,可使用重定向,方法如下: 在 ...

  10. 四、jquery中的事件与应用

    当用户浏览页面时,浏览器会对页面代码进行解释或编译--这个过程实质上是通过时间来驱动的,即页面在加载时,执行一个Load事件,在这个事件中实现浏览器编译页面代码的过程.时间无论在页面元素本身还是在元素 ...