以下文章记录了为 Netlify 构建百万开发者微型网站的过程。 这个项目是由几个人完成的,我们在这里记录了一些构建过程的一部分——主要关注动画方面,以防对构建类似体验的其他人有所帮助。

从 SVG 构建 Vue 应用程序
SVG 的妙处在于你可以把它想象成一个巨大的战舰游戏,以及它的坐标系。 你实际上是在考虑 x、y、宽度和高度。
<div id="app">
<app-login-result-sticky v-if="user.number" />
<app-github-corner />
<app-header />
<!-- this is one big SVG -->
<svg id="timeline" xmlns="http://www.w3.org/2000/svg" :viewBox="timelineAttributes.viewBox">
<!-- this is the desktop path -->
<path
class="cls-1 timeline-path"
transform="translate(16.1 -440.3)"
d="M951.5,7107..."
/>
<!-- this is the path for mobile -->
<app-mobilepath v-if="viewportSize === 'small'" />
<!-- all of the stations, broken down by year -->
<app2016 />
<app2017 />
<app2018 />
<app2019 />
<app2020 />
<!-- the 'you are here' marker, only shown on desktop and if you're logged in -->
<app-youarehere v-if="user.number && viewportSize === 'large'" />
</svg>
</div>
在更大的应用程序组件中,我们有大型标题,但正如您所看到的,其余部分是一个巨大的 SVG。 从那里,我们将剩余的巨大 SVG 分解成几个组件
- 桌面和移动设备的“糖果乐园”式路径,由 Vuex 存储中的状态有条件地显示
- 有 27 个站点,不包括它们对应的文本,还有许多装饰性组件,例如灌木、树木和路灯,在一个组件中很难跟踪这么多内容,因此它们按年份进行分解
- “您在此处”标记,仅在桌面且您已登录时显示
SVG 非常灵活,因为我们不仅可以在该坐标系中 绘制绝对和相对形状和路径,我们还可以在 SVG 中绘制 SVG。 我们只需要定义这些 SVG 的 x
、y
、width
和 height
,我们就可以将它们安装在更大的 SVG 中,这正是我们将对所有这些组件所做的事情,以便我们可以在需要时调整它们的位置。 组件内的 <g>
代表 group
,你可以将其视为 HTML 中的 div
。
因此,在年份组件中,它看起来像这样
<template>
<g>
<!-- decorative components -->
<app-tree x="650" y="5500" />
<app-tree x="700" y="5550" />
<app-bush x="750" y="5600" />
<!-- station component -->
<app-virtual x="1200" y="6000" xSmall="50" ySmall="15100" />
<!-- text component, with slots -->
<app-text
x="1400"
y="6500"
xSmall="50"
ySmall="15600"
num="20"
url-slug="jamstack-conf-virtual"
>
<template v-slot:date>May 27, 2020</template>
<template v-slot:event>Jamstack Conf Virtual</template>
</app-text>
...
</template>
<script>
...
export default {
components: {
// loading the decorative components in syncronously
AppText,
AppTree,
AppBush,
AppStreetlamp2,
// loading the heavy station components in asyncronously
AppBuildPlugins: () => import("@/components/AppBuildPlugins.vue"),
AppMillion: () => import("@/components/AppMillion.vue"),
AppVirtual: () => import("@/components/AppVirtual.vue"),
},
};
...
</script>
在这些组件中,您可以看到许多模式
- 我们有灌木和树木用于装饰,我们可以通过 props 使用
x
和y
值将它们撒在周围 - 我们可以拥有独立的站点组件,它们也有两个不同的定位值,一个用于大型设备,另一个用于小型设备
- 我们有一个文本组件,它有三个可用的
slots
,一个用于日期,两个用于两行不同的文本 - 我们还在同步加载装饰性组件,并异步加载那些更重的 SVG 站点
SVG 动画

SVG 动画使用 GreenSock (GSAP) 完成,以及他们的新 ScrollTrigger 插件。 我写了一篇关于 如何使用 GSAP 进行动画制作 的指南,用于今年早些时候发布的最新 3.0 版本。 如果你不熟悉这个库,这可能是一个不错的起点。
使用该插件很直观,以下是我们需要的基本功能
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger.js";
import { mapState } from "vuex";
gsap.registerPlugin(ScrollTrigger);
export default {
computed: {
...mapState([
"toggleConfig",
"startConfig",
"isAnimationDisabled",
"viewportSize",
]),
},
...
methods: {
millionAnim() {
let vm = this;
let tl;
const isScrollElConfig = {
scrollTrigger: {
trigger: `.million${vm.num}`,
toggleActions: this.toggleConfig,
start: this.startConfig,
},
defaults: {
duration: 1.5,
ease: "sine",
},
};
}
},
mounted() {
this.millionAnim();
},
};
首先,我们导入 gsap 和我们需要的包,以及来自 Vuex 存储的状态。 我将 toggleActions
和 start
配置设置放在存储中,并将它们传递到每个组件中,因为在工作时,我需要试验要触发动画的 UI 位置,这使我无需分别配置每个组件。
存储中的那些配置看起来像这样
export default new Vuex.Store({
state: {
toggleConfig: `play pause none pause`,
startConfig: `center 90%`,
}
}
此配置分解为
toggleConfig
:在页面向下滚动时播放动画(另一个选项是说重新启动,如果再次看到它,它将重新触发),当它超出视窗范围时暂停(这可能略微有助于性能),并且在向上滚动页面时不会反向重新触发。startConfig
表示当元素的中心向下从视窗高度的 90% 开始时,触发动画开始。
这些是我们为这个项目确定的设置,还有很多其他设置! 你可以通过 这段视频 了解所有选项。
对于这个特定的动画,如果它是横幅动画,不需要在滚动时触发,或者如果它在时间线上稍后出现,我们需要对它进行不同的处理。 我们传入一个 prop,并使用它根据 props 中的数字传入该配置
if (vm.num === 1) {
tl = gsap.timeline({
defaults: {
duration: 1.5,
ease: "sine",
},
});
} else {
tl = gsap.timeline(isScrollElConfig);
}
然后,对于动画本身,我使用的是所谓的 时间线上的标签,你可以将其视为识别播放头上你可能想要挂起动画或功能的时间点。 我们必须确保也将数字 prop 用于标签,这样我们才能使标题和页脚组件的时间线保持分离。
tl.add(`million${vm.num}`)
...
.from(
"#front-leg-r",
{
duration: 0.5,
rotation: 10,
transformOrigin: "50% 0%",
repeat: 6,
yoyo: true,
ease: "sine.inOut",
},
`million${vm.num}`
)
.from(
"#front-leg-l",
{
duration: 0.5,
rotation: 10,
transformOrigin: "50% 0%",
repeat: 6,
yoyo: true,
ease: "sine.inOut",
},
`million${vm.num}+=0.25`
);
百万开发者动画中有很多事情要做,所以我只隔离一部分动作来分解:上面是女孩摆动双腿。 我们有两只腿分别摆动,两只腿都重复多次,并且 yoyo: true
使 GSAP 知道我希望动画在每次交替时反转。 我们正在旋转双腿,但使它看起来更逼真的是 transformOrigin 从腿的中心顶部开始,因此当它旋转时,它围绕膝盖轴旋转,就像膝盖那样:)
添加动画切换

我们希望让用户能够在没有动画的情况下浏览网站,如果他们有前庭疾病,因此我们创建了一个动画播放状态的切换。 切换本身没有什么特别之处——它通过 mutation 更新 Vuex 存储中的状态,正如您所料
export default new Vuex.Store({
state: {
...
isAnimationDisabled: false,
},
mutations: {
updateAnimationState(state) {
state.isAnimationDisabled = !state.isAnimationDisabled
},
...
})
真正的更新发生在最顶层的 App 组件中,我们在那里收集所有动画和触发器,然后根据存储中的状态调整它们。 我们 watch
isAnimationDisabled
属性以查看是否有变化,当发生变化时,我们获取应用程序中所有 scrolltrigger 动画的实例。 我们没有 .kill() 动画,这是一个选项,因为如果我们这样做,我们就无法重新启动它们。
相反,我们要么将它们的进度设置为最后一帧(如果动画已禁用),要么如果我们正在重新启动它们,我们将它们的进度设置为 0,这样它们在设置为在页面上触发时就可以重新启动。 如果我们在这里使用 .restart(),所有动画都会播放,并且我们不会看到它们在我们继续向下滚动页面时触发。 两全其美!
watch: {
isAnimationDisabled(newVal, oldVal) {
ScrollTrigger.getAll().forEach((trigger) => {
let animation = trigger.animation;
if (newVal === true) {
animation && animation.progress(1);
} else {
animation && animation.progress(0);
}
});
},
},
SVG 可访问性
我绝不是可访问性方面的专家,所以如果我在此犯了错误,请告诉我——但我对这个网站进行了相当多的研究和测试,并且很兴奋的是,当我通过语音朗读在 Macbook 上进行测试时,该网站的相关信息是可以遍历的,因此我将分享我们为实现这一点所做的事情。
对于包装所有内容的初始 SVG,我们没有应用角色,以便屏幕阅读器可以在其中进行遍历。 对于树木和灌木,我们应用了 role="img"
,以便屏幕阅读器跳过它,而对于我们应用了唯一 id
和 title
的更详细的站点,它是 SVG 中的第一个元素。 我们还应用了 role="presentation"
。
<svg
...
role="presentation"
aria-labelledby="analyticsuklaunch"
>
<title id="analyticsuklaunch">Launch of analytics</title>
我从 Heather Migliorisi 的这篇文章 和 Leonie Watson 的这篇很棒的文章 中学到了很多东西。
SVG 中的文本会在您浏览页面时宣布它本身,并且会找到链接,所有文本都会被朗读。 这是上面提到的那些插槽的文本组件的样子。
<template>
<a
:href="`https://www.netlify.com/blog/2020/08/03/netlify-milestones-on-the-road-to-1-million-devs/#${urlSlug}`"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="450"
height="250"
:x="svgCoords.x"
:y="svgCoords.y"
viewBox="0 0 280 115.4"
>
<g :class="`textnode text${num}`">
<text class="d" transform="translate(7.6 14)">
<slot name="date">Jul 13, 2016</slot>
</text>
<text class="e" transform="translate(16.5 48.7)">
<slot name="event">Something here</slot>
</text>
<text class="e" transform="translate(16.5 70)">
<slot name="event2" />
</text>
<text class="h" transform="translate(164.5 104.3)">View Milestone</text>
</g>
</svg>
</a>
</template>
这是一段视频,演示了如果我在 Mac 上浏览 SVG 时会听到什么声音
如果您有其他改进建议,请告诉我们!
如果您想查看代码或提交 PR,该仓库也是开源的。
感谢我的同事 Zach Leatherman 和 Hugues Tennier 与我一起完成了这项工作,他们的投入和工作对这个项目至关重要,它只有通过团队合作才能完成! 对 Alejandro Alvarez 的设计表示感谢,他做得非常出色。 向大家致敬。 🙌
太棒了! 我知道 Sarah 会从 ScrollTrigger 中创造出一些很棒的东西! 期待使用它作为灵感! :D
这太棒了,Sarah——很高兴看到你继续展示 SVG 在为动画故事和赋予网页生命力方面是多么引人注目!
我不得不承认,我有点惊讶于硬质矩形白色卡片以及那里使用的
font-size
的重量(与极其精致的插图点相比); 但我承认我在这件事上有点固执己见,哈哈。总的来说,这是一个很棒的示例,展示了使用 SVG 和 GSAP 以及有效地讲述故事的可能性!
我非常感兴趣的是了解这个项目是如何向相关 Netflix 利益相关者进行说明的。时间线是如何展开的。你一定用某种引人入胜的方式推销了 ROI,对吧?
我多次尝试为这类项目进行辩护,这些项目显然需要比静态页面花费更长的时间才能完成,但我要么不得不抽出家庭时间加班加点,因为我强烈地认为要以正确的方式来完成(这显然不可持续),要么就无法按照我期望的精细程度实现页面。也许你可以分享一些关于“获得利益相关者认可”的建议,Sarah。我认为,如果我们能掌握这个非技术方面,我们社区会看到更多 SVG/GSAP 的创意。
好的,还有一点,BRAVO 似乎很恰当 :-)
这真是太棒了!
如何学习并制作像这样的自定义动画 SVG?