赞
踩
h5实现仿微信那种页面切换时左右横滑的动画效果,页面前进时新页面从右往左滑,页面后退时旧页面从左往右滑。
之前详细阐述了v1版本的实现思路和代码,后来在实践过程中发现v1存在的诸多问题,已经做了重构,所以这里分享一下新的实现方式,且称为v2版本吧,核心思路还是一样的。(以下内容包含v1和v2)
只实现路由切换动画很简单,只需要给路由出口router-view用transition元素包装一下,加上过渡样式即可:
<transition :name="myName">
<router-view/>
</transition>
但这里要实现的动画效果是页面前进后退不一样,问题的难点就在于如何区分页面前进和后退。
百度了一下,网上实现的方式千奇百怪,但大多都不是我想要的,我希望的效果是:
1.低侵入,不会对已有的代码造成影响;
2.使用方便,不需要额外配置,不会对以后写其他代码造成限制。
其实就是保证封装独立性和代码低耦合。
本文判断前进后退的思路也是借鉴了网上的代码,大概思路:
@/store/index.js
handleRouteChange 方法是在beforeEach导航守卫里使用。
{ state: { directionName: '', // 页面切换方向:slide-left左滑前进,slide-right右滑后退 routeStack: [], // 存储路由栈 }, getters: { directionName (state) { return state.directionName || '' }, routeStack (state) { return state.routeStack || [] }, }, mutations: { PUSH_ROUTE_STACK (state, route) { state.routeStack.push(JSON.parse(JSON.stringify(route))) }, POP_ROUTE_STACK (state) { state.routeStack.pop() }, SET_DIRECRTION_NAME (state, direction) { state.directionName = direction } }, actions: { // 记录页面前进/后退 handleRouteChange ({ getters, commit }, payload) { const { to, from } = payload let len = getters.routeStack.length if (len === 0) { const homePath = '/home' // 项目首页的路由,换成你自己的 if (to.path === homePath) { // 直接打开首页 commit('PUSH_ROUTE_STACK', to) } else { // 直接打开非首页 commit('PUSH_ROUTE_STACK', { path: homePath }) commit('PUSH_ROUTE_STACK', to) } } else if (len === 1) { // 先打开首页,再打开其他路由 commit('SET_DIRECRTION_NAME', 'slide-left') commit('PUSH_ROUTE_STACK', to) } else { let lastBeforeRoute = getters.routeStack[len - 2] if (lastBeforeRoute.path === to.path) { // 打开上一页路由,后退 commit('POP_ROUTE_STACK') commit('SET_DIRECRTION_NAME', 'slide-right') } else { // 前进 commit('PUSH_ROUTE_STACK', to) commit('SET_DIRECRTION_NAME', 'slide-left') } } } } }
@/router/index.js
上述vuex部分的 homePath 就是这里配置的 /home 。
routes: [
{
path: '/',
redirect: '/home'
},
{
path: '/home',
name: 'home',
component: () => import(/* webpackChunkName: "home" */ '@/views/home/index.vue'),
meta: {
title: '首页',
}
},
]
调用vuex里定义的 handleRouteChange 方法。
import router from '@/router'
import store from '@/store'
router.beforeEach((to, from, next) => {
store.dispatch('handleRouteChange', { to, from })
next()
})
这里我把transition过渡部分封装成了组件PageSlide
:
<template> <transition :name="directionName"> <slot></slot> </transition> </template> <script> import { mapGetters } from 'vuex' export default { name: 'PageSlide', computed: { ...mapGetters(['directionName']) }, } </script> <style lang="less" scoped> .slide-left-enter-active, .slide-left-leave-active, .slide-right-enter-active, .slide-right-leave-active { position: absolute; width: 100%; transition: all 0.3s cubic-bezier(0.55, 0, 0.1, 1); } .slide-left-enter, .slide-right-leave-active { opacity: 0; transform: translate(100%, 0); } .slide-left-leave-active, .slide-right-enter { opacity: 0; transform: translate(-100%, 0); } </style>
在App.vue里的路由出口上包装一下。
<template> <div id="app"> <PageSlide> <router-view /> </PageSlide> </div> </template> <script> import PageSlidefrom '@/components/PageSlide' export default { name: 'App', components: { PageSlide, }, } </script>
v1版本通过vuex存储页面历史和页面方向,通过beforeEach全局路由导航守卫来获取要跳转的路由信息,对比判断页面方向。
后来想了一下:
那么改造后一个组件就实现了所有代码逻辑。
v1里路由栈routeStack数据存储的是所有路由route的完整数据,而当前功能其实只用到path这一个数据而已,那就只存储path,同时也减去了 JSON.parse(JSON.stringify(route))
的深克隆操作。
pushRouteStack (route) {
const { path } = route
this.routeStack.push({ path })
},
v1里存储vuex在页面刷新后路由栈routeStack数据会初始化清空,所以这次把routeStack数据同步存储在sessionStorage里,页面刷新后从sessionStorage读取出来。
created () {
this.routeStack = this.getSessionRouteStack()
},
pushRouteStack (route) {
const { path } = route
this.routeStack.push({ path })
this.setSessionRouteStack()
},
popRouteStack () {
this.routeStack.pop()
this.setSessionRouteStack()
},
大概瞄了一眼,vue3.0不兼容的部分应该就只有transition类名吧:
class-enter 修改为 class-enter-from
;
class-leave 修改为 class-leave-from
。
那就全写上:
.slide-left-enter,
.slide-left-enter-from,
.slide-right-leave-active {
opacity: 0;
transform: translate(@distanceX, 0);
}
.slide-left-leave-active,
.slide-right-enter,
.slide-right-enter-from {
opacity: 0;
transform: translate(-@distanceX, 0);
}
更多vue3.0升级可以参考:项目升级vue3.0经验总结
这一版对页面前进后退的判断逻辑进行了重构,考虑了更多细节场景的处理,路由栈初始时默认存放首页路由:
handleRouteChange (to) { const len = this.routeStack.length const currentRoute = this.routeStack[len - 1] // 判断是否是刷新路由 if (currentRoute.path !== to.path) { if (len === 1) { // 初次打开非首页路由,前进 this.setPageForward(to) } else { // len > 1 const lastRoute = this.routeStack[len - 2] if (lastRoute.path === to.path) { // 打开上一页路由,后退 this.setPageBack(currentRoute) } else { if (to.path === this.indexPagePath) { // 中途打开首页,后退,重置路由栈 this.setDirectionName('slide-right') this.resetRouteStack() } else { // 常规情况,前进 this.setPageForward(to) } } } } },
<template> <transition :name="directionName"> <slot></slot> </transition> </template> <script> export default { name: 'PageSlide', props: { // 首页路由path indexPagePath: { type: String, default: '/', }, }, data () { return { directionName: '', // 页面切换方向:slide-left左滑前进,slide-right右滑后退, routeStack: [], // 存储路由栈 } }, watch: { '$route' (val) { this.handleRouteChange(val) } }, created () { this.routeStack = this.getSessionRouteStack() || [{ path: this.indexPagePath }] }, methods: { handleRouteChange (to) { const len = this.routeStack.length const currentRoute = this.routeStack[len - 1] // 判断是否是刷新路由 if (currentRoute.path !== to.path) { if (len === 1) { // 初次打开非首页路由,前进 this.setPageForward(to) } else { // len > 1 const lastRoute = this.routeStack[len - 2] if (lastRoute.path === to.path) { // 打开上一页路由,后退 this.setPageBack(currentRoute) } else { if (to.path === this.indexPagePath) { // 中途打开首页,后退,重置路由栈 this.setDirectionName('slide-right') this.resetRouteStack() } else { // 常规情况,前进 this.setPageForward(to) } } } } }, // 前进 setPageForward (route) { this.setDirectionName('slide-left') this.pushRouteStack(route) }, // 后退 setPageBack () { this.setDirectionName('slide-right') this.popRouteStack() }, // 设置页面方向 setDirectionName (name) { this.directionName = name }, // 重置 resetRouteStack () { this.routeStack = [{ path: this.indexPagePath }] this.setSessionRouteStack() }, pushRouteStack (route) { const { path } = route this.routeStack.push({ path }) this.setSessionRouteStack() }, popRouteStack () { this.routeStack.pop() this.setSessionRouteStack() }, setSessionRouteStack () { try { sessionStorage.setItem('routeStack', JSON.stringify(this.routeStack)) } catch (error) { console.warn('storage is not supported') } }, getSessionRouteStack () { const str = sessionStorage.getItem('routeStack') if (!str) { return null } let val = [] try { val = JSON.parse(str) } catch (error) { console.warn('parse routeStack wrong') } return val }, } } </script> <style lang="less" scoped> @distanceX: 100%; .slide-left-enter-active, .slide-left-leave-active, .slide-right-enter-active, .slide-right-leave-active { position: absolute; width: 100%; transition: all 0.3s cubic-bezier(0.55, 0, 0.1, 1); } .slide-left-enter, .slide-right-leave-active { opacity: 0; transform: translate(@distanceX, 0); } .slide-left-leave-active, .slide-right-enter { opacity: 0; transform: translate(-@distanceX, 0); } </style>
<template> <div id="app"> <PageSlide indexPagePath="/home"> <router-view /> </PageSlide> </div> </template> <script> import PageSlidefrom '@/components/PageSlide' export default { name: 'App', components: { PageSlide, }, } </script>
不管是v1还是v2、vue还是react,核心思想不变,即:
存储路由访问历史数据,获取下一页路由数据,进行对比判断出页面方向。
react版的实现方式上主要需要解决以下问题:
import { withRouter } from 'react-router-dom' class Main extends Component { constructor (props) { super(props) this.state = {} } componentWillReceiveProps (nextProps) { console.log(nextProps.location) } render() { return ( <div>demo</div> ) } } export default withRouter(Main)
const Demo = function (props) {
useEffect(() => {
console.log(props.location)
}, [props.location])
}
剩下的逻辑就和vue大同小异了,自己实现吧。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。