Grtsinry43的前端札记 | 大三技术成长实录 & 学习笔记 | 「岁月漫长,值得等待」
文章
技术学习

利用Vue自定义指令(directives)实现全站动画效果

2024年10月12日 12 分钟阅读 浏览 0 喜欢 0 评论 0

最近一直在学习 React 高阶知识,因此对于主页的开发再一次停滞了,主要也是一段时间内没有找到什么灵感,今天空闲时间打开又添加了一些另外的功能,其中包括了折腾了好久的主页加载 Spring (不是那个 Spring 啦)弹簧效果和模糊效果。

最后的效果在 Grtsinry43 的个人主页 (最近还会考虑继续更新,哭)

环境准备

这里我的网站是使用 Nuxt.js 开发的,使用了"nuxt": "^3.12.4",于是就可以使用 Vue.js 的所有语法和写法,当然也包括自定义指令啦!这就为我们的集成添加了可行性

前置知识

首先我们要知道 Vue 中的指令是类似v-xxx的形式,比如我们平时常用的 v-model v-for 等等都是其提供的指令,当然也为我们保留了自定义的能力。

问题引入

在标准 Vue/Vue+Vite 项目中,其往往位于 /directives 路径,在我们自定义之后在 main.js/main.ts 文件中引入即可

js
import { createApp } from 'vue';
import App from './App.vue';
import scrollSpring from '@/directives/scrollSpring';

const app = createApp(App);

app.directive('scroll-spring', scrollSpring);

app.mount('#app');

而在 Nuxt.js项目中,其主 app 对象创建与挂载,以及 SSR 实现等都是由框架统一管理,为了引入里定自定义指令,我们要借助 nuxt 强大的插件系统。

问题解决

我们可以参考对应的文档: Vue指令

其给出了这样一段示例

js
export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.vueApp.directive('focus', {
    mounted (el) {
      el.focus()
    },
    getSSRProps (binding, vnode) {
      // 你可以在这里提供SSR特定的props
      return {}
    }
  })
})

由于我们无需单独处理 SSR 部分,直接引入插件注册指令即可,Nuxt 会自动扫描并加载 /plugins 路径的文件,无需手动添加。

实现这段动画的大致思路就是,首先所有元素默认 opacity 为 0,并且有向下的位移,当元素移动到视口内即添加标签设置为可见,并恢复位置,当然也可以配合 filter blur 等实现模糊渐显的效果,于是写好如下 css

css
/* 初始状态,元素处于下方且不可见,并带有模糊效果 */
.scroll-item {
  opacity: 0;
  filter: blur(10px); /* 元素模糊 25px */
  transform: translateY(20px); /* 元素初始位移 20px */
  transition: transform 0.5s ease-out, opacity 0.5s ease-out, filter 0.5s ease-out; /* 为 filter 添加动画 */
}

/* 当元素进入视口时,透明度变为 1,模糊度变为 0,且上移回原位 */
.scroll-in {
  opacity: 1;
  filter: blur(0); /* 模糊效果消失 */
  transform: translateY(0); /* 元素回到原始位置 */
  transition: transform 0.5s ease-out, opacity 0.5s ease-out, filter 0.5s ease-out; /* 确保 filter 也有动画 */
}

有了思路之后就写好对应的指令 js /plugins/scrollSpring.js

js
// 导出一个 Nuxt 插件
export default defineNuxtPlugin((nuxtApp) => {
    // 定义一个自定义指令 'scroll-spring'
    nuxtApp.vueApp.directive('scroll-spring', {
        // 当元素被挂载到 DOM 中时调用的钩子
        mounted(el) {
            // 设置 Intersection Observer 的选项
            const options = {
                root: null, // 使用浏览器视口作为根元素
                rootMargin: '0px', // 根元素的边距
                threshold: 0.1 // 交叉的阈值,当 10% 的目标元素在视口内时触发回调
            };

            // 观察者回调函数
            const callback = (entries) => {
                entries.forEach(entry => {
                    // 如果目标元素与视口相交
                    if (entry.isIntersecting) {
                        // 添加动画类
                        el.classList.add('scroll-in'); 
                    } else {
                        // 移出视口时移除动画类
                        el.classList.remove('scroll-in'); 
                    }
                });
            };

            // 创建一个 Intersection Observer 实例,并传入回调函数和选项
            const observer = new IntersectionObserver(callback, options);
            // 开始观察当前元素
            observer.observe(el);
        },
        // 当元素从 DOM 中卸载时调用的钩子
        unmounted(el) {
            // 创建一个空的 Intersection Observer 实例
            const observer = new IntersectionObserver(() => {});
            // 停止观察当前元素
            observer.unobserve(el);
        }
    });
});

这里我们利用 Intersection Observer ,当元素进入视口触发回调添加类名,移出视口则去掉类名

接下来我们只要在对应的元素引入就可以啦! 注意 scroll-itemv-scroll-spring 缺一不可,前者负责决定开始状态,后者是动画和结束效果

这里简单贴一个代码片段,也可以参考我主页对应的仓库~ /pages/index.vue

vue
<div class="slogan font-jb-mono scroll-item" v-scroll-spring>
          <p>Coding,</p>
          <p>build a better world</p>
          <p>together!</p>
        </div>
        <span class="slogan-cn scroll-item" v-if="locale === 'zh'" v-scroll-spring>{{ t('slogan.cn') }}</span>
        <br/>
        <div class="button-container scroll-item" v-scroll-spring>
          <UButton to="https://github.com/grtsinry43" target="_blank"
                   icon="i-grommet-icons:github" style="vertical-align: -4px"
                   class="btn-item github-link bg-blue-400 text-black dark:bg-blue-800 dark:text-white">
            {{ t('buttons.github') }}
          </UButton>
          <UButton :label="t('buttons.learningLog')" color="gray" class="btn-item scroll-item" v-scroll-spring>
            <template #trailing>
              <UIcon name="i-heroicons-arrow-right-20-solid" class="w-5 h-5 btn-more-icon"/>
            </template>
          </UButton>
          <UButton color="gray" class="btn-item scroll-item" disabled v-scroll-spring>
            {{ t('buttons.resume') }}
          </UButton>
        </div>

最后效果

https://blogoss.grtsinry43.com/uploads/24/10/dfd9b723931112b166843ff4b28d68fa.gif/improved

分享此文
评论区在赶来的路上...