开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第7天,点击查看活动详情
前言
在用 Tauri 写 App 时发现与其开多窗口,不如单页面路由,同时为了体现各页面间的跳转,选择通过路由的过渡动效特性体现。在实践过程中查阅了不少资料,由于选择了 Vue3 + Typescript ,踩了不少的坑,在此稍作记录,希望能对你有所帮助。
设定布局为十字形,如下图所示。Demo 地址 dynamic-router。
官方文档搭建
根据官方文档 基于路由的动态过渡 进行设置,让我们先来实现两个界面间简单的左右平移过渡。具体代码可以查看 dynamic-router/two-page。
// ./assets/style/RouterTransition.less
.route-slide-in-left-enter-active,
.route-slide-in-left-leave-active {
transition: all 0.85s ease-in-out;
}
.route-slide-in-left-enter-to {
position: absolute;
left: 0%;
top: 0;
}
.route-slide-in-left-enter-from {
position: absolute;
left: -100%;
top: 0;
}
.route-slide-in-left-leave-to {
opacity: 0;
}
.route-slide-in-left-leave-from {
opacity: 1;
}
.route-slide-out-left-enter-active,
.route-slide-out-left-leave-active {
transition: all 0.85s ease-in-out;
}
.route-slide-out-left-enter-to {
opacity: 1;
}
.route-slide-out-left-enter-from {
opacity: 0;
}
.route-slide-out-left-leave-to {
position: absolute;
left: -100%;
top: 0;
}
.route-slide-out-left-leave-from {
position: absolute;
left: 0;
top: 0;
}
// App.vue
<!-- App.vue -->
<script setup lang="ts">
const build_router_transitionname = (name: any): string => {
return (name === undefined ? '' : name)
}
</script>
<template>
<router-view v-slot="{ Component, route }">
<transition :name="build_router_transitionname(route.meta.transitionName)">
<component :is="Component" />
</transition>
</router-view>
</template>
<style lang="less">
@import "./assets/style/reset.css";
@import "./assets/style/RouteTransition.less";
html, body {
overflow: hidden;
}
</style>
// router/index.ts
import { createRouter, createWebHashHistory, RouteRecordRaw } from "vue-router";
import MainPage from '../views/MainPage.vue';
import LeftPage from '../views/LeftPage.vue';
const routes: RouteRecordRaw[] = [
{
path: '/',
component: MainPage
},
{
path: '/left',
component: LeftPage
}
]
const router = createRouter({
history: createWebHashHistory(),
routes,
})
router.afterEach((to, from) => {
const toDepth = to.path.split('/').length
const fromDepth = from.path.split('/').length
if (to.path == '/left') {
to.meta.transitionName = "route-slide-in-left";
} else if (to.path == '/' && from.path == '/left') {
to.meta.transitionName = "route-slide-out-left";
}
})
export default router;
但是运行 pnpm run dev 后发现并没有出现预期的过渡效果,这是为什么呢?
细心的同学应该已经发现了,router 实际用到的 transition 是以 router.transition 来决定的,但实际赋值的却是 router.transitionname,正是这两者的不一致导致了过渡效果的失效。英文文档并不受此影响,pr 已通过并合并,所以平时还是应该以英文文档为主。
将 transition 统一为 router.transitonname, 现在我们就可以看到两个界面平滑的过渡了。
让我们再添加一点点细节,就可以实现十字形界面的平滑过渡了。具体代码可以查看 dynamic-router/all-page。
// router/index.ts
import { createRouter, createWebHashHistory, RouteRecordRaw } from "vue-router";
import MainPage from '../views/MainPage.vue';
import LeftPage from '../views/LeftPage.vue';
import RightPage from '../views/RightPage.vue';
import UpPage from '../views/UpPage.vue';
import DownPage from '../views/DownPage.vue';
const routes: RouteRecordRaw[] = [
{
path: '/',
component: MainPage
},
{
path: '/left',
component: LeftPage
},
{
path: '/right',
component: RightPage
},
{
path: '/up',
component: UpPage
},
{
path: '/down',
component: DownPage
},
]
const router = createRouter({
history: createWebHashHistory(),
routes,
})
router.afterEach((to, from) => {
if (to.path == '/right') {
to.meta.transitionName = "route-slide-in-right";
} else if (to.path == '/left') {
to.meta.transitionName = "route-slide-in-left";
} else if (to.path == '/up') {
to.meta.transitionName = "route-slide-in-up";
} else if (to.path == '/down') {
to.meta.transitionName = "route-slide-in-down";
} else if (to.path == '/' && from.path == '/right') {
to.meta.transitionName = "route-slide-out-right";
} else if (to.path == '/' && from.path == '/left') {
to.meta.transitionName = "route-slide-out-left";
} else if (to.path == '/' && from.path == '/up') {
to.meta.transitionName = "route-slide-out-up";
} else if (to.path == '/' && from.path == '/down') {
to.meta.transitionName = "route-slide-out-down";
}
})
export default router;
<!-- views/MainPage.vue -->
<script setup lang="ts">
import router from '../router'
const jumpTo = (to: any) => {
router.push({ path: to })
}
</script>
<template lang="">
<div class="container">
<div id="title" class="title">
This is the Main Page
</div>
<div class="action">
<el-button type="primary" round @click="jumpTo('/left')">
to Left
</el-button>
<el-button type="primary" round @click="jumpTo('/right')">
to Right
</el-button>
<el-button type="primary" round @click="jumpTo('/up')">
to Up
</el-button>
<el-button type="primary" round @click="jumpTo('/down')">
to Down
</el-button>
</div>
</div>
</template>
<style lang="less" scoped>
.container {
position: relative;
width: 100%;
height: 100vh;
background-color: #e6f7ff;
}
.title {
padding-top: 20px;
text-align: center;
}
.action {
margin-top: 20px;
text-align: center;
}
</style>
使用第三方库
动画效果全部手写相信大部分人是不愿意的,简单好用的第三方库当然备受青睐,我们的原则是 偷懒! 提升效率!
在这里我们选择 Animate.css 作为动画库,相信有了它,一定能大大提升开发效率吧!
现在让我们重新添加一点点细节。
// router/index.ts
...
router.afterEach((to, from) => {
if (to.path == '/right') {
to.meta.transitionName = "animate__animated animate__slideInRight";
} else if (to.path == '/left') {
to.meta.transitionName = "animate__animated animate__slideInLeft";
} else if (to.path == '/up') {
to.meta.transitionName = "animate__animated animate__slideInDown";
} else if (to.path == '/down') {
to.meta.transitionName = "animate__animated animate__slideInUp";
} else if (to.path == '/' && from.path == '/right') {
to.meta.transitionName = "animate__animated animate__slideOutRight";
} else if (to.path == '/' && from.path == '/left') {
to.meta.transitionName = "animate__animated animate__slideOutLeft";
} else if (to.path == '/' && from.path == '/up') {
to.meta.transitionName = "animate__animated animate__slideOutUp";
} else if (to.path == '/' && from.path == '/down') {
to.meta.transitionName = "animate__animated animate__slideOutDown";
}
})
...
然后意外的发现动画的运行时间是跑出来了,动画没出来,why?
可不可能是 animate.css 本身失效了?让我们添加给 title 添加个动效做下测试。
// views/MainPage.vue
<template lang="">
...
<div class="action">
...
<el-button type="primary" round @click="toggleAnime">
Anime!
</el-button>
</div>
</template>
<script setup lang="ts">
...
const toggleAnime = () => {
let ele_title = document.getElementById('title');
if (ele_title === null) {
alert("No such element");
} else {
ele_title.setAttribute('class', 'title animate__animated animate__bounce')
setTimeout(() => {
if (ele_title != null) {
ele_title.setAttribute('class', 'title')
}
}, 800);
}
}
</script>
发现一切正常,说明 animate.css 本身并没有问题。
那会不会是像最初的那个 transition 和 transitionName 对不上类似,不是赋像 animate__animated animate__bounce 这样的值,稍加试验我们也可以发现并不是这样,并且观察 vue-devtools 也可以观察到 router 的 transition name 是对应正确的,那会是什么原因呢?
万变不离其宗,不如我们去看看 vue 的 transition。
翻看文档我们可以发现,transiton 的 name 其实并不是实际生效的 css,vue 会对其进行自动处理添加尾缀,如 enter-active, enter-from 等,由于之前我们都是手打的,补齐了这部分尾缀,因此实现了动画效果,那我们是不是可以合理怀疑 animate.css 不符合这部分的要求,所以才导致了动画效果并未触发。好像很有道理的样子,正好上面我们对 title 进行了点小改动,让我们再对它下下手。
<template>
...
<transition name="animate__animated animate__bounce">
<div id="title" class="title">
This is the Main Page
</div>
</transition>
...
</template>
发现的确,标题现在失去了动画效果,按照之前的猜想,我们现在给它手动指定好动画效果。
<template>
...
<transition name="custom"
enter-active-class="animate__animated animate__tada"
leave-active-class="animate__animated animate__bounceOutRight">
<div id="title" class="title" v-show="showTitle">
This is the Main Page
</div>
</transition>
...
</template>
<script setup lang="ts">
...
const showTitle = ref(false);
const toggleAnime = () => {
showTitle.value = !showTitle.value;
}
</script>
终于,标题重新动起来了!说明我们的猜想是没有错的,也和文档中的 Custom Transition Classes 一致,说明就应该这么做。
那最后当然就是重新添加"细节"了,由于各种原因,并不能百分百达到手写的效果,相较起来也没有方便多少。具体代码可以查看 dynamic-router/animated。
<!-- App.vue -->
<script setup lang="ts">
const build_router_transitionname = (name: any): string => {
return (name === undefined ? '' : name)
}
</script>
<template>
<router-view v-slot="{ Component, route }">
<transition
:name="route.path"
:enter-to-class="`animate__animated ${route.meta.transitionEnterFrom}`"
:leave-to-class="`animate__animated ${route.meta.transitionLeaveTo}`">
<component :is="Component" />
</transition>
</router-view>
</template>
<style lang="less">
@import "./assets/style/reset.css";
@import "./assets/style/RouteTransition.less";
html, body {
overflow: hidden;
}
</style>
// router/index.ts
router.afterEach((to, from) => {
if (to.path == '/right') {
to.meta.transitionEnterFrom = "animate__slideInRight";
to.meta.transitionLeaveTo = "animate__slideOutLeft";
} else if (to.path == '/left') {
to.meta.transitionEnterFrom = "animate__slideInLeft";
to.meta.transitionLeaveTo = "animate__slideOutRight";
} else if (to.path == '/up') {
to.meta.transitionEnterFrom = "animate__slideInUp";
to.meta.transitionLeaveTo = "animate__slideOutDown";
} else if (to.path == '/down') {
to.meta.transitionEnterFrom = "animate__slideInDown";
to.meta.transitionLeaveTo = "animate__slideOutUp";
} else if (to.path == '/' && from.path == '/right') {
to.meta.transitionEnterFrom = "animate__slideInLeft";
to.meta.transitionLeaveTo = "animate__slideOutRight";
} else if (to.path == '/' && from.path == '/left') {
to.meta.transitionEnterFrom = "animate__slideInRight";
to.meta.transitionLeaveTo = "animate__slideOutLeft";
} else if (to.path == '/' && from.path == '/up') {
to.meta.transitionEnterFrom = "animate__slideInDown";
to.meta.transitionLeaveTo = "animate__slideOutUp";
} else if (to.path == '/' && from.path == '/down') {
to.meta.transitionEnterFrom = "animate__slideInUp";
to.meta.transitionLeaveTo = "animate__slideOutDown";
}
})
好像会抖?还会卡?!
看到小标题先不要怕,如果你使用的就是我上面写的代码,其实是不会遇到这个问题的。但往往动画效果会根据实际情况而有所不同,这个时候页面就可能出现抖动或者卡的现象了。由于这部分的效果 gif 图无法展示,想要直观观察的话请 clone dynamic-router/shake 分支代码,并在本地运行。
实际的抖动很容易触发,而我们实际上修改的地方只有一处,即 RouteTransition.less 中的动画效果:
// ./assets/style/RouterTransition.less
// before
.route-slide-in-left-leave-to {
opacity: 0;
}
.route-slide-in-left-leave-from {
opacity: 1;
}
// after
.route-slide-in-left-enter-to {
position: absolute;
left: 0%;
top: 0;
}
.route-slide-in-left-enter-from {
position: absolute;
left: -100%;
top: 0;
}
相信反应快的同学已经知道原因了。没错,就是因为元素占位问题:在 Left Page 的进入动画执行到最终的时候,由于我们并没有对 Main Page 设置执行动画,其元素始终在文档中保持着相应位置,最终路由完成过渡又要求它一下子消失,因此导致了抖动的问题。
那应该怎么样改动让他不抖动呢?很简单,让元素不占位即可。我们可以通过修改过渡效果,如设置 opacity:0 或 display:none 来让元素不占位。当然也应该还有其他方法可以做到,欢迎补充。
好像还是有点问题
确实还是有点问题,不知道你能不能想到。
现在让我们来假设这样一个场景,Mainpage 为 100vh,是个简单的展示界面,Leftpage 或者 Rightpage 是个表单界面,超过 100vh, 此时触发路由过渡动画后将会发生什么事情?让我们来做个简单的实验吧。具体代码请 clone dynamic-router/height 分支代码,并在本地运行。
当我们前往 Left Page,下拉至页面最下端,点击最下面的跳转按钮,我们可以看到 Main Page 被强行拉长,留了一段空白用以匹配不同页面间缺少的长度。很明显,这是我们不想要的,而原因也很简单,就是不同页面间长度不匹配造成的。原因我们至到了,那么该怎么解决呢?
我们可以看到 Mainpage 最后被强行拉长了,留了一段空白用以匹配不同页面间缺少的长度。很明显,这是我们不想要的,那么怎么解决呢?
- 所有页面都添加一个最外层的container,高度应填充满整个页面,同时所有组件需被包含于该 container,不允许 absolute 等定位
- 在路由过渡时自动滚动至最顶层
很明显,方案一限制性很大,方案二可行,但额外的动画效果不一定是我们想要的,具体选择哪个,就看实际需要了。当然,笔者相信肯定还有更好的方案来解决这一问题,欢迎补充!