此前,我使用了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. github push时,要求密码的问题

    整几次才搞的有点明白: 1 clone 项目 用 SSH: 在github 上如下图 2 C:\Users\<用户名> 下如果有 "_netrc" 文件: 如果含有 如 ...

  2. HTML5 学习笔记(三)——本地存储

    目录 一.HTML4客户端存储 1.1.提交表单发送到服务器的信息 1.2.客户端本地存储概要 二.localStorage 2.1.添加 2.2.取值 2.3.修改 2.4.删除 2.5.跨页面与跨 ...

  3. BZOJ 2152 & 点分治

    Description: 只是打法法塔前测试一下板子 Code: /*================================= # Created time: 2016-04-20 14:3 ...

  4. Android SDK Manager 更新代理配置

    转自:http://www.cnblogs.com/tao560532/p/4483067.html 出现问题: 消除SDK更新时,有可能会出现这样的错误:Download interrupted: ...

  5. ACM : POJ 2676 SudoKu DFS - 数独

    SudoKu Time Limit:2000MS     Memory Limit:65536KB     64bit IO Format:%lld & %llu POJ 2676 Descr ...

  6. dom4j解析示例

    收藏信息.xml <?xml version="1.0" encoding="GB2312" standalone="no"?> ...

  7. MongoDB数据库安装与连接

  8. ASP.NET图形验证码的生成

    效果: 调用方法: int[] r = QAPI.VerifImage.RandomList();//取得随机数种子列 );//产生验证码字符 pictureBox1.Image = QAPI.Ver ...

  9. 安装cocoaPods的详细步骤

    先大概说下安装的步骤: Xcode 这个是开发必须的, HomeBrew RVM Ruby CocoaPods 一.HomeBrew: 打开官网链接:http://brew.sh/index_zh-c ...

  10. 工作总结_js倒计时

    最近在弄一个倒计时抽奖的项目,由于是每天的某个时间段所以,网上也没有找到自己合适的.就自己写了一个留下来以供参考.其中最值得注意的一点是不同种类型的手机对自定义的时间支持方式是不一样的.苹果时间只能支 ...