专栏首页与前端沾边[day-ui] Affix 组件学习
原创

[day-ui] Affix 组件学习

固钉组件是把页面某个元素相对页面 HTML 或者某个 dom 内定位显示,例如固定页面顶部/底部显示,页面宽高改变也会保持原位置。如果进行滚动,超过定义的范围就会固定定位,否则会跟随页面滚动

上一节我们介绍了 DButtonDIcon 的实现,所以新建 affix 文件目录结构我们就不多介绍了。我们主要学习一下内部实现方式,本质就是位置定位,我们要看下用了哪些判断和第三方库,如果有哪里不对欢迎指正。

效果分析

  1. 第一种情况是没有设置容器,可以根据 position 位置设置固定定位,如果位置设置 top,那么当监听到页面滚动,如果当前元素的 top 值小于设置的偏移量,设置 fixed 定位(反之 bottom 是比较 bottom 值大于页面高度和偏移量的差值设置 fixed 定位)
  2. 第二种情况是设置容器,那么 top / bottom 的是只在容器内显示的,容器不在页面后,定位元素也就消失。如果设置的 top 值,那么当当前元素 top 值小于偏移量同时容器的 bottom 大于0,元素 fixed 定位(反之 bottom 偏移需要计算页面高度和 bottom 值得对比)。 最近学习了解到fixed 定位默认是相对与窗口的,但是如果给父节点定义属性 transform、filter、perspective,fixed 定位就会相对父集,大家感兴趣的话可以自行查看。

代码分析

dom 结构

<template>
  <div ref="root" class="d-affix" :style="rootStyle">
    <!-- 定位元素 滚动时监听 root 位置和页面可视区的关系设置 fixed,定位的时候设置样式-->
    <div :class="{ 'd-affix--fixed': state.fixed }" :style="affixStyle">
      <slot></slot>
    </div>
  </div>
</template>

外层定义 d-affix 类,高度和内部的元素相同,为了当内部元素 fixed 定位脱离文档流时,页面占位结构不变;同时需要对比 d-affixtopbottom 值判断元素何时脱离文档,何时复位。

属性

props: {
  // 定位元素的层级
  zIndex: {
    type: Number,
    default: 100
  },
  // 在哪个容器内,没传就是视图
  target: {
    type: String,
    default: ''
  },
  // 上下偏移量
  offset: {
    type: Number,
    default: 0
  },
  // 距上边距下边距
  position: {
    type: String,
    default: 'top'
  }
},
// 对外暴露两个方法,监听滚动和 fixed 状态改变
emits: ['scroll', 'change'],

setUp 核心

// 定位元素属性
const state = reactive({
  fixed: false,
  height: 0, // height of target 滚动时获取赋值
  width: 0, // width of target
  scrollTop: 0, // scrollTop of documentElement
  clientHeight: 0, // 窗口高度
  transform: 0 // 元素在 target 中定位时 y 方向移动
})

// 计算属性,滚动时才能具体获取

// d-affix 类一直存在文档流中,只要宽高,滚动位置判断是否 fixed
const rootStyle = computed(() => {
  return {
    height: state.fixed ? `${state.height}px` : '',
    width: state.fixed ? `${state.width}px` : ''
  }
})
// 定位元素属性
const affixStyle = computed(() => {
  if (!state.fixed) return
  const offset = props.offset ? `${props.offset}px` : 0
  const transform = state.transform
    ? `translateY(${state.transform}px)`
    : ''

  return {
    height: `${state.height}px`,
    width: `${state.width}px`,
    top: props.position === 'top' ? offset : '',
    bottom: props.position === 'bottom' ? offset : '',
    transform: transform,
    zIndex: props.zIndex
  }
})

滚动时定位属性的判断:

const updateState = () => {
  // 获取 d-affix 节点信息
  const rootRect = root.value.getBoundingClientRect()
  // 获取 target 节点的信息
  const targetRect = target.value.getBoundingClientRect()
  state.height = rootRect.height
  state.width = rootRect.width
  // 没有 target 取 html 的 scrollTOP(有 target 在 target 中滚动)
  state.scrollTop =
    scrollContainer.value === window
      ? document.documentElement.scrollTop
      : scrollContainer.value.scrollTop

  state.clientHeight = document.documentElement.clientHeight
  // 设置上边距
  if (props.position === 'top') {
    if (props.target) {
      // 定位元素在 target 元素中滑动距离,bottom 持续改变
      const difference = targetRect.bottom - props.offset - state.height
      // target 元素top在可视区外面,bottom在可视区进行定位
      state.fixed = props.offset > rootRect.top && targetRect.bottom > 0
      state.transform = difference < 0 ? difference : 0
    } else {
      // 以html为相对容器,页面滚动,固定定位(d-affix 在可视区外)
      state.fixed = props.offset > rootRect.top
    }
  } else {
  // 设置下边距
    if (props.target) {
      const difference =
        state.clientHeight - targetRect.top - props.offset - state.height
      state.fixed =
        state.clientHeight - props.offset < rootRect.bottom &&
        state.clientHeight > targetRect.top
      state.transform = difference < 0 ? -difference : 0
    } else {
      // offset + bottom > 视图高度,元素进行定位
      state.fixed = state.clientHeight - props.offset < rootRect.bottom
    }
  }
}
const onScroll = () => {
  updateState()
  emit('scroll', {
    scrollTop: state.scrollTop,
    fixed: state.fixed
  })
}

watch(
  () => state.fixed,
  () => {
    emit('change', state.fixed)
  }
)
// 页面挂载的时候
onMounted(() => {
  if (props.target) {
    // 注意传的格式
    target.value = document.querySelector(props.target)
    if (!target.value) {
      throw new Error(`target is not existed: ${props.target}`)
    }
  } else {
    target.value = document.documentElement // html
  }
  // 下面我们分析辅助函数
  scrollContainer.value = getScrollContainer(root.value)
  // 函数式编程,on 改写的 addEventListener
  on(scrollContainer.value, 'scroll', onScroll)
  addResizeListener(root.value, updateState)
})
// 页面即将关闭取消监听移除
onBeforeMount(() => {
  off(scrollContainer.value, 'scroll', onScroll)
  removeResizeListener(root.value, updateState)
})

辅助函数

  • on// 函数式编程处理元素监听 export const on = function(element, event, handler, useCapture = false) { if (element && event && handler) { element.addEventListener(event, handler, useCapture) } }export const off = function(element, event, handler, useCapture = false) { if (element && event && handler) { element.removeEventListener(event, handler, useCapture) } }/** * 获取滚动容器 * @param {*} el 滚动的容器 * @param {*} isVertical 竖直滚动还是水平滚动 * @returns */ export const getScrollContainer = (el, isVertical) => { if (isServer) return let parent = el while (parent) { // 都没有就是 window if ([window, document, document.documentElement].includes(parent)) { return window } // 容器是否可滚动 if (isScroll(parent, isVertical)) { return parent } parent = parent.parentNode } return parent }export default typeof window === 'undefined'/** * * @param {*} el * @param {*} isVertical 是否垂直方向 overflow-y * @returns */ export const isScroll = (el, isVertical) => { if (isServer) return const determineDirection = isVertical === null || isVertical === undefined const overflow = determineDirection ? getStyle(el, 'overflow') : isVertical ? getStyle(el, 'overflow-y') : getStyle(el, 'overflow-x') return overflow.match(/(scroll|auto)/) }// 获取元素的属性值 export const getStyle = function(element, styleName) { if (isServer) return if (!element || !styleName) return null styleName = camelize(styleName) if (styleName === 'float') { /** * ie6~8下:style.styleFloat FF/chrome 以及ie9以上:style.cssFloat */ styleName = 'cssFloat' // FF/chrome 以及ie9以上 float兼容性写法 } try { const style = element.style[styleName] if (style) return style // 获取window对象, firefox低版本3.6 才能使用getComputed方法,iframe pupup extension window === document.defaultView,否则指向错误 // https://www.cnblogs.com/yuan-shuai/p/4125511.html const computed = document.defaultView.getComputedStyle(element, '') return computed ? computed[styleName] : '' } catch (e) { return element.style[styleName] } }resize-observer-polyfill 库这个库是我第一次见到,如果不看源码都不知道的。觉得还是挺有意思的,这里做个简单介绍。
  • off
  • getScrollContainer
  • isSserver
  • isScroll
  • getStyle

这个库主要作用是监听元素 size 改变。通常情况下我们监听大小改变只能使用 window.size 或者 window.orientationchange(移动端屏幕横向纵向显示)。resize 事件会在 1s内触发 60 次左右,所以很容易在改变窗口大小时候引发性能问题,所以当我们监听某个元素变化的时候就显得有些浪费。

ResizeObserver API 是新增的,在有些浏览器还存在兼容性,这个库可以很好的进行兼容。ResizeObserver 使用了观察者模式,当元素 size 发生改变时候触发(节点的出现隐藏也会触发)。

用法

const observer = new ResizeObserver(entries => {
  entries.forEach(entry => {
    console.log('大小位置', entry.contentRect)
    console.log('监听的dom', entry.target)
  })
})
// 监听的对象是body,可以改变浏览器窗口大小看打印效果
observer.observe(document.body)// dom节点,不是类名 id名
  • width:指元素本身的宽度,不包含 padding,border
  • height:指元素本身的高度,不包含 padding,border
  • top:指 padidng-top 的值
  • left:指 padding-left 的值
  • right:指 left + width 的值
  • bottom: 值 top + height 的值

方法

  • ResizeObserver.disconnect() 取消所有元素的监听
  • ResizeObserver.observe() 监听元素
  • ResizeObserver.unobserve() 结束某个元素的监听

组件使用

我们在 onMounted 中对 root 元素监听。页面滚动时候要监听,元素大小改变也要监听

import ResizeObserver from 'resize-observer-polyfill'
import isServer from './isServer'

const resizeHandler = function(entries) {
  for (const entry of entries) {
    /**
     * const {left, top, width, height} = entry.contentRect;
     * 'Element:', entry.target
        Element's size: ${ width }px x ${ height }px`
        Element's paddings: ${ top }px ; ${ left }px`
     */
    const listeners = entry.target.__resizeListeners__ || []
    if (listeners.length) {
      // 元素改变直接执行方法
      listeners.forEach(fn => fn())
    }
  }
}
// 监听element元素size改变,执行fn
export const addResizeListener = function(element, fn) {
  if (isServer || !element) return
  if (!element.__resizeListeners__) {
    element.__resizeListeners__ = []
    /**
     * https://github.com/que-etc/resize-observer-polyfill
     *
     */
    element.__ro__ = new ResizeObserver(resizeHandler)
    // 观察的对象
    element.__ro__.observe(element)
  }
  element.__resizeListeners__.push(fn)
}
// 退出移除监听
export const removeResizeListener = function(element, fn) {
  if (!element || !element.__resizeListeners__) return
  element.__resizeListeners__.splice(element.__resizeListeners__.indexOf(fn), 1)
  if (!element.__resizeListeners__.length) {
    // 取消监听
    element.__ro__.disconnect()
  }
}

以上就是对 affix 组件的学习。如有不对欢迎指正。下一篇我们进行 alert 组件的学习。如果文章对您有帮助,欢迎关注公众号 与前端沾边,或者加小编微信:wajh123654789,一起学习。

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • day 83 Vue学习三之vue组件

      我们在进行vue开发的时候,还记得我们自己创建的vm对象吗,这个vm对象我们称为一个大组件,根组件(页面上叫Root),在一个网页的开发中,根据网页上的功能...

    py3study
  • BootStrap应用开发学习入门1

    什么是字体图标? 答:字体图标是在 Web 项目中使用的图标字体,可以通过基于项目的 Bootstrap 来免费使用这些图标。

    WeiyiGeek
  • [day-ui] 组件库打包环境配置

    上一节我们书写了 button 和 icon 组件,单元测试和文档也都完成了,接下来我们把写好的库打包发布到 npm 上。之后我们建个小 vue3 的项目,安装...

    测不准
  • Android UI新组件学习和使用

    今天来学习总结一下,Android 后添加的一些新的组件和UI效果,Material Dialog,SwipeRefreshLayout,ListPopupWi...

    砸漏
  • [day-ui]基于 vue3.0 从 0-1 搭建组件库 - 环境搭建

    其实之前使用 vue2 的时候就想写个开源组件库,学习交流使用。如果公司有自己的需求也可以快速上手。开始想的是能在网上找到好的教程,环境搭建 - 组件编写 - ...

    测不准
  • 深入理解bootstrap

    1.CSS12栅格系统:以规则的网格阵列来指导和规范网页中的版面布已有以及信息分布

    硬核项目经理
  • Web-第五天 BootStrap学习

    将使用Bootstrap重写首页,整个案例中将使用到Bootstrap各种模块,为了方便编程,将采用拆分的原则,各个模块单独编写,最后组合。

    Java帮帮
  • Quartz Cron表达式的二三事

    最近在解决产品上的一个需求,就是定期生成报告(Report),我们叫做Scheduled Report。 原理:UI获取用户输入的时间信息,后台使用Spring...

    宋凯伦
  • [day-ui]DButton 组件和 DIcon 组件实现

    上一篇中我们已经把组件的基础架构和文档的雏形搭建好了。下面我们从最简单的 button 和 icon 组件入手,熟悉下 vue3 的语法结构和组件的单元测试。看...

    测不准
  • 基于深度学习的移动用户点击行为大规模建模(CS)

    对移动设备上的用户点击或点击序列进行建模可以提高我们对交互行为的理解,并通过推荐用户可能想要点击的下一个元素为UI优化提供机会。我们分析了4000多名手机用户的...

    用户8440711
  • GitHub 热点速览 Vol.30:那些提升效率的小工具们

    以下内容摘录自微博@HelloGitHub 的 GitHub Trending 及 Hacker News 热帖(简称 HN 热帖),选项标准:新发布 | 实用...

    HelloGitHub
  • GitHub 热点速览 Vol.28:有品位程序员的自我修养

    以下内容摘录自微博 HelloGitHub 的 GitHub Trending 及 Hacker News 热帖(简称 HN 热帖),选项标准:新发布 | 实用...

    HelloGitHub
  • awesome-css-cn命名习惯和方式参考其他资源原文链接

    CSS 资源列表,内容包括:CSS预处理器、框架、CSS结构、代码风格指南、命名习惯、播客、演讲视频、大网站的 CSS 开发经验等等。 预处理器 更快地编译 C...

    guanguans
  • [每周日-先行者课堂笔记] -- react版的倒计时实现

    image.png 各位同学们大家好,今天是4月9号周日,今天我们继续来做“倒计时”这个前端组件。之前我们是使用原生js来实现的,其实更多的只是实现了功能。 这...

    web前端教室
  • Sketch颠覆者!静电的Figma完全学习日记-Day.02

    静电说:在发表Sketch颠覆者!静电的Figma完全学习日记-Day.01后,我收到了不少小伙伴的留言。其中有一些表达了质疑的声音,大概是这样的:

    用户5009027
  • qt动态切换语言教程

    在C++ GUI Qt4一书中,动态语言切换也就是Qt的国际化是属于Qt的高级部分,今天就来让高级的部分简单化。

    用户5908113
  • Flutter组件学习(一)—— Text组件

    之前说会给大家一一讲解 Flutter 中的组件,今天咱们就从 Text 组件开始,无图言X,先上图:

    用户2802329
  • 用 Recursive Neural Networks 得到分析树

    ---- CS224d-Day 10: Recursive neural networks -- for parsing 课程链接 视频链接 课件链接 ...

    杨熹
  • [day-ui] Alert 组件学习

    从样式功能来看,整体不是很复杂,alert 组件主要包括了主题色,title,关闭按钮,关闭事件,居中,加粗等

    测不准

扫码关注云+社区

领取腾讯云代金券