day-ui - Affix 组件学习

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

固钉组件是把页面某个元素相对页面 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)
  }
}
  • off
export const off = function(element, event, handler, useCapture = false) {
  if (element && event && handler) {
    element.removeEventListener(event, handler, useCapture)
  }
}
  • getScrollContainer
/**
 * 获取滚动容器
 * @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
}
  • isSserver
export default typeof window === 'undefined'
  • isScroll
/**
 *
 * @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)/)
}
  • getStyle
// 获取元素的属性值
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 库

这个库是我第一次见到,如果不看源码都不知道的。觉得还是挺有意思的,这里做个简单介绍。

这个库主要作用是监听元素 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 组件的学习。如有不对欢迎指正。

目录
相关文章
|
22天前
|
缓存 搜索推荐 索引
「Mac畅玩鸿蒙与硬件12」鸿蒙UI组件篇2 - Image组件的使用
在鸿蒙应用开发中,Image 组件用于加载和显示图片资源,并提供多种属性来控制图片的显示效果和适配方式。本篇将带你学习如何在鸿蒙应用中加载本地和远程图片、设置图片样式以及实现简单的图片轮播功能。
87 7
「Mac畅玩鸿蒙与硬件12」鸿蒙UI组件篇2 - Image组件的使用
|
14天前
|
搜索推荐 Android开发 开发者
探索安卓开发中的自定义视图:打造个性化UI组件
【10月更文挑战第39天】在安卓开发的世界中,自定义视图是实现独特界面设计的关键。本文将引导你理解自定义视图的概念、创建流程,以及如何通过它们增强应用的用户体验。我们将从基础出发,逐步深入,最终让你能够自信地设计和实现专属的UI组件。
|
8天前
|
搜索推荐 前端开发 开发者
「Mac畅玩鸿蒙与硬件19」鸿蒙UI组件篇9 - 自定义动画实现
自定义动画让开发者可以设计更加个性化和复杂的动画效果,适合表现独特的界面元素。鸿蒙提供了丰富的工具,支持通过自定义路径和时间控制来创建复杂的动画运动。本篇将带你学习如何通过自定义动画实现更多样化的效果。
61 11
「Mac畅玩鸿蒙与硬件19」鸿蒙UI组件篇9 - 自定义动画实现
|
8天前
|
UED
「Mac畅玩鸿蒙与硬件17」鸿蒙UI组件篇7 - Animation 组件基础
在应用开发中,动画效果可以增强用户体验。鸿蒙框架提供了 translate、scale 和 rotate 等动画功能,允许对组件进行平移、缩放和旋转等操作。本篇将介绍 Animation 组件的基础知识和示例代码。
57 10
「Mac畅玩鸿蒙与硬件17」鸿蒙UI组件篇7 - Animation 组件基础
|
6天前
|
前端开发 开发者
「Mac畅玩鸿蒙与硬件22」鸿蒙UI组件篇12 - Canvas 组件的动态进阶应用
在鸿蒙应用中,Canvas 组件可以实现丰富的动态效果,适合用于动画和实时更新的场景。本篇将介绍如何在 Canvas 中实现动画循环、动态进度条、旋转和缩放动画,以及性能优化策略。
34 6
「Mac畅玩鸿蒙与硬件22」鸿蒙UI组件篇12 - Canvas 组件的动态进阶应用
|
6天前
|
前端开发 开发者
「Mac畅玩鸿蒙与硬件23」鸿蒙UI组件篇13 - 自定义组件的创建与使用
自定义组件可以帮助开发者实现复用性强、逻辑清晰的界面模块。通过自定义组件,鸿蒙应用能够提高代码的可维护性,并简化复杂布局的构建。本篇将介绍如何创建自定义组件,如何向组件传递数据,以及如何在不同页面间复用这些组件。
28 5
「Mac畅玩鸿蒙与硬件23」鸿蒙UI组件篇13 - 自定义组件的创建与使用
|
9天前
|
存储 数据安全/隐私保护 开发者
「Mac畅玩鸿蒙与硬件13」鸿蒙UI组件篇3 - TextInput 组件获取用户输入
在鸿蒙应用开发中,TextInput 组件用于接收用户输入,适用于文本、密码等多种输入类型。本文详细介绍鸿蒙 TextInput 组件的使用方法,包括输入限制、样式设置、事件监听及搜索框应用,帮助你灵活处理鸿蒙应用中的用户输入。
45 7
「Mac畅玩鸿蒙与硬件13」鸿蒙UI组件篇3 - TextInput 组件获取用户输入
|
24天前
|
自然语言处理 开发者
「Mac畅玩鸿蒙与硬件11」鸿蒙 UI 组件篇1 - Text 和 Button 组件详解
本篇将详细介绍鸿蒙应用开发中的 Text 和 Button 组件。通过本篇内容,你将学习如何使用 Text 组件显示文本、格式化文本样式,以及如何使用 Button 组件处理点击事件并自定义样式。掌握这些基本组件的用法将为后续的 UI 开发奠定基础。
92 4
「Mac畅玩鸿蒙与硬件11」鸿蒙 UI 组件篇1 - Text 和 Button 组件详解
|
2月前
|
JavaScript 索引
Vue开发中Element UI/Plus使用指南:常见问题(如Missing required prop: “value“)及中文全局组件配置解决方案
Vue开发中Element UI/Plus使用指南:常见问题(如Missing required prop: “value“)及中文全局组件配置解决方案
125 0
|
3月前
|
JavaScript
从零开始写一套广告组件【一】搭建基础框架并配置UI组件库
其实这个从零有点歧义,因为本质上是要基于`tdesign-vue-next`来进行二次封装为一套广告UI组件库,现在让我们在一起快乐的搭建自己的广告UI库之前,先对以下内容做出共识:
86 0
从零开始写一套广告组件【一】搭建基础框架并配置UI组件库