我见过的最令人印象深刻的前端开发示例中,有一些涉及到流畅的页面过渡效果,就像在移动应用中一样。 然而,尽管人们对这类交互的想象力非常丰富,但我访问的实际网站上却很少见到。 实现这些运动效果有很多方法!
我们将构建以下内容
我们将构建这些概念的最简单的提炼版本,以便您可以将其应用于任何应用程序,然后如果您想深入了解,我还会提供此更复杂应用程序的代码。
今天我们将讨论如何使用 Vue 和 Nuxt 创建它们。 页面过渡和动画有很多移动部件(哈哈,我真是太搞笑了),但别担心! 本文中没有时间涵盖的内容,我们将在其他资源中链接到。
为什么?
近年来,网络因与原生 iOS 和 Android 应用体验相比显得“过时”而受到批评。 在两种状态之间进行过渡可以减少用户的认知负荷,因为当用户扫描页面时,他们必须创建包含在页面上的所有内容的心理地图。 当我们从一个页面移动到另一个页面时,用户必须重新映射整个空间。 如果某个元素在几个页面上重复出现,但略有更改,则会模仿我们生物学上训练出的预期体验——没有人会突然出现在房间里或突然发生变化;他们是从另一个房间过渡到这个房间的。 你的眼睛会看到一个相对于你来说较小的物体。 当它们靠近你时,它们会变得更大。 如果没有这些过渡,变化可能会令人震惊。 它们迫使用户重新映射位置,甚至重新理解相同的元素。 正因为如此,这些效果在帮助用户感到宾至如归并在网络上快速获取信息的体验中变得至关重要。
好消息是,实现这些类型的过渡完全可行。 让我们深入了解!
先决条件
如果您不熟悉 Nuxt 以及如何使用它创建 Vue.js 应用程序,我在这里有一篇关于该主题的文章 介绍了相关内容。 如果您熟悉 React 和 Next.js,那么 Nuxt.js 就是 Vue 的等效项。 它提供服务器端渲染、代码分割,最重要的是,提供了页面过渡的钩子。 尽管它提供的页面过渡钩子非常出色,但这不是我们在本教程中实现大部分动画的方式。
为了理解我们今天正在使用的过渡是如何工作的,您还需要了解 <transition />
组件以及 CSS 动画和过渡之间的区别。 我在这里 更详细地介绍了这两者。 您还需要了解 <transition-group />
组件的基本知识,并且 Snipcart 的这篇博文 是了解它的绝佳资源。
即使您在阅读这些文章后会更详细地了解所有内容,但在整个文章中遇到问题时,我都会向您简要介绍正在发生的事情。
入门
首先,我们想启动我们的项目:
# if you haven’t installed vue cli before, do this first, globally:
npm install -g @vue/cli
# or
yarn global add @vue/cli
# then
vue init nuxt/starter my-transitions-project
npm i
# or
yarn
# and
npm i vuex node-sass sass-loader
# or
yarn add vuex node-sass sass-loader
太好了!现在您会注意到我们有一个 pages 目录。 Nuxt 将获取该目录中的任何 .vue
文件并自动为我们设置路由。 非常棒。 我们可以在此处创建一些要使用的页面,在本例中:about.vue
和 users.vue
。
设置我们的钩子
如前所述,Nuxt 提供了一些页面钩子,这些钩子对于页面到页面的过渡非常有用。 换句话说,我们有页面进入和离开的钩子。 因此,如果我们想创建一个动画,使我们能够在页面之间进行很好的淡入淡出,我们可以做到,因为类钩子已经对我们可用。 我们甚至可以为每个页面命名新的过渡,并使用 JavaScript 钩子来实现更高级的效果。
但是,如果我们有一些不想离开和重新进入的元素,而是希望它们过渡位置怎么办? 在移动应用程序中,事物在从一个状态移动到另一个状态时并不总是离开。 有时它们会从一个点无缝地过渡到另一个点,这使得整个应用程序感觉非常流畅。
步骤一:Vuex 存储
我们首先要使用 Vuex 设置一个集中式状态管理存储,因为我们需要保存我们当前所在的页面。
Nuxt 将假设此文件位于 store 目录中,并命名为 index.js
import Vuex from 'vuex'
const createStore = () => {
return new Vuex.Store({
state: {
page: 'index'
},
mutations: {
updatePage(state, pageName) {
state.page = pageName
}
}
})
}
export default createStore
我们同时存储页面,并创建一个允许我们更新页面的 mutation。
步骤二:中间件
然后,在我们的中间件中,我们需要一个名为 pages.js
的脚本。 这将使我们能够在任何其他组件之前访问正在更改和更新的路由,因此它将非常高效。
export default function(context) {
// go tell the store to update the page
context.store.commit('updatePage', context.route.name)
}
我们还需要在我们的 nuxt.config.js
文件中注册中间件
module.exports = {
...
router: {
middleware: 'pages'
},
...
}
步骤三:注册我们的导航
现在,我们将进入我们的 layouts/default.vue
文件。 此目录允许您为不同的页面结构设置不同的布局。 在我们的例子中,我们不会创建一个新的布局,而是修改我们为每个页面重复使用的那个布局。 我们的模板一开始会是这样的
<template>
<div>
<nuxt/>
</div>
</template>
并且 nuxt/
标签将插入我们不同页面模板中的任何内容。 但是,与其在每个页面上重复使用导航组件,不如在这里添加它,它将在每个页面上始终如一地显示
<template>
<div>
<app-navigation />
<nuxt/>
</div>
</template>
<script>
import AppNavigation from '~/components/AppNavigation.vue'
export default {
components: {
AppNavigation
}
}
</script>
这对我们来说也很棒,因为它不会在每次页面重新路由时都重新渲染。 它将在每个页面上保持一致,并且因此,我们*不能*插入我们的页面过渡钩子,而是可以使用 Vuex 和中间件之间构建的内容构建我们自己的钩子。
步骤四:在导航组件中创建我们的过渡
现在我们可以构建导航了,但我在这里也将使用此 SVG 对我们将为大型应用程序实现的基本功能进行一个小演示
<template>
<nav>
<h2>Simple Transition Group For Layout: {{ page }}</h2>
<!--simple navigation, we use nuxt-link for routing links-->
<ul>
<nuxt-link exact to="/"><li>index</li></nuxt-link>
<nuxt-link to="/about"><li>about</li></nuxt-link>
<nuxt-link to="/users"><li>users</li></nuxt-link>
</ul>
<br>
<!--we use the page to update the class with a conditional-->
<svg :class="{ 'active' : (page === 'about') }" xmlns="http://www.w3.org/2000/svg" width="200" height="200" viewBox="0 0 447 442">
<!-- we use the transition group component, we need a g tag because it’s SVG-->
<transition-group name="list" tag="g">
<rect class="items rect" ref="rect" key="rect" width="171" height="171"/>
<circle class="items circ" key="circ" id="profile" cx="382" cy="203" r="65"/>
<g class="items text" id="text" key="text">
<rect x="56" y="225" width="226" height="16"/>
<rect x="56" y="252" width="226" height="16"/>
<rect x="56" y="280" width="226" height="16"/>
</g>
<rect class="items footer" key="footer" id="footer" y="423" width="155" height="19" rx="9.5" ry="9.5"/>
</transition-group>
</svg>
</nav>
</template>
<script>
import { mapState } from 'vuex'
export default {
computed: mapState(['page'])
}
</script>
我们在这里做了几件事。 在脚本中,我们将页面名称从存储中作为计算值引入。 mapState
将让我们从存储中引入任何其他内容,这在我们以后处理大量用户信息时会很方便。
在模板中,我们有一个常规的导航,带有nuxt-link
,这是我们在 Nuxt 中用于路由链接的。我们还有一个类,它将根据页面条件进行更新(当它是关于页面时,它将更改为.active
)。
我们还在许多将更改位置的元素周围使用了<transition-group>
组件。<transition-group>
组件有点神奇,因为它在幕后应用了FLIP的概念。如果您之前听说过 FLIP,您会非常高兴听到这一点,因为它是一种在网络上进行动画的非常高效的方式,但通常需要大量的计算来实现。如果您之前没有听说过 FLIP,那么阅读以了解其工作原理绝对是件好事,也许更重要的是,您不再需要做所有这些事情来实现这种效果!我能听到“太棒了!”吗?
这是使它起作用的 CSS。我们基本上说明了我们希望所有元素在我们创建的“active”钩子上如何定位。然后我们告诉元素在某些内容发生更改时应用过渡。您会注意到,即使我只是沿一个 X 或 Y 轴移动某些内容,我也在使用 3D 变换,因为变换对于性能来说更好,而不是 top/left/margin 来减少绘制,并且我想启用硬件加速。
.items,
.list-move {
transition: all 0.4s ease;
}
.active {
fill: #e63946;
.rect {
transform: translate3d(0, 30px, 0);
}
.circ {
transform: translate3d(30px, 0, 0) scale(0.5);
}
.text {
transform: rotate(90deg) scaleX(0.08) translate3d(-300px, -35px, 0);
}
.footer {
transform: translateX(100px, 0, 0);
}
}
这是一个简化的 Pen,没有页面过渡,但只是为了展示运动
我想指出,我在这里使用的任何实现都是我为放置和移动做出的选择 - 你真的可以创建任何你喜欢的效果!我在这里选择 SVG 是因为它用少量代码传达了布局的概念,但您不需要使用 SVG。我还使用过渡而不是动画,因为它们本质上是声明式的 - 您实际上是在声明:“当 Vue 中切换此类时,我希望将其重新定位在此处”,然后过渡的唯一工作是在任何内容发生变化时描述移动。这对于这种情况非常棒,因为它非常灵活。然后我可以决定将其更改为任何其他条件放置,它仍然可以工作。
太好了!这将为我们提供效果,在页面之间流畅如丝般顺滑,我们还可以为页面的内容提供一个不错的过渡
.page-enter-active {
transition: opacity 0.25s ease-out;
}
.page-leave-active {
transition: opacity 0.25s ease-in;
}
.page-enter,
.page-leave-active {
opacity: 0;
}
我还添加了一个来自Nuxt 网站的示例,以表明您仍然可以在页面内进行内部动画
好的,这对于小型演示有效,但现在让我们将其应用于更现实的东西,例如我们之前的示例。同样,演示站点在这里,带有所有代码的仓库在这里。
概念相同
- 我们将页面的名称存储在 Vuex 存储中。
- 中间件提交突变以让存储知道页面已更改。
- 我们为每个页面应用一个特殊类,并嵌套每个页面的过渡。
- 导航在每个页面上保持一致,但我们有不同的位置并应用了一些过渡。
- 页面的内容有一个微妙的过渡,我们根据用户事件构建了一些交互
唯一的区别是这是一个稍微复杂一点的实现。应用于元素的 CSS 将在导航组件中保持不变。我们可以告诉浏览器我们希望所有元素位于哪个位置,并且由于元素本身应用了过渡,因此每次页面更改时都会应用该过渡,并且它将移动到新位置。
// animations
.place {
.follow {
transform: translate3d(-215px, -80px, 0);
}
.profile-photo {
transform: translate3d(-20px, -100px, 0) scale(0.75);
}
.profile-name {
transform: translate3d(140px, -125px, 0) scale(0.75);
color: white;
}
.side-icon {
transform: translate3d(0, -40px, 0);
background: rgba(255, 255, 255, 0.9);
}
.calendar {
opacity: 1;
}
}
就是这样!我们保持简洁,并在相对容器中使用 flexbox、grid 和绝对定位,以确保所有设备都能轻松转换,并且整个项目中只有很少的媒体查询。我主要使用 CSS 进行导航更改,因为我可以声明性地声明元素及其过渡的位置。对于任何用户驱动事件的微交互,我使用 JavaScript 和 GreenSock,因为它允许我非常无缝地协调大量移动并稳定跨浏览器的transform-origin
,但您可以通过多种方式实现这一点。我可以改进此演示应用程序或在此动画的基础上构建的方法有很多,这是一个快速项目,用于在现实环境中展示一些可能性。
记住要硬件加速并使用变换,您可以实现一些美观、类似原生应用程序的效果。我期待看到您制作了什么!网络在美观运动、放置和交互方面具有巨大的潜力,从而减少了用户的认知负担。
你好,Sarah,感谢你的教程。
步骤 4 缺少一些信息,对于我这个 Vue 新手来说有点令人困惑
您应该提到内容(HTML、JavaScript、SCSS 和 CSS)应该进入文件 components/AppNavigation.vue 中
最好写出对于 SCSS 编译,您需要将代码放在
标签之间,在同一个 .vue 文件中。
也许最好发布整个 vue 文件?
您好,这就是我提到先决条件部分的原因 - 本文假设您具有一定的基本熟悉程度,如果您没有,最好先通读其他材料,这可能会带来更丰富的体验。所有完整的 .vue 文件都发布在链接的两个 github 仓库中!这两个演示都是完整的应用程序,因此逐一介绍每个部分将花费太长时间,而且大部分内容与主要概念无关。如果您正在尝试学习,也强烈推荐 Egghead 的 Nuxt 课程。
嘿,是我,我又来了。我还意识到,SCSS 代码中存在错误。您使用带 3 个参数的 translateX,但 translateX 只期望一个,这就是动画无法正确工作的原因。但是您可以使用 translate3d 代替。
抓住了!这是一个复制粘贴错误,如果您想在更新页面时查看,它在 codepen 中是正确的。谢谢!
好的帖子。对于那些在其环境中无法使用 JS 框架的人,这里有一个旧版本的页面过渡:https://css-tricks.org.cn/add-page-transitions-css-smoothstate-js/
不错。谢谢!
感谢此信息,我刚开始研究 Vue,这确实让我更感兴趣
显然,这最适合平面文件 - 您是否将类似这样的内容连接到 CMS?您是否对它加载动态内容的方式感到满意?(我作为 CMS 工作人员但对 JS 框架很陌生的人提出了这个问题……)
Sarah,这篇文章真是太棒了!
看到这个话题越来越受欢迎,我真的很高兴,因为这是我个人在当前 Web 开发讨论中一直缺少的东西。我将尝试加强我的 Nuxt 技能以更好地理解您的实现
您在其他地方有更多示例,您在哪里应用您的技术?我非常想在过渡时看到“复杂”的转换、主细节视图更改……
如果您有兴趣,我开始创建类似的东西,尽管更通用 - 用于 React(尚未在任何地方宣布):https://github.com/mdugue/react-dip
干杯!
在这种情况下,例如,个人资料图标在“关于”页面之外保持通用。这种方法需要提取共享元素并将其保留在更高级别。这样做会增加一层嵌套。
如果我们可以在两个不同的 Vue 组件中使用一些公共 ID(如“anim-ref”)标记一个 div,而 Vue 会理解同一个 div 将保留在这些 vue 组件中进行渲染,那将是很好的 :)
嘿,Sarah,好文章!我想知道是否可以使用 Vuepress 完成这样的事情?您将如何处理它,因为您将无法使用 Nuxt?
嗨,Sarah!
“AppNavigation.vue”/SCSS 部分可能缺少一条 CSS 规则
没有它,“文本”行在“关于”页面上会超出屏幕。(不太确定为什么!)
在查看 Codepen 时发现,那里一切都很好。
非常感谢您撰写这篇清晰简洁的文章,以及您通常所做的一切 :)
这是一篇很棒的文章!您是否有可能为 Angular 编写一篇?
我做不到,因为我不了解 Angular,但我的朋友兼同事 Simona Cotin 会!敬请期待!
太棒了,Sarah!Vue 正在成为一个强大的力量 - 特别是与 Nuxt 结合使用。喜欢在网络上看到这种类似原生的 UI 过渡。实现起来并不容易,但确实令人鼓舞!
我对 CSS 动画的世界还很陌生——有些动画在 Firefox (59.0.2,Linux) 上有点卡顿/僵硬,但在 Chromium 上却非常流畅。Firefox 的动画支持是否不如 Chromium(如果是这样,是否存在某些类型的过渡/动画需要避免?)或者这似乎是我的机器/设置问题?
不确定外部链接的政策是什么,但这是我看到的
Firefox:https://gfycat.com/gifs/detail/WigglyTepidBufflehead
Chromium:https://gfycat.com/gifs/detail/CanineSociableIndianelephant
(由于录制帧率较低,不太明显,抱歉:/)
无论如何,一如既往,萨拉的文章很棒!
我也在想这个问题,但我的手机上使用的是相同版本的 Firefox——非常卡顿。
不过我想这篇文章更多的是展示可能性,而不是“如何优化动画”。如果我有机会,今天可能会深入研究一下。
很棒的文章!
我在 Firefox (59.0.2,Mac) 上遇到了同样的问题。动画不太流畅,我甚至可以看到过渡过程中的一些“黑线”伪影。
是的,我注意到了同样的情况,并且在这篇文章发布几周前,我向 Firefox 团队展示了这个问题。这实际上并非我第一次注意到该浏览器中的动画滞后,一些其他 Web 动画朋友也注意到自从更新以来,动画速度一直很慢。他们表示正在调查此事,并且浏览器仍在处理这种渲染方式,因此希望它能够得到改进!
这太棒了!我现在正在我的第一个 Nuxt 项目中工作,很想实现一些这样的过渡。感谢分享。
我一直都在寻找这样的东西。非常感谢!很好奇是否可以在没有 Next 的情况下实现这种效果,或者 Next 是否使得实现这种效果变得可能和/或相对容易?
我们实际上正在撰写一篇文章来演示这一点!很快就会发布。主要问题并不是中间件,而是 React 没有等效于 transition-group 的东西,因此使用 Vue 实现起来更容易一些。但并非不可能!敬请期待。
感谢分享这篇文章。我正在考虑将我们公司的网站从 React 迁移到 Vue,基本上只是为了学习目的,并且我们使用了许多像这样的全页面过渡。这对我来说非常有帮助。
我想知道在这里使用
will-change
来提高动画性能是否有任何好处?我们实际上已经在进行硬件加速,因此
will-change
在这里可能不会有太大帮助。除此之外,我最近都避免使用它。它有一些已知问题,并且可能导致图像出现很多问题。更多信息请访问:https://greensock.com/will-change萨拉,如果使用 GSAP,我们是否可以获得更“原生”的性能?我只是想知道——如果我打算使用它——为什么不使用它并使动画更 DRY 呢。有什么想法吗?
嗨!是的——实际上一些微交互已经使用了 GSAP :) 我在这里使用 CSS 是为了让每个人都能理解它是如何工作的,并使用他们喜欢的东西(演示的目的是降低所有受众的入门门槛)。我发现 GSAP 在 Firefox 上的性能要好得多,并且我已经与他们的团队分享了这一点。使用过渡的一个好处是,很容易声明每个页面应该是什么样子,然后保持运动一致,但实际上,一切皆有可能!
这都很棒,但是对于我们这些根本不使用 Vue 或任何 JavaScript 的人来说呢?标题似乎暗示了一种纯 CSS 的方法……
抱歉,标题中哪里传达了纯 CSS 的意思?我一直都不擅长命名东西 :)
顺便说一句,这里还有另一篇文章使用 jQuery 来实现类似的效果,如果你可以使用 jQuery 的话。对于纯 CSS,你肯定可以做一些障眼法,但很难将它们连接起来。
嗨呀!
“我可以通过无数种方法改进这个演示”,我想知道其中的一些方法并进行改进。
哇,这是一个我思考过一段时间的惊人效果!!难以置信
页面过渡一直是我想要更好地掌握的东西;作为一个“在抽象之前学习基础知识”的学习者,是否有可能会有一个此指南的变体,即一个“小屋”系列/课程/指南,用于在没有任何框架的情况下实现页面过渡?