React SPA应用中丝滑般的过场动画

1,751 阅读4分钟

引言


这是我的处女作品,也是构思了很久,不知如何下手🙄。那就先在引言部分说说为什么要开始写文章吧。

我看掘金也有较长时间了,刷到过很多优秀的文章,不仅仅补充了自己的知识盲区,而且还掌握了很多知识细节,但是我们这样子获取的知识都是碎片化的,实际场景中去解释的时候,会发现碎片拼凑有些困难,甚至无法顺畅的解释一些细节知识,这点我个人深有体会,于是决定开始通过写文章的方式来锻炼一下,写一篇文章很花时间,写一篇好的文章更是要花时间,写一篇文章会逼迫自己去了解这个知识的细节点,只有自己先了然于心,理解消化后,写出来的东西才会有读者接受。

废话不多说,下面直接开始。

效果图

pageanimation

知识准备

  1. react-transition-group

  2. window hashchange事件

如何准确的把握用户或者开发人员的跳转方式

作为开发人员,我们代码逻辑里面跳转到下一个页面或者回退到上一个页面的时候,会有好几种方式,而作为用户,其实只有浏览器的前进后退(无论是左右滑屏还是安卓物理键,最终表现为浏览器的前进后退),总结了以下表格

动作 方式 动画
前进 react-router结合history.js的原生api:props.history.push\props.history.replace; window.location.href; window.location.replace;go(n); browser forward; 自右向左
后退 go(n)/goback(); browser back; 自左向右

接下来是重点,如何在用户点击或者程序执行的时候,提前知道页面正确的过场动画呢?首先,我们要知道react-router的router render方法,另外我们要了解window hashchange,如下表格统计了俩个事件与页面组件执行顺序。

动作
props.history.push\props.history.replace router render hashchange
window.location.href; window.location.replace;go(n)/goback(); browser back; browser forward; hashchange router render

对于我们代码层面,props.history.push\replace 完全可以做到自控,统一书写规范,全部使用props.history的api,但是如果在一个旧的项目里面增加过场动画,你会发现,页面跳转基本通过 window.location 的api。注意表格的第二行,先触发hashchange事件的动作,既有前进的方法,也有后退的方法,我们怎么区别呢?

本地维护一个historyStack,只记录hash值,因为如果业务情况复杂的话,很多参数会通过url上动态变化来控制,所以如果直接存全部url的话,达不到我们的目的。既然提到业务复杂会动态改变url的参数,我们可以通过props.history.replace的方式完成,这种方式会触发router render,由于react组件没有任何改变,所以不会引起dom更新,这个方法是可行的。但是个人更加推荐使用 html5 history的 replaceState、pushState的方式去动态改变url的参数,俩个方法不会引起任何render或者hashchange事件,但是因为我们本地维护了historyStack,所以我们需要对这俩个api进行改造。具体看以下代码:

import React from 'react'
import ReactDOM from 'react-dom'
import { HashRouter, Switch, Route } from 'react-router-dom'
import { TransitionGroup, CSSTransition } from 'react-transition-group'
import { createBrowserHistory } from 'history'

import Page1 from 'containers/Page1'
import Page2 from 'containers/Page2'
import Page3 from 'containers/Page3'
import Page4 from 'containers/Page4'

// 一个不准确的 appHistoryStack,不对外暴露接口,不能当做历史记录参考
const setStorage = true
const appHistoryStack = setStorage && sessionStorage.getItem('appHistoryStack') && JSON.parse(sessionStorage.getItem('appHistoryStack')) || [getPathname(window.location.href)]
const replaceState = history.replaceState
const pushState = history.pushState
let appAction = 'FORWARD'
let onAnimation = false
let animationClassName = ''

function getPathname(url) {
  if (url.indexOf('#/') !== -1) {
    const hash = url.split('#/')[1]
    return hash.split('?')[0]
  }
  return window.location.pathname
}

history.replaceState = function() {
  setTimeout(() => {
    const newPathname = getPathname(window.location.href)
    appHistoryStack.splice(appHistoryStack.indexOf(newPathname), 1, newPathname)
  }, 0)
  replaceState.apply(history, arguments)
}
history.pushState = function() {
  setTimeout(() => appHistoryStack.push(getPathname(window.location.href)), 0)
  pushState.apply(history, arguments)
}

window.addEventListener('hashchange', (HashChangeEvent) => {
  const { newURL, oldURL } = HashChangeEvent
  const newURLPathname = getPathname(newURL)
  const oldURLPathname = getPathname(oldURL)
  if (newURLPathname !== oldURLPathname) {
    const newURLIndex = appHistoryStack.indexOf(newURLPathname)
    const oldURLIndex = appHistoryStack.indexOf(oldURLPathname)
    if (newURLIndex === -1) {
      appHistoryStack.push(newURLPathname)
    }
    if (newURLIndex === -1 || newURLIndex - oldURLIndex > 0) {
      appAction = 'FORWARD'
    } else {
      appAction = 'GOBACK'
    }
  } else {
    appHistoryStack.splice(newURLPathname, 1, newURLPathname)
  }
})

ReactDOM.render((
  <HashRouter history={createBrowserHistory()}>
    <Route render={({ location, history }) => {
      if (['PUSH', 'REPLACE'].includes(history.action)) {
        animationClassName = onAnimation && animationClassName ? animationClassName : 'slide-left'
      } else {
        animationClassName = appAction === 'FORWARD' ? 'slide-left' : 'slide-right'
      }
      return (
        <TransitionGroup className={animationClassName}>
          <CSSTransition
            key={location.pathname}
            classNames="animation"
            timeout={304}
            onEnter={() => {
              onAnimation = true
            }}
            onEntered={() => {
              onAnimation = false
              sessionStorage.setItem('appHistoryStack', JSON.stringify(appHistoryStack))
            }} >
            <Switch location={location}>
              <Route exact path="/page1" component={Page1}/>
              <Route exact path="/page2" component={Page2}/>
              <Route exact path="/page3" component={Page3}/>
              <Route exact path="/page4" component={Page4}/>
            </Switch>
          </CSSTransition>
        </TransitionGroup>
      )
    }}/>
  </HashRouter>
), document.getElementById('app'))

具体源码地址:git地址