以前也写过一个差不多的,但上一个写的存在很多问题,尤其是css要使用 justify-content: space-around才能正常计算下滑宽度,导致了在很多场景都不实用,其实那一天就想到更好的解决办法了,但是因为懒......就一直没更新文章......🙈


最终效果图↓


1.使用Vue制作一个没有动画的初始导航栏

HTML代码

<div class="nav" style="margin: 25px">
    <!-- 标题列表 -->
    <div class="nav-list">
      <div class="nav-tab" v-for="(item, index) in list" :key="index" @click="onClick(index)" :class="{ active : index === currentIndex }">{{ item }}</div>
    </div>
    <!-- 下划线 -->
    <i class="nav-line"></i>
</div>

JS代码

export default {
  data() {
    return {
      currentIndex: 0,
      list: ["首页", "热门", "假面骑士", "叶问有枪", "基努里维斯"]
    }
  },
  methods: {
    // 点击某个选项时执行
    onClick(index) {
      this.currentIndex = index
    },
  },
}

CSS代码

.nav {
  position: relative;
}

.nav-list {
  width: 100%;
  display: flex;
  align-items: center;
  gap: 20px;
}

.nav-tab {
  height: 50px;
  line-height: 50px;
  cursor: pointer;
}

.active {
  color: red;
  font-weight: bold;
}

.nav-line {
  width: 18px;
  height: 4px;
  border-radius: 2px;
  background-color: red;
  position: absolute;
  bottom: 0;
}

至此,实现了以下效果↓

上面的代码应该不难理解吧,可以看到,现在只是单纯的修改了点击元素的颜色,而下划线还是固定在最开始的地方,下一步就需要为下划线计算应该滑动的距离


2.获取元素在网页中的位置信息

这里首先要介绍一个API,getBoundingClientRect()

用法呢也很简单↓

let DOMRect = DOM.getBoundingClientRect()

它的返回值是一个 DOMRect 对象,这个对象是一组矩形数据的集合,就是该元素的 CSS 边框大小。拥有 left, top, right, bottom, x, y, width, 和 height 这几个以像素为单位的只读属性用于描述整个边框。除了 width 和 height 以外的属性是相对于视图窗口的左上角来计算的

看这两张图大概能明白这个API的作用吧,它能获取到给定元素距离你当前网页左上角的距离信息,以及元素自身的宽高,这个宽高等于元素的offsetWidth与offsetHeight值。这个API的兼容性还挺好的,看网上写的IE5以后都能支持,如果对这个API还有不懂的可以去搜,教程一堆一堆的,我就不细说了。

注意:UniApp项目中请使用boundingClientRect(callback)

如果你的项目使用的是uniapp进行开发,这里就不要使用getBoundingClientRect了,为了符合小程序的语言规范以及避免在小程序上出现bug,uniapp中不应使用DOM的原生方法,uniapp官方已经提供了对应的boundingClientRect(callback)方法,该方法返回的数据内容跟DOM提供的API基本一致,不同的是uniapp的方法返回的数据是在回调函数的参数里携带的,具体用法请挪步uniapp官网进行查看↓

https://uniapp.dcloud.net.cn/api/ui/nodes-info.html#nodesref-boundingclientrect


3.计算下划线移动的距离,添加滑动动画

一张简单的示意图(丑😑),其中,黑色边框就是我们的网页视窗主体,黄色部分就是我们的导航栏,元素A/B就是导航栏里的两个选项。参考我们上面步骤1的代码产生的效果,下划线一开始会贴在导航栏的起始位置,也就是这张图里黄色导航栏的左边,而如果我们想将下划线移动到元素A的正中间的下方,

其实就也就是计算出 a-c+b 的结果,这个结果就是我们需要移动的距离。其中:

a代表:当前点击元素距离网页视口左侧的坐标
b代表:当前点击元素宽度的一半
c代表:导航栏元素距离网页视口左侧的坐标

也就是说 最终移动距离 = 当前选项距离视口左侧距离 - 整个导航栏距离视口左侧距离 + 当前选项宽度的一半

而这些我们都能获取到,当我们获取到了最终距离后,就可以动态赋值给下划线的left样式,然后下划线就可以移动到我们需要的位置了,所以还要为下划线元素添加动态style样式,因此有了以下代码↓

别忘了给下划线自身样式添加transform: "translateX(-50%)",这样才可以保证移动后是居中的哦~

<div class="nav" style="margin: 25px">
    <!-- 标题列表 -->
    <div class="nav-list">
      <div class="nav-tab" v-for="(item, index) in list" :key="index" @click="onClick(index)" :class="{ active : index === currentIndex }">{{ item }}</div>
    </div>
    <!-- 下划线 -->
    <i class="nav-line" :style="inlineStyle"></i>
 </div>
data() {
    return {
      currentIndex: 0,
      io: null,
      list: ["首页", "热门", "假面骑士", "叶问有枪", "基努里维斯"],
      leftWidth: null, // 导航栏距离窗口左侧的坐标
      inlineStyle: {
        transform: "translateX(-50%)",
        transition: "left 0.2s ease-in-out",
        left: 0,
      },
    }
  },
methods: {
    // 点击某个选项时执行
    onClick(index) {
      this.currentIndex = index
      this.lineMove(index)
    },
    // 计算下划线滑动距离
    lineMove(index) {
      // 获取当前点击的DOM元素
      let dom = document.getElementsByClassName("nav-tab")[index]
      // 使用DOM对象提供的getBoundingClientRect方法获取该元素相对于当前网页视口的位置信息
      let domRect = dom.getBoundingClientRect()
      // 最终移动距离 = 当前选项距离视口左侧距离 - 整个导航栏距离视口左侧距离 + 当前选项宽度的一半
      this.inlineStyle.left = domRect.left - this.leftWidth + (domRect.width / 2) + "px"
    },
  },
  mounted() {
    // 初始获取导航栏距离窗口左侧距离
    this.leftWidth = document.querySelector(".nav-list").getBoundingClientRect().left
    this.lineMove(0)
  },

看图↑,到这里,我们需要的效果已经实现出来了

存在问题:

但是仍然存在一个小问题,那就是页面初始化的时候下划线也会有一个滑动的动画,这是因为我们添加了一个固定的transition: "left 0.2s ease-in-out",这就导致页面在初始化后第一次计算下划线到第一个选项的距离时,CSS的过渡效果也奏效了。其实这也没什么影响,但是能解决还是解决一下最好,我这里使用了一个笨办法来解决↓

解决方案:

思路就是页面初始加载的时候,我们不要为下划线添加过渡效果,当后续我们点击导航时,再为下划线添加过渡效果。

data() {
    return {
      inlineStyle: {
        transform: "translateX(-50%)",
        transition: "unset", // 无过渡效果
        // transition: "left 0.3s ease-in-out",
        left: 0,
      },
    }
  },
methods: {
    // 点击某个选项时执行
    onClick(index) {
      this.currentIndex = index
      // 点击时修改过渡效果
      this.inlineStyle.transition = "left 0.2s ease-in-out"
      this.lineMove(index)
    },
  },

OK,解决了,这是一个比较简单易懂的解决办法,其实也可以为父元素添加事件并使用vue的once修饰符,这样只在第一次点击的时候修改过渡效果就可以了。

你以为这就完了吗?其实还有一个问题,那就是页面尺寸变化时,我们的下划线位置可就错了。


4.监听导航栏组件尺寸变化,修改下滑线位置

不管是在开发中,还是日常使用中,经常因网页窗口化或者其他各种因素导致页面内元素尺寸发生变化,而我们这个导航栏组件只在点击导航选项时才去计算了下划线的滑动距离,因此在元素尺寸发生变化时就会出现位置错误的bug↓

其实这个也好解决,只要监听一下网页尺寸的变化,也就是window.onresize事件,当页面尺寸变化时,我们再根据当前的选项,重新调用一下lineMove方法就好了,但是吧,onresize这个事件并不是万能的,也有很多的缺点。

首先,onresize事件是window对象的事件,并不能单独的给某个DOM元素绑定,要监听尺寸就只能监听整个网页的尺寸,其次吧,有时候网页窗口尺寸虽然变化了,但他内部的DOM元素尺寸不一定也跟着发生了变化,这样监听onresize事件就显得有些多余。这只是部分原因,这里不推荐使用onresize事件进行监听。

ResizeObserver 检测DOM尺寸变化

ResizeObserver API是一个比较新的API,ResizeObserver可以同时监听一个或者多个DOM元素,当监听的DOM元素尺寸发生变化时,ResizeObserver的回调函数就会触发,并且回调会将变化后的DOM及其尺寸信息放入第一个参数中,供我们使用↓

var io = new ResizeObserver( entries => {
  console.log('contentRect信息数组',entries);
});
// 观察一个或多个元素
io.observe(DOM);
io.observe(document.body);

ResizeObserver回调函数的第一个参数内容

注意啊,这里的contentRect内容信息,指的是元素的contentbox的信息,不是我们通常使用的borderbox的信息,contentbox与borderbox的区别我就不在这里讲了,做前端的这些都应该明白,不明白就去问度娘吧,绝对把你整的明明白白的。borderBoxSize属性中才是borderbox的值,不过只有宽高两个值。

注意:ResizeObserver存在兼容性问题,可以使用polyfill(兼容补丁)解决

由于ResizeObserver是一个比较新的API啊,所以兼容性比较差,IE浏览器是完全不兼容的,chroma、Firefox等主流浏览器也只有高版本的才支持该特性,我这里使用resize-observer-polyfill来使其兼容各大浏览器,可以兼容到IE9。

如果你的项目使用了elementUI,就可以直接引入resize-observer-polyfill了,不需要再npm或者yarn安装该依赖了,elementUI里的组件也是使用了resize-observer-polyfill来监听尺寸变化,所以我们的依赖包里已经有该依赖文件了。

导入该依赖,修改我们的代码↓

import ResizeObserver from 'resize-observer-polyfill'

export default {
  data() {
    return {
      currentIndex: 0,
      io: null,
      list: ["首页", "热门", "假面骑士", "叶问有枪", "基努里维斯"],
      leftWidth: null, // 导航栏距离窗口左侧的坐标
      inlineStyle: {
        transform: "translateX(-50%)",
        transition: "unset",
        left: 0,
      },
    }
  },
  methods: {
    // 点击某个选项时执行
    onClick(index) {
      this.currentIndex = index
      this.inlineStyle.transition = "left 0.2s ease-in-out"
      this.lineMove(index)
    },
    // 计算下划线滑动距离
    lineMove(index) {
      // 获取当前点击的DOM元素
      let dom = document.getElementsByClassName("nav-tab")[index]
      // 使用DOM对象提供的getBoundingClientRect方法获取该元素相对于当前网页视口的位置信息
      let domRect = dom.getBoundingClientRect()
      // 最终移动距离 = 当前选项距离视口左侧距离 - 整个导航栏距离视口左侧距离 + 当前选项宽度的一半
      this.inlineStyle.left = domRect.left - this.leftWidth + (domRect.width / 2) + "px"
    },
  },
  mounted() {
    // 初始获取导航栏距离窗口左侧距离
    this.leftWidth = document.querySelector(".nav-list").getBoundingClientRect().left
    this.lineMove(0)
    // 监听导航组件尺寸变化,尺寸变化时重新计算下划线距离
    this.io = new ResizeObserver((entries) => {
      // 重新获取导航栏距离窗口左侧的坐标
      this.leftWidth = document.querySelector(".nav-list").getBoundingClientRect().left
      // 重新计算当前下划线的距离
      this.lineMove(this.currentIndex)
    })
    this.io.observe(document.querySelector(".nav-list"))
  },
  beforeDestroy () {
    // 销毁组件前断开观察
    this.io.disconnect()
  }
}

最终效果

好的,就到这里,一个简单的导航栏组件就算完成了,其实我们还可以给ResizeObserver的回调添加防抖,来避免回调在尺寸变化期间的多次触发,但是这里我懒(😅😅😅),就没加,感兴趣的小伙伴可以自己去加一个,不会加的话,可以百度,或者看elementUI的组件源码里有写如何给回调添加防抖,实在不行问我也行....


最终代码

<template>
  <div class="nav" style="margin: 25px">
    <!-- 标题列表 -->
    <div class="nav-list">
      <div class="nav-tab" v-for="(item, index) in list" :key="index" @click="onClick(index)" :class="{ active : index === currentIndex }">{{ item }}</div>
    </div>
    <!-- 下划线 -->
    <i class="nav-line" :style="inlineStyle"></i>
  </div>
</template>

<script>
import ResizeObserver from 'resize-observer-polyfill'

export default {
  data() {
    return {
      currentIndex: 0,
      io: null,
      list: ["首页", "热门", "假面骑士", "叶问有枪", "基努里维斯"],
      leftWidth: null, // 导航栏距离窗口左侧的坐标
      inlineStyle: {
        transform: "translateX(-50%)",
        transition: "unset",
        left: 0,
      },
    }
  },
  methods: {
    // 点击某个选项时执行
    onClick(index) {
      this.currentIndex = index
      this.inlineStyle.transition = "left 0.2s ease-in-out"
      this.lineMove(index)
    },
    // 计算下划线滑动距离
    lineMove(index) {
      // 获取当前点击的DOM元素
      let dom = document.getElementsByClassName("nav-tab")[index]
      // 使用DOM对象提供的getBoundingClientRect方法获取该元素相对于当前网页视口的位置信息
      let domRect = dom.getBoundingClientRect()
      // 最终移动距离 = 当前选项距离视口左侧距离 - 整个导航栏距离视口左侧距离 + 当前选项宽度的一半
      this.inlineStyle.left = domRect.left - this.leftWidth + (domRect.width / 2) + "px"
    },
  },
  mounted() {
    // 初始获取导航栏距离窗口左侧距离
    this.leftWidth = document.querySelector(".nav-list").getBoundingClientRect().left
    this.lineMove(0)
    // 监听导航组件尺寸变化,尺寸变化时重新计算下划线距离
    this.io = new ResizeObserver((entries) => {
      // 重新获取导航栏距离窗口左侧的坐标
      this.leftWidth = document.querySelector(".nav-list").getBoundingClientRect().left
      // 重新计算当前下划线的距离
      this.lineMove(this.currentIndex)
    })
    this.io.observe(document.querySelector(".nav-list"))
  },
  beforeDestroy () {
    // 销毁组件前断开观察
    this.io.disconnect()
  }
}
</script>

<style scoped>
.nav {
  position: relative;
}

.nav-list {
  width: 100%;
  display: flex;
  align-items: center;
  gap: 20px;
}

.nav-tab {
  height: 50px;
  line-height: 50px;
  cursor: pointer;
}

.active {
  color: red;
  font-weight: bold;
}

.nav-line {
  width: 18px;
  height: 4px;
  border-radius: 2px;
  background-color: red;
  position: absolute;
  bottom: 0;
}
</style>

如若转载,请注明出处,谢谢😄😄😄

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐