H5如何实现App中前进后退的过渡效果

1,198 阅读5分钟

  我们平时使用的app,比如掘金app,当我们从首页,点进去一篇文章时,我们可以看到文章页面是左滑进入的;当我们点击返回时,文章右滑退出(首页也同时左滑和右滑,只是幅度小一些)。
我们的H5页面,通常也会要求类似的效果,那么如何实现呢?

如何做

1. 区分前进与后退
2. 实现不同的过渡效果
前进与后退的判断,通常是在路由变化后,通过比较history.state中存储的数据的变化得出的;
过渡效果,就是css的transition和animation了

vue3

<div class="app-container">
    <router-view v-slot="{ Component }">
        <transition :name="transitionName">
            <component :is="Component" />
        </transition>
    </router-view>
</div>
// App.vue
import{ useRouter } from vue-router

const router = useRouter()

const tabsPath = ['/tab1','/tab2','/tab3'] // tab栏的地址
const transitionName = ref('idle')
let position = window.history.state.position
let delta = 0 // delta > 0: 前进,delta < 0: 后退
// router.push是前进效果,router.back/router.go(-1)是后退效果

router.afterEach((to,from)=>{
    const nextPosition = window.history.state.position
    delta = nextPosition - position
    position = nextPosition
    // tab之间的切换,不需要过渡效果
    if (tabsPath.includes(to.path) && tabsPath.includes(from.path)) {
        transitionName.value = 'idle'
        return
    }
    if (delta > 0) {
        transitionName.value = 'forward'
    } else if(delta < 0) {
        transitionName.value = 'back'
    } else {
        transitionName.value = 'idle'
    }
})
.app-content{
    display: flex;
    flex-wrap: nowrap;
    width: 100vw;
    height: 100%;
    overflow: hidden;
    & > .page { // 路由页面根元素
        flex: 0 0 100%;
        width: 100%;
        height: 100%;
    }
}
.forward-enter-active {
    transition: transform 0.25s ease-out;
    box-shadow: -100vw 0 0 rgba(0, 0, 0, 0.3);
}
.forward-enter-to {
    transform: translate3d(-100%, 0, 0);
}
.forward-leave-active {
    transition: transform 0.25s ease-out;
}
.forward-leave-to {
    transform: translate3d(-20%, 0, 0);
}

.back-enter-from {
    transform: translate3d(-20%, 0, 0);
}
.back-enter-active{
    order: 0;
    transition: transform 0.25s ease-out;
}
.back-leave-from {
    transform: translate3d(-100%, 0, 0);
}
.back-leave-active {
    order: 1;
    transition: transform 0.25s ease-out;
    box-shadow: -100vw 0 0 rgba(0, 0, 0, 0.3);
}

<Transition>组件cn.vuejs.org/guide/built…

基于CSS的过渡可以使用transition或animation,如果只使用其中一种就可以实现你想要的效果,就不要2种混用; 2种混用时,如果没有显式地传入 type prop 来声明,会导致过渡结束时可能出现闪烁现象:

image.png

vue2

vue2中我们可以在App.vue中监听$route的变化,变化后可以通过key值比较来判断是前进还是后退:

template部分,vue2和vue3有点不同,vue2是transition包裹router-view

过渡动效:v3.router.vuejs.org/zh/guide/ad…

const tabsPath = ['/tab1','/tab2','/tab3']
export default {
    data() {
        return {
            position: +window.history.state.key,
            transitionName: 'idle'
        }
    },
    watch: {
        $route(to, from) {
            // 虽然随着程序运行,key会变大,但是想要超过2^53-1,几乎不可能
            // 所以此处就直接转成number计算了
            const delta = +window.history.state.key - this.position
            this.position = +window.history.state.key
            // 剩下的逻辑和vue3一样,只是改成vue2写法
             if (tabsPath.includes(to.path) && tabsPath.includes(from.path)) {
                this.transitionName = 'idle'
                return
            }
            if (delta > 0) {
                this.transitionName = 'forward'
            } else if(delta < 0) {
                this.transitionName = 'back'
            } else {
                this.transitionName = 'idle'
            }
        }
    }
}
// css同上

为什么可以用history.state进行判断

1. vue3
image.png

image.png
从vue-router4 源码中可以看到,切换路由时,会传一个StateEntry类型的state,里面记录了:

  • back: 上一个页面的path(router.back/浏览器返回按钮 返回到的path)
  • current:当前页面的path
  • forward:下一个页面的path(router.forward/浏览器前进按钮 进入到的path)
  • position: 当前path在历史记录里的位置

根据back和forward,可以判断出是从哪个页面进来的,或者从哪个页面返回的。有时候会有不同页面进来,需要做不同逻辑、不同展示的需求。

2. vue2
image.png

image.png
从vue-router3 中可以看到,v3版本的state中,只存了一个key: (window.performance/Date).now().toFixed(3)。当然,不管是window.performance.now还是Date.now,都是随着程序运行,越来越大的一个值(返回时的key是不变的):
img_v3_02ej_f523101f-0f93-4f98-8a0e-1f6aa3de20cg.jpg

img_v3_02ej_d6e578d4-3c0e-463b-b57f-e8bcb929e9fg.jpg


react-transition-group

react并没有像vue那样,原生提供了一个transition组件;当我们想要实现类似vue中的效果时,就得自己实现了。当然,社区肯定有了对应的实现,就是react-transition-group(好似官方提供的)

官网:reactcommunity.org/react-trans…

其中的<SwitchTransition>有点像vue中的<transition>,提供了out-inin-out2种模式,但是它不能实现enter和exit同时执行的效果;如果是web端的路由页面间的过渡,倒是可以使用,web上淡出淡入是常用的效果。H5中我们想要的效果是同时执行的,这种就只能使用<TransitionGroup>了。

<TransitionGroup>可以控制一组<Transition> 和 <CSSTransition>,通过前后变化的比较,来管理它们的状态:是进入,还是离开。如果同时多个组件一起改变,那么改变的组件都会有过渡效果。这就是我们想要的。

image.png

react-router中state.idx类似于vue-router4 中的state.position;react-router没有导航守卫,所以需要使用useLayoutEffect来处理路由变化带来的副作用

function App() {
    const location = useLocation()
    const nodeRef = createRef<HTMLDivElement>()
    const [delta, setDelta] = useState(0)
    const idx = useRef(window.history.state.idx)
 
    useLayoutEffect(() => {
        setDelta(window.history.state.idx - idx.current)
        idx.current = window.history.state.idx
    }, [location])
    return 
         <TransitionGroup className={`app-container ${delta > 0 ? 'forward' : delta < 0 ?'back' : 'idle'}`}>
             <CSSTransition
                key={location.pathname}
                nodeRef={nodeRef}
                timeout={250}
                classNames="slide"
             >
                 <div ref={nodeRef} className="app-content">{useOutlet()}</div>
             </CSSTransitino>
         </TransitionGroup>
}
/* react中,进入的元素总是在前面,这和vue是相反的--vue中进入的总是在后面*/
.forward .slide-enter {
  order: 1;
}
.forward .slide-enter-active {
  transform: translate3d(-100%, 0, 0);
  transition: transform 250ms ease-out;
  box-shadow: -100vw 0 0 rgba(0,0,0,0.3);
}
.forward .slide-exit {
  order: 0;
}
.forward .slide-exit-active {
  transform: translate3d(-20%, 0, 0);
  transition: transform 250ms ease-out;
}
.back .slide-enter {
  transform: translate3d(-20%, 0, 0);
}
.back .slide-enter-active {
  transform: translate3d(0, 0, 0);
  transition: transform 250ms ease-out;
}
.back .slide-exit {
  transform: translate3d(-100%, 0, 0);
}
.back .slide-exit-active {
  transform: translate3d(0, 0, 0);
  transition: transform 250ms ease-out;
  box-shadow: -100vw 0 0 rgba(0,0,0,0.3);
}

react-transition-group.gif 1. nodeRef
<CSSTransition>的nodeRef属性,用来获取子元素的node;如果不指定的话,它会通过findDOMNode方法去获取,这会报一个warning,因为已经废弃

image.png

a626f460-d036-4071-b02a-855e368d1102.jpeg
如果使用nodeRef的话,由于key是变化的,所以需要每次生成新的nodeRef,并且还得把它传递给子组件,所以还得需要一个元素来包裹outlet(直接使用 <Outlet />会导致离开的过渡页面内容立即改变,需要用useOutlet)
2. classNames
由于进入和离开的过渡不同,所以需要动态设置classNames;但是由于它的实现逻辑是通过React.cloneElement;classNames的变化,不会影响到已经clone的元素,所以只能把动态类名提升到TransitionGroup上

image.png

问题

  1. useEffect会导致前进后退切换时,过渡出现问题
  2. <Outlet />为啥不行

总结

1. 元素/组件顺序

  • vue中元素/组件,离开的在前,进入的在后
  • react中元素/组件,进入的在前,离开的在后

2. 类添加的时机--以enter说明

vue
1. 动画开始,enter-from和enter-active同时添加
2. 下一帧,去掉enter-from, 加上enter-to
3. 动画结束,去掉enter-from和enter-to
react
1. 动画开始,加上enter
2. 下一帧,加上enter-active
3. 动画结束,去掉enter和enter-active,加上enter-done