网页页面过渡的原生动画效果

Avatar of Sarah Drasner
Sarah Drasner 发布

DigitalOcean 为您旅程的每个阶段提供云产品。 立即开始使用 200 美元的免费额度!

我见过的最令人印象深刻的前端开发示例中,有一些涉及到流畅的页面过渡效果,就像在移动应用中一样。 然而,尽管人们对这类交互的想象力非常丰富,但我访问的实际网站上却很少见到。 实现这些运动效果有很多方法!

我们将构建以下内容

我们将构建这些概念的最简单的提炼版本,以便您可以将其应用于任何应用程序,然后如果您想深入了解,我还会提供此更复杂应用程序的代码。

今天我们将讨论如何使用 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.vueusers.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,但您可以通过多种方式实现这一点。我可以改进此演示应用程序或在此动画的基础上构建的方法有很多,这是一个快速项目,用于在现实环境中展示一些可能性。

记住要硬件加速并使用变换,您可以实现一些美观、类似原生应用程序的效果。我期待看到您制作了什么!网络在美观运动、放置和交互方面具有巨大的潜力,从而减少了用户的认知负担。