一个有趣的效果--动态生成动画导航

在接下来的这个项目中,我们即将使用纯 JavaScript 和 CSS 来创建一个具有动态动画效果的导航栏。这篇文章将详细解析该代码的实现,包括 HTML 结构、CSS 样式、JavaScript 逻辑等方面,帮助你理解每一个步骤和实现思路。文章内容将逐步拆解,涵盖从页面结构、样式设计到功能实现的各个细节。

项目概述

这个项目的核心目标是创建一个包含动画效果的导航栏。具体功能包括:

  1. 动态导航项:当用户将鼠标悬停在导航项上时,显示一个附加的面板。
  2. 面板动画:面板会根据鼠标悬停的位置进行平滑过渡,显示不同的内容。
  3. 过渡效果:每个导航项的高亮状态和面板显示都有精美的动画效果,增强用户体验。

HTML 结构

HTML 基本框架

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>一个动态动画导航</title>
    <style>
        /* 样式在这里 */
    </style>
</head>

<body>
    <script>
        /* JavaScript 逻辑在这里 */
    </script>
</body>

</html>

HTML 文档是非常标准的结构,包含了 headbody 两大部分:

  1. <head> 部分:定义了页面的字符编码、视口设置和页面的标题。标题为 “一个动态动画导航”,用于描述页面内容。
  2. <body> 部分:里面没有直接的 HTML 内容,而是通过 JavaScript 动态生成和管理导航栏的结构。

导航栏元素

在页面的 body 中,我们没有直接放置导航栏的 HTML 代码,而是通过 JavaScript 动态生成。接下来我们将深入分析这些 JavaScript 代码的工作原理。

CSS 样式解析

全局样式

body, html, ul, p {
    margin: 0;
    padding: 0;
}

这一段代码是用来移除 bodyhtmlulp 元素的默认 margin 和 padding,以确保布局没有多余的间隙。这是前端开发中的常见做法,有助于在不同浏览器中获得一致的效果。

导航栏 .nav

.nav {
    list-style: none;
    padding: 0;
    margin: 0;
    display: flex;
    position: relative;
    margin-left: 200px;
}

.nav 是一个容器元素,负责展示导航栏中的各个导航项。它使用了 flex 布局,使得每个 li 元素可以水平排列。此外,通过 position: relative 来为可能添加的子元素(如下拉面板)提供定位上下文,margin-left: 200px 是为了给导航栏留出空间。

导航项 .nav li

.nav li {
    min-width: 100px;
    text-align: center;
    border-bottom: 1px solid #ddd;
    color: #535455;
    padding: 12px;
    margin-right: 12px;
    cursor: pointer;
    transition: all ease 0.2s;
}

每个导航项 (li) 有如下样式:

  • min-width: 100px:确保每个项至少占据 100px 宽度。
  • text-align: center:使文本居中显示。
  • border-bottom: 1px solid #ddd:为每个导航项添加一个细线,增强视觉效果。
  • padding: 12pxmargin-right: 12px:设置内外边距,使项之间保持一定的间距。
  • cursor: pointer:当鼠标悬停在导航项上时,显示为可点击的手形光标。
  • transition: all ease 0.2s:使所有样式变化(如颜色、背景色、缩放等)具有过渡效果,持续时间为 0.2 秒,效果为平滑过渡。

面板 .nav-panel-wrapper

.nav-panel-wrapper {
    border: 1px solid #dedede;
    position: absolute;
    top: 60px;
    left: 0;
    padding: 12px;
    border-radius: 4px;
    box-shadow: 0px 0px 12px rgba(0, 0, 0, 0.32);
    display: none;
    overflow: hidden;
}

.nav-panel-wrapper 是每个导航项的下拉面板,包含以下样式:

  • position: absolute:使面板相对于 .nav 容器进行绝对定位。
  • top: 60px:将面板放置在导航项下方(假设导航栏的高度为 60px)。
  • border-radius: 4px:为面板添加圆角,使其看起来更加圆滑。
  • box-shadow:为面板添加阴影效果,使其更加立体,增加视觉层次感。
  • display: none:面板默认是隐藏的,只有在用户悬停时才会显示。
  • overflow: hidden:确保面板内容不会溢出其容器。

动画样式

.scale-up-top {
    animation: scale-up-top 0.2s cubic-bezier(0.39, 0.575, 0.565, 1) both;
}

@keyframes scale-up-top {
    0% {
        transform: scale(0.5);
        transform-origin: 50% 0%;
    }
    100% {
        transform: scale(1);
        transform-origin: 50% 0%;
    }
}

.scale-up-top 类通过动画效果使面板从小到大逐渐放大,并且设置了动画的持续时间为 0.2 秒,使用了 cubic-bezier 函数来创建缓动效果。@keyframes scale-up-top 定义了放大过程的具体动画帧:从 50% 的缩放大小(即最小状态)逐渐过渡到 100%(即原始大小)。

JavaScript 逻辑解析

工具类 AnimateNavUtils

AnimateNavUtils 是一个工具类,提供了一些常用的方法,简化了 DOM 操作的代码:

  • $:根据选择器返回文档中的第一个匹配元素。
  • createElement:根据传入的 HTML 字符串创建一个新的 DOM 元素。
  • addClassremoveClasshasClass:分别用于为元素添加、移除、检查 CSS 类。
  • insertNode:将一个新的节点插入到指定的元素中,或者替换现有节点。
  • create:创建一个新的 DOM 元素节点。
  • setStyle:为元素动态设置样式。

这些工具方法大大简化了后续类的实现,使得代码更具可读性和复用性。

动画导航类 AnimateNav

AnimateNav 类是核心部分,负责处理导航栏的渲染、事件绑定和面板的动画效果。

构造函数
constructor({ data }) {
    super();
    this.data = data;
    this.panelDelayTimer = null;
    this.currentIndex = 0;
    this.panelEle = null;
    this.navEle = null;
}

在构造函数中,我们接收一个 data 参数,它是一个包含导航项信息的数组。panelDelayTimer 用来控制面板的显示延迟,currentIndex 用来记录当前导航项的索引,panelElenavEle 分别存储面板和导航栏的 DOM 元素引用。

mount 方法
mount(el) {
    const container = this.isString(el) ? this.$(el) : document.body;
    this.render(container);
}

mount 方法负责将导航栏挂载到指定的 DOM 元素中。如果传入的参数是一个字符串(例如选择器),则查找对应的元素;如果是其他类型,则默认为 document.body

render 方法
render(container) {
    if (!this.isArray(this.data) || this.data?.length === 0) {
        return;
    }
    const node = this.createElement(`
        <ul class="nav">
            ${this.data.map(item => `<li data-sub="${item.sub}" data-index="${item.index}" class="nav-item">${item.text}</li>`).join('')}
            <div class="nav-panel-wrapper"> </div>
        </ul>
    `);
    ...
}

render 方法负责生成导航栏的 HTML 结构并将其插入到页面中。它首先检查 data 是否有效,确保它是一个数组且非空。接着,它动态创建一个包含 <ul class="nav"><div class="nav-panel-wrapper"> 的 HTML 结构。

  • data.map(item => ...) 生成每个导航项的 <li> 元素,并根据 data-subdata-index 设置相应的自定义属性。
  • this.navElethis.panelEle 分别存储了导航栏容器和面板容器的 DOM 元素引用,方便后续操作。
  • 最后,调用 bindEvents 方法来绑定事件处理器。

绑定事件 bindEvents

bindEvents() {
    const items = Array.from(this.navEle.querySelectorAll('.nav-item'));
    items.forEach(item => {
        item.addEventListener('mouseenter', (e) => {
            const index = e.target.dataset.index;
            this.showPanel(index);
        });

        item.addEventListener('mouseleave', () => {
            this.hidePanel();
        });
    });
}

showPanel(index) {
    const item = this.navEle.querySelector(`[data-index="${index}"]`);
    const subItems = item.getAttribute('data-sub');
    this.panelEle.innerHTML = subItems ? subItems : '没有子项';
    this.addClass(this.panelEle, 'scale-up-top');
    this.setStyle(this.panelEle, {
        display: 'block',
        top: `${item.offsetTop + item.offsetHeight + 12}px`
    });
}

hidePanel() {
    this.removeClass(this.panelEle, 'scale-up-top');
    this.setStyle(this.panelEle, { display: 'none' });
}

bindEvents 方法中,我们为每个导航项添加了 mouseentermouseleave 事件监听器:

  • mouseenter:当鼠标进入某个导航项时,调用 showPanel 方法显示对应的面板,并填充子项内容。
  • mouseleave:当鼠标离开导航项时,调用 hidePanel 隐藏面板。

showPanel 方法

showPanel(index) {
    const item = this.navEle.querySelector(`[data-index="${index}"]`);
    const subItems = item.getAttribute('data-sub');
    this.panelEle.innerHTML = subItems ? subItems : '没有子项';
    this.addClass(this.panelEle, 'scale-up-top');
    this.setStyle(this.panelEle, {
        display: 'block',
        top: `${item.offsetTop + item.offsetHeight + 12}px`
    });
}

showPanel 方法根据导航项的索引 (data-index) 显示相应的子项。如果该项有子项(存储在 data-sub 属性中),则将这些子项填充到面板中。如果没有子项,则显示默认的消息('没有子项')。然后,通过 scale-up-top 动画类使面板执行放大动画,并将面板的显示位置设为导航项的下方。

hidePanel 方法

hidePanel() {
    this.removeClass(this.panelEle, 'scale-up-top');
    this.setStyle(this.panelEle, { display: 'none' });
}

hidePanel 方法用于隐藏面板。它会移除面板的动画类 scale-up-top,并通过 setStyle 将面板的 display 属性设置为 none,使其消失。

总结

动画和交互效果

  1. 悬停时显示面板:当用户将鼠标悬停在导航项上时,会触发面板的显示,面板内容来自 data-sub 属性。
  2. 平滑动画:面板在显示和隐藏时应用了平滑的缩放动画,使得界面显得更加动态和流畅。
  3. 动态子项内容:通过自定义的 data-sub 属性,每个导航项可以动态地包含不同的子项或其他内容。

来看一个在线示例如下所示:

jcode

当然这个导航还有可以优化和扩展的空间,如下:

优化和扩展

  1. 响应式设计:当前代码没有完全考虑到移动端的布局,可以进一步优化以适应不同设备屏幕的大小。
  2. 面板延迟:目前面板的显示和隐藏没有延迟处理,未来可以根据需要加入延迟显示、隐藏的效果,提升交互体验。
  3. 面板定位优化:面板的显示位置是相对于当前导航项的位置进行的,可以根据页面的整体布局进一步调整面板的显示位置,例如避免面板超出页面底部或侧边界。

整体来说,这个动态导航效果是通过结合 JavaScript 的 DOM 操作和 CSS 动画来实现的,结构清晰,动画流畅,能够为用户提供良好的互动体验。


夕水
5.3k 声望5.8k 粉丝

问之以是非而观其志,穷之以辞辩而观其变,资之以计谋而观其识,告知以祸难而观其勇,醉之以酒而观其性,临之以利而观其廉,期之以事而观其信。