我们平时使用的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 来声明,会导致过渡结束时可能出现闪烁现象:
vue2
vue2中我们可以在App.vue中监听$route的变化,变化后可以通过key值比较来判断是前进还是后退:
template部分,vue2和vue3有点不同,vue2是transition包裹router-view
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
从vue-router4 源码中可以看到,切换路由时,会传一个StateEntry类型的state,里面记录了:
- back: 上一个页面的path(router.back/浏览器返回按钮 返回到的path)
- current:当前页面的path
- forward:下一个页面的path(router.forward/浏览器前进按钮 进入到的path)
- position: 当前path在历史记录里的位置
根据back和forward,可以判断出是从哪个页面进来的,或者从哪个页面返回的。有时候会有不同页面进来,需要做不同逻辑、不同展示的需求。
2. vue2
从vue-router3 中可以看到,v3版本的state中,只存了一个key: (window.performance/Date).now().toFixed(3)。当然,不管是window.performance.now还是Date.now,都是随着程序运行,越来越大的一个值(返回时的key是不变的):
react-transition-group
react并没有像vue那样,原生提供了一个transition组件;当我们想要实现类似vue中的效果时,就得自己实现了。当然,社区肯定有了对应的实现,就是react-transition-group(好似官方提供的)
其中的<SwitchTransition>有点像vue中的<transition>,提供了out-in和in-out2种模式,但是它不能实现enter和exit同时执行的效果;如果是web端的路由页面间的过渡,倒是可以使用,web上淡出淡入是常用的效果。H5中我们想要的效果是同时执行的,这种就只能使用<TransitionGroup>了。
<TransitionGroup>可以控制一组<Transition> 和 <CSSTransition>,通过前后变化的比较,来管理它们的状态:是进入,还是离开。如果同时多个组件一起改变,那么改变的组件都会有过渡效果。这就是我们想要的。
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);
}
1. nodeRef
<CSSTransition>的nodeRef属性,用来获取子元素的node;如果不指定的话,它会通过findDOMNode方法去获取,这会报一个warning,因为已经废弃了
如果使用nodeRef的话,由于key是变化的,所以需要每次生成新的nodeRef,并且还得把它传递给子组件,所以还得需要一个元素来包裹outlet(直接使用 <Outlet />会导致离开的过渡页面内容立即改变,需要用useOutlet)
2. classNames
由于进入和离开的过渡不同,所以需要动态设置classNames;但是由于它的实现逻辑是通过React.cloneElement;classNames的变化,不会影响到已经clone的元素,所以只能把动态类名提升到TransitionGroup上
问题
- useEffect会导致前进后退切换时,过渡出现问题
<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