瀑布流+长列表+懒加载处理方案

目录

01: 业务组件:构建基础列表展示

02: 通用组件:瀑布流组件构建分析

03: 通用组件-瀑布流:构建瀑布流布局,获取容器宽度与列宽 

04: 通用组件-瀑布流:区分图片预加载,获取元素关键属性

05: 通用组件-瀑布流:触发计算,定位 item 位置

06: 通用组件-瀑布流:适配移动端,动态列

07: 通用组件-瀑布流:无需图片预加载时,优化功能处理   

08: 通用组件-瀑布流:总结

09: 通用组件:长列表 infinite 构建分析 

长列表的实现原理

我们使用长列表时,希望如何进行使用

10: 通用组件:构建长列表 infinite 组件 

11: 通用组件:应用 infinite 结合 waterfall 实现长列表瀑布流效果

12: 通用组件:解决首次数据无法铺满全屏时,数据无法继续加载

13: 图片懒加载构建原因+实现原理

图片懒加载实现原理

14: 通用指令:实现图片懒加载 

15. 深入 vite:指令的自动注册 

16: 指定彩色占位图

17: 总结 


 

01: 业务组件:构建基础列表展示

// tailwind 类名
xl:mt-4

// 编译为
@media (min-width: 1280px) {
    .xl\:mt-4 {
        margin-top: 1rem
    }
}
- src/views/main
- - components
- - - list
- - - - index.vue
- - - - item.vue


# main/index.vue 中
<list-vue></list-vue>

02: 通用组件:瀑布流组件构建分析

        现在的样式中存在的问题:每个 item 应该横向排列,第二行的 item 顺序连接到当前最短的列中,而这个也就是构建瀑布流的核心逻辑。

        想要实现这个核心逻辑,我们的每个 item 肯定就不可以使用正常的布局方式,而必须使用 absolute 绝对布局,通过 top 和 left 来手动控制位置。

        明确好了这个核心逻辑之后,接下来如何进行构建呢?

        想要搞明白一个复杂组件的构建机制,那么最好的方式是想象一下:当你去使用这个组件时,你希望如何进行使用。

        按照你期望的使用方式来反向思考构建过程,这样在主要的思想上就不会出现大的偏差。

        那么我们期望,将来这个通用组件可以这样使用:

<m-waterfall
    :data="" // 数据源
    :nodeKey="" // 唯一标识的 key
    :column="" // 渲染的列数
    :picturePreReading="" // 是否需要图片预渲染(在不知道图片高度的情况下)
>
    <template v-slot="{ item, width }">
        // 对应的 item
    </template>
</m-waterfall>

综合来看,整个瀑布流组件的构建,我们需要分成几部分:

        1. 通过 props 传递关键数据(部分):

                1. data:数据源

                2. nodeKey:唯一标识

                3. column:渲染的列数

                4. picturePreReading:是否需要图片预渲染

        2. 瀑布流渲染机制:通过 absolute 配合 relative 完成布局,布局逻辑:每个 item 应该横向排列,第二行的 item 顺序连接到当前最短的列中。

        3. 通过 作用域插槽 将每个 item 中涉及到的关键数据,传递到 item 视图中。

明确好了以上几点之后,接下来我们就可以进行对应的实现了。 

03: 通用组件-瀑布流:构建瀑布流布局,获取容器宽度与列宽 

- src/libs
- - waterfall
- - - index.vue
<template>
  <div
    class="relative"
    ref="containerTarget"
    :style="{
      // 因为当前为 relative 布局,所以需要主动指定高度
      height: containerHeight + 'px' 
    }"
  >
    <!-- 因为列数不确定,所以需要根据列数计算每列的宽度,-->
    <!-- 所以等待列宽计算完成,并且有了数据源之后进行渲染 -->
    <template v-if="columnWidth && data.length">
      <!-- 通过动态的 style 来去计算对应的列宽、left、top -->
      <div
        class="m-waterfall-item absolute duration-300"
        :style="{
          width: columnWidth + 'px',
          left: item._style?.left + 'px',
          top: item._style?.top + 'px'
        }"
        v-for="(item, index) in data"
        :key="nodeKey ? item[nodeKey] : index"
      >
        <slot :item="item" :width="columnWidth" :index="index" />
      </div>
    </template>
    <!-- 可以给一个加载中的描述,没有也无所谓 -->
    <!-- <div v-else>加载中...</div> -->
  </div>
</template>
<script setup>
import { ref, onMounted, computed, watch, nextTick, onUnmounted } from 'vue'

const props = defineProps({
  // 数据源
  data: {
    type: Array,
    required: true
  },
  // 唯一标识的 key
  nodeKey: {
    type: String
  },
  // 列数
  column: {
    default: 2,
    type: Number
  },
  // 列间距
  columnSpacing: {
    default: 20,
    type: Number
  },
  // 行间距
  rowSpacing: {
    default: 20,
    type: Number
  },
  // 是否需要进行图片预读取
  picturePreReading: {
    type: Boolean,
    default: true
  }
})

// 容器的总高度
const containerHeight = ref(0)
// 记录每列高度的容器。key:所在列  val:列高
const columnHeightObj = ref({})
/**
 * 构建记录各列的高度的对象。
 */
const useColumnHeightObj = () => {
  columnHeightObj.value = {}
  for (let i = 0; i < props.column; i++) {
    columnHeightObj.value[i] = 0
  }
}

// 容器实例
const containerTarget = ref(null)
// 容器总宽度(不包含 padding、margin、border)
const containerWidth = ref(0)
// 容器左边距,计算 item left 时,需要使用定位
const containerLeft = ref(0)
/**
 * 计算容器宽度
 */
const useContainerWidth = () => {
  const { paddingLeft, paddingRight } = getComputedStyle(
    containerTarget.value,
    null
  )
  // 容器左边距
  containerLeft.value = parseFloat(paddingLeft)
  // 容器宽度
  containerWidth.value =
    containerTarget.value.offsetWidth -
    parseFloat(paddingLeft) -
    parseFloat(paddingRight)
}
// 列宽 = (容器的宽度 - 所有的列间距宽度) / 列数
const columnWidth = ref(0)
// 列间距合计
const columnSpacingTotal = computed(() => {
  // 如果是5列,则存在 4 个列间距
  return (props.column - 1) * props.columnSpacing
})
/**
 * 开始计算
 */
const useColumnWidth = () => {
  // 获取容器宽度
  useContainerWidth()
  // 计算列宽
  columnWidth.value =
    (containerWidth.value - columnSpacingTotal.value) / props.column
}
onMounted(() => {
  // 计算列宽
  useColumnWidth()
})

04: 通用组件-瀑布流:区分图片预加载,获取元素关键属性

        想要计算每列的 left、right,就需要拿到每个 item 的高度,因为只有有了每个 item 的高度,才可以判断下一列的第一个 item 的位置。

同时我们根据 picturePreReading 又可以分为两种情况:

        1. 需要图片预加载时:图片高度未知

        2. 不需要图片预加载时:图片高度已知

根据以上分析可得出以下代码:

// item 高度集合
let itemHeights = []
/**
 * 监听图片加载完成
 */
const waitImgComplete = () => {
  itemHeights = []
  // 拿到所有元素
  let itemElements = [...document.getElementsByClassName('m-waterfall-item')]
  // 获取所有元素的 img 标签
  const imgElements = getImgElements(itemElements)
  // 获取所有 img 标签的图片
  const allImgs = getAllImg(imgElements)
  onCompleteImgs(allImgs).then(() => {
    // 图片加载完成,获取高度
    itemElements.forEach((el) => {
      itemHeights.push(el.offsetHeight)
    })
    // 渲染位置
    useItemLocation()
  })
}

/**
 * 图片不需要预加载时,计算 item 高度
 */
const useItemHeight = () => {
  itemHeights = []
  // 拿到所有元素
  let itemElements = [...document.getElementsByClassName('m-waterfall-item')]
  // 计算 item 高度
  itemElements.forEach((el) => {
    // 依据传入数据计算出的 img 高度
    itemHeights.push(el.offsetHeight)
  })
  // 渲染位置
  useItemLocation()
}

/**
 * 渲染位置
 */
const useItemLocation = () => {
    console.log(itemHeights)
}

/**
 * 触发计算
 */
watch(
    () => props.data,
    (newVal) => {
        nextTick(() => {
            if (props.picturePreReading) {
                waitImgComplete()
            } else {
                useItemHeight()
            }
        })
    },
    {
        immediate: true,
        deep: true
    }
)
/**
 * 从 itemElement 中抽离出所有的 imgElements
 */
export const getImgElements = (itemElements) => {
  const imgElements = []
  itemElements.forEach((el) => {
    imgElements.push(...el.getElementsByTagName('img'))
  })
  return imgElements
}

/**
 * 生成所有的图片链接数组
 */
export const getAllImg = (imgElements) => {
  return imgElements.map((imgElement) => {
    return imgElement.src
  })
}

/**
 * 监听图片数组加载完成(通过 promise 完成)
 */
export const onComplateImgs = (imgs) => {
  // promise 集合
  const promiseAll = []
  // 循环构建 promiseAll
  imgs.forEach((img, index) => {
    promiseAll[index] = new Promise((resolve, reject) => {
      const imageObj = new Image()
      imageObj.src = img
      imageObj.onload = () => {
        resolve({
          img,
          index
        })
      }
    })
  })
  return Promise.all(promiseAll)
}

05: 通用组件-瀑布流:触发计算,定位 item 位置

/**
 * 为每个 item 生成位置属性
 */
const useItemLocation = () => {
  // 遍历数据源
  props.data.forEach((item, index) => {
    // 避免重复计算
    if (item._style) {
      return
    }
    // 生成 _style 属性
    item._style = {}
    // left
    item._style.left = getItemLeft()
    // top
    item._style.top = getItemTop()
    // 指定列高度自增
    increasingHeight(index)
  })

  // 指定容器高度
  containerHeight.value = getMaxHeight(columnHeightObj.value)
}

/**
 * 返回下一个 item 的 left
 */
const getItemLeft = () => {
  // 最小高度所在的列 * (列宽 + 间距)
  const column = getMinHeightColumn(columnHeightObj.value)
  return (
    column * (columnWidth.value + props.columnSpacing) + containerLeft.value
  )
}

/**
 * 返回下一个 item 的 top
 */
const getItemTop = () => {
  // 列高对象中的最小的高度
  return getMinHeight(columnHeightObj.value)
}

/**
 * 指定列高度自增
 */
const increasingHeight = (index) => {
  // 最小高度所在的列
  const minHeightColumn = getMinHeightColumn(columnHeightObj.value)
  // 该列高度自增
  columnHeightObj.value[minHeightColumn] +=
    itemHeights[index] + props.rowSpacing
}

/**
 * 在组件销毁时,清除所有的 _style
 */
onUnmounted(() => {
  props.data.forEach((item) => {
    delete item._style
  })
})

// 触发计算
watch(
  () => props.data,
  (newVal) => {
    // 重置数据源
    const resetColumnHeight = newVal.every((item) => !item._style)
    if (resetColumnHeight) {
      // 构建高度记录容器
      useColumnHeightObj()
    }
    nextTick(() => {
      if (props.picturePreReading) {
        waitImgComplate()
      } else {
        useItemHeight()
      }
    })
  },
  {
    immediate: true,
    deep: true
  }
)
/**
 * 返回列高对象中的最小高度所在的列
 */
export const getMinHeightColumn = (columnHeightObj) => {
  const minHeight = getMinHeight(columnHeightObj)
  return Object.keys(columnHeightObj).find((key) => {
    return columnHeightObj[key] === minHeight
  })
}

/**
 * 返回列高对象中的最小的高度
 */
export const getMinHeight = (columnHeightObj) => {
  const columnHeightArr = Object.values(columnHeightObj)
  return Math.min(...columnHeightArr)
}

/**
 * 返回列高对象中的最大的高度
 */
export const getMaxHeight = (columnHeightObj) => {
  const columnHeightArr = Object.values(columnHeightObj)
  return Math.max(...columnHeightArr)
}

06: 通用组件-瀑布流:适配移动端,动态列

        目前,我们的瀑布流组件可以适配 pc 端,但是移动端还存在一些问题。下一步,需要让我们的瀑布流组件可以适配移动端场景。

想要适配移动端,关键在于三点:

列数的变化

列数的变化想要处理比较简单,我们只需要利用 isMobileTerminal 方法进行判断即可。

在 src/views/main/components/list/index.vue 中:

:column="isMobileTerminal ? 2 : 5"
<m-waterfall
  :data="pexelsList"
  :column="isMobileTerminal ? 2 : 5"
  :picturePreReading="false"
  class="w-full px-1"
>
    <template v-slot="{ item, width }">
        <itemVue :data="item" :width="width" @click="onToPins" />
    </template>
</m-waterfall>

列宽和定位 

比较麻烦的地方在于 列宽和定位 如何进行重新计算,即:重新计算的时机是什么?

我们期望当 列数发生变化时,重新进行计算:

/**
 * 监听列数变化,重新构建瀑布流
 */
const reset = () => {
  // 延迟 100 毫秒,否则会导致宽度计算不正确
  setTimeout(() => {
    // 重新计算列宽
    useColumnWidth()
    // 重置所有的定位数据,因为 data 中进行了深度监听,所以该操作会触发 data 的 watch
    props.data.forEach((item) => {
      item._style = null
    })
  }, 100)
}
/**
 * 监听列数变化
 */
watch(
  () => props.column,
  () => {
    if (props.picturePreReading) {
      // 在 picturePreReading 为 true 的前提下,需要首先为列宽设置空,
      // 列宽设置空之后,会取消瀑布流渲染
      columnWidth.value = 0
      // 等待页面渲染之后,重新执行计算。否则在 item 没有指定过高度的前提下,
      // 计算出的 item 高度会不正确
      nextTick(reset)
    } else {
      reset()
    }
  }
)

07: 通用组件-瀑布流:无需图片预加载时,优化功能处理   

// src/views/main/components/list/index.vue
<template>
    <itemVue :width="width" ></itemVue>
</template>

<script setup>
    部分逻辑在上一节中
</script>
// src/views/main/components/list/item.vue

<script setup>
const props = defineProps({
    width: {
        type: Number,
        default: 0
    }
})
</script>

<template>
    <img :style="{
        height: (width / data.photoWidth) * data.photoHeight + 'px'
    }" />
</template>

08: 通用组件-瀑布流:总结

        瀑布流是一个比较复杂的通用组件,因为我们要尽量做到 普适,所以就需要考虑到各种场景下的处理方案,尽量可以满足日常开发的场景。这就在原本复杂的前提下,让这个功能变得更加复杂了。

下面我们就再来梳理一下整个瀑布流的构建过程:

1. 瀑布流的核心就是:通过 relative 和 absolute 定位的方式,来控制每个 item 的位置。

2. 影响瀑布流高度的主要元素,通常都是 img 标签。

3. 有些服务端会返回关键 img 的高度,有些不会。所以我们就需要分别处理:

        1. 当服务端 不返回 高度时:我们需要等待 img 加载完成之后,再来计算高度。然后通过得到的高度计算定位。否则会出现高度计算不准确,导致定位计算不准确的问题。

        2. 当服务端 返回 高度时:开发者必须利用此高度为 item 进行高度设定。一旦 item 具备指定高度,我们就不需要等待 img 加载的过程。这样效率更高,并且可以使业务的逻辑变得更加简单。

4. 当进行响应式切换时,同样需要区分对应的场景:

        1. 当服务器 不返回 高度时:我们需要 重新执行整个渲染流程,虽然会耗费一些性能,但是这样可以最大可能的避免出现逻辑错误。让组件拥有更强的普适性

        2. 当服务端 返回 高度时:我们同样需要重新计算 列宽定位。但是因为 item 具备明确的高度,所以我们可以直接拿到具体的高度,而无需重复整个渲染流程。从而可以实现更多的交互逻辑。比如:位移动画、将来的图片懒加载占位……

09: 通用组件:长列表 infinite 构建分析 

处理好瀑布流之后,接下来我们就需要处理对应的长列表功能。

我们知道对于首页中的瀑布流而言,是需要进行长列表展示的,也就是说它是一个分页的数据。

对于这种分页功能而言,又应该如何进行实现呢?

想要搞明白这个问题,同样需要分成两个方面去看:

        1. 长列表的实现原理是什么?

        2. 使用长列表时,希望如何进行使用?

长列表的实现原理

所谓长列表分页加载,其实指的就是:当滚动到列表底部时,加载数据。

想要实现长列表组件,围绕着的依然是这句话。

想要实现这个功能,我们需要做的核心一点就是能够 监听到列表滚动到底部。

想要监听到列表滚动到底部的话,可以利用 intersectionObserver,该接口可以判断:目标元素与其祖先元素或顶级文档视口(viewport)的交叉状态(是否可见)。

我们可以利用这个特性,把一个元素 置于列表底部,当这个元素可见时,则表示 列表滚动到了底部。

原生的 intersectionObserver 使用起来比较复杂,好在 vueuse 提供了 useIntersectionObserver 方法。

我们使用长列表时,希望如何进行使用

这个的判断和瀑布流的判断逻辑一样,通过这样的逻辑,可以让我们知道这个组件的 prop 应该如何构建。

我们期望使用它时是这样的:

<m-infinite-list
    v-model="" // 当前是否处于加载状态
    :isFinished="" // 数据是否全部加载完成
    @onLoad="" // 加载下一页数据的触发事件
>
 列表
</m-infinite-list>

10: 通用组件:构建长列表 infinite 组件 

- src/libs
- - infinite
- - - index.vue
<template>
  <div>
    <!-- 内容 -->
    <slot />
    <div ref="laodingTarget" class="h-6 py-4">
      <!-- 加载更多 -->
      <m-svg-icon
        v-show="loading"
        class="w-4 h-4 mx-auto animate-spin"
        name="infinite-load"
      ></m-svg-icon>
      <!-- 没有更多数据了 -->
      <p v-if="isFinished" class="text-center text-base text-zinc-400">
        已经没有更多数据了!
      </p>
    </div>
  </div>
</template>

<script setup>
import { ref, watch } from 'vue'
import { useIntersectionObserver, useVModel } from '@vueuse/core'

const props = defineProps({
  // 是否处于加载状态
  modelValue: {
    type: Boolean,
    required: true
  },
  // 数据是否全部加载完成
  isFinished: {
    type: Boolean,
    default: false
  }
})

const emits = defineEmits(['onLoad', 'update:modelValue'])

// 处理 loading 状态
const loading = useVModel(props)

// 滚动的元素
const laodingTarget = ref(null)
// 记录当前是否在底部(是否交叉)
const targetIsIntersecting = ref(false)
useIntersectionObserver(
  laodingTarget,
  ([{ isIntersecting }], observerElement) => {
    // 获取当前交叉状态
    targetIsIntersecting.value = isIntersecting
    // 触发 load
    emitLoad()
  }
)

/**
 * 触发 load
 */
const emitLoad = () => {
  // 当 加载更多的视图可见、loading为false、数据尚未全部加载完 时,加载更多数据。
  if (targetIsIntersecting.value && !loading.value && !props.isFinished) {
    // 修改加载数据标记
    loading.value = true
    // 触发加载更多行为
    emits('onLoad')
  }
}

/**
 * 监听 loading 的变化,解决数据加载完成后,首屏未铺满的问题
 */
watch(loading, (val) => {
  // 触发 load,延迟处理,等待 渲染和 useIntersectionObserver 的再次触发
  setTimeout(() => {
    emitLoad()
  }, 100)
})
</script>

<style lang="scss" scoped></style>

11: 通用组件:应用 infinite 结合 waterfall 实现长列表瀑布流效果

// src/views/main/components/list/index.vue 

<template>
    <!-- 列表处理 -->
    <m-infinite-list
      v-model="isLoading"
      :isFinished="isFinished"
      @onLoad="getPexelsData"
    >
      <m-waterfall
        :data="pexelsList"
        :column="isMobileTerminal ? 2 : 5"
        :picturePreReading="false"
        class="w-full px-1"
      >
        <template v-slot="{ item, width }">
          <itemVue :data="item" :width="width" @click="onToPins" />
        </template>
      </m-waterfall>
    </m-infinite-list>
</template>

<script setup>
const store = useStore()

/**
 * 构建数据请求
 */
let query = {
  page: 1,
  size: 20,
  categoryId: '',
  searchText: ''
}
// 数据是否在加载中
const isLoading = ref(false)
// 数据是否全部加载完成
const isFinished = ref(false)
// 数据源
const pexelsList = ref([])
/**
 * 加载数据的方法
 */
const getPexelsData = async () => {
  // 数据全部加载完成则 return
  if (isFinished.value) {
    return
  }

  // 完成第一次请求之后,后续请求让 page 自增
  if (pexelsList.value.length) {
    query.page += 1
  }

  // 触发接口请求
  const res = await getPexelsList(query)
  // 初始请求清空数据源
  if (query.page === 1) {
    pexelsList.value = res.list
  } else {
    pexelsList.value.push(...res.list)
  }
  // 判断数据是否全部加载完成
  if (pexelsList.value.length === res.total) {
    isFinished.value = true
  }
  // 修改 loading 标记
  isLoading.value = false
}

</script>

12: 通用组件:解决首次数据无法铺满全屏时,数据无法继续加载

问题:

        isIntersecting 为 true、!loading.vlaue 为 false、!props.isFinished 为 true。这样的条件 只会触发一次 emits('onLoad')。

解决:

        明确好了原因之后,再解决起来就比较简单了。只需要让 加载数据 的方法,在 loading.value 发生变化之后,重新进行一次判断,就可以了。使用 watch。

问题:

        src/views/components/list/index.vue 中,获取完数据,我们把 loading.value 设为 false。在 src/libs/infinite/index.vue 中,监听到 loading 的改变,再次触发加载更多行为。数据渲染需要时间,此时数据未渲染完成,targetIsIntersecting 依旧为 true,因此会出现 连续两次 触发加载更多行为 的问题。

        性能 和 简单逻辑 的衡量。 普适的东西就是权衡的艺术。

解决: 

        使用 setTimeout。

// src/libs/infinite/index.vue

// 滚动的元素
const laodingTarget = ref(null)

// 记录当前是否在底部(是否交叉)
const targetIsIntersecting = ref(false)

useIntersectionObserver(
  laodingTarget,
  ([{ isIntersecting }], observerElement) => {
    // 获取当前交叉状态
    targetIsIntersecting.value = isIntersecting
    // 触发 load
    emitLoad()
  }
)

/**
 * 触发 load
 */
const emitLoad = () => {
  // 当 加载更多的视图可见、loading为false、数据尚未全部加载完 时,加载更多数据。
  if (targetIsIntersecting.value && !loading.value && !props.isFinished) {
    // 修改加载数据标记
    loading.value = true
    // 触发加载更多行为
    emits('onLoad')
  }
}

/**
 * 监听 loading 的变化,解决数据加载完成后,首屏未铺满的问题
 */
watch(loading, (val) => {
  // 触发 load。延迟处理。等待 渲染 和 useIntersectionObserver 的再次触发。
  setTimeout(() => {
    emitLoad()
  }, 100)
})

13: 图片懒加载构建原因+实现原理

        屏幕未显示的已加载的图片,代表着多余的请求。多余的这些请求就会很浪费。如果我们不想有这种浪费,就可以利用 图片懒加载 功能进行实现。

图片懒加载实现原理

图片懒加载如何进行实现呢?想要搞明白这个,我们就需要先明白图片懒加载的原理是什么。

所谓图片懒加载指的是:当图片不可见时,不见在图片。当图片可见时,才去加载图片。 

大家看见这个“不可见 && 可见”是不是觉得很眼熟。这不就是实现 长列表 时用过的套路吗? 

据此,咱们的实现方案就呼之欲出了。

我们可以 监听所有图片是否被可见,如果图片处于不可见状态,那么就不加载图片。如果图片处于可见状态,那么就开始加载图片。 

而这个功能的实现关键就是 IntersectionObserver 。

14: 通用指令:实现图片懒加载 

- src
- - directives
- - - modules
- - - - lazy.js
- - - index.js
// src/directives/modules/lazy.js

import { useIntersectionObserver } from '@vueuse/core'

export default {
  // 图片懒加载:在用户无法看到图片时,不加载图片,在用户可以看到图片后加载图片。
  // 如何判断用户是否看到了图片:useIntersectionObserver。
  // 如何做到不加载图片(网络):img 标签渲染图片,指的是 img 的 src 属性,
  // src 属性是网络地址时,则会从网络中获取该图片资源。
  // 那么如果 img 标签不是网络地址呢?把该网络地址默认替换为非网络地址,
  // 然后当用户可见时,再替换成网络地址。
  mounted(el) {
    // 1. 拿到当前 img 标签的 src
    const imgSrc = el.src
    // 2. 把 img 标签的 src 替换为本地地址
    el.src = ''

    const { stop } = useIntersectionObserver(el, ([{ isIntersecting }]) => {
      if (isIntersecting) {
        // 3. 当图片可见时,加载图片
        el.src = imgSrc
        // 4. 停止监听
        stop()
      }
    })
  }
}
// src/directives/index.js

import lazy from './modules/lazy'

/**
 * 全局注册指令
 */
export default {
    install(app) {
        app.directive('lazy', lazy)
    }
}
// src/main.js

import mDirectives from './directives'

app.use(mDirectives)

15. 深入 vite:指令的自动注册 

        此时,在 src/directives/index.js 中,我们面临一个和注册组件时同样的问题。那就是:如果指令过多,一个一个地注册未免过于麻烦。最好有一种方式完成 指令的自动注册。 

        想要完成这个功能,我们依然要使用 Glob导入(import.meta.globEager)Object.entries​​​​​​​ 功能。

/**
 * 全局指令注册
 */
export default {
  async install(app) {
    // https://cn.vitejs.dev/guide/features.html#glob-import
    // import.meta.glob 函数中 { eager: true } 作为第二个参数 为同步导入
    const directives = import.meta.glob('./modules/*.js', { eager: true })
    for (const [key, value] of Object.entries(directives)) {
      // 拼接组件注册的 name
      const arr = key.split('/')
      const directiveName = arr[arr.length - 1].replace('.js', '')
      // 完成注册
      app.directive(directiveName, value.default)
    }
  }
}

16: 指定彩色占位图

- src/utils
- - color.js
// src/utils/color.js

/**
 * 生成随机色值
 */
export const randomRGB = () => {
  const r = Math.floor(Math.random() * 255)
  const g = Math.floor(Math.random() * 255)
  const b = Math.floor(Math.random() * 255)
  return `rgb(${r}, ${g}, ${b})`
}
// 使用

<div :style="{ backgroundColor: randomRGB() }" ></div>

17: 总结 

本篇文章借用首页 list 模块业务,实现了:

        1. 瀑布流

        2. 长列表

        3. 懒加载

        4. 指令自动注册

        5. ……

等通用组件或指令。

        这些内容都是咱们日常开发中非常常见的功能。现在我们把它们抽离出来之后,日后的开发中就可以直接拿过来使用了。

        现在对于首页而言,我们所欠缺的内容还是比较多的,比如:

        1. search 模块到首页的联动

        2. item 上的点击处理

        这些内容又会给大家带来什么新奇的体验呢?我们后面继续更新。 

  • 19
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
### 回答1: 下面是一个基于 jQuery 的下拉瀑布和 Mustache 模板的示例代码: HTML 部分: ```html <div id="content"></div> <div id="loading">Loading...</div> ``` CSS 部分: ```css #content { column-count: 3; column-gap: 10px; } .item { break-inside: avoid-column; margin-bottom: 10px; } img { width: 100%; } #loading { display: none; text-align: center; font-size: 20px; color: #999; margin-top: 20px; } ``` JS 部分: ```javascript $(function() { var page = 1; var isLoading = false; var isEnd = false; function loadData() { if (isLoading || isEnd) { return; } isLoading = true; $('#loading').show(); $.ajax({ url: 'data.php', type: 'get', data: { page: page }, dataType: 'json', success: function(data) { if (data.length > 0) { var template = $('#template').html(); Mustache.parse(template); var rendered = Mustache.render(template, data); $('#content').append(rendered); page++; } else { isEnd = true; } }, complete: function() { isLoading = false; $('#loading').hide(); } }); } $(window).scroll(function() { var scrollTop = $(this).scrollTop(); var windowHeight = $(this).height(); var contentHeight = $('#content').height(); if (scrollTop + windowHeight >= contentHeight && contentHeight > 0) { loadData(); } }); loadData(); }); ``` 其中,data.php 是一个返回 JSON 数据的后端接口,返回数据格式如下: ```json [ { "id": 1, "title": "item 1", "image": "image1.jpg" }, { "id": 2, "title": "item 2", "image": "image2.jpg" }, ... ] ``` Mustache 模板部分: ```html <script id="template" type="x-tmpl-mustache"> {{#items}} <div class="item"> <h2>{{title}}</h2> <img src="{{image}}" alt=""> </div> {{/items}} </script> ``` 以上代码实现了一个基本的下拉瀑布和 Mustache 模板的示例,你可以根据自己的需求进行修改和扩展。 ### 回答2: jQuery是一种行的JavaScript库,用于简化HTML文档遍历、事件处理、动画效果和AJAX等操作。下拉是一种网页方式,在用户滚动到页面底部时,通过发送请求更多内容。瀑布是一种多列布局方式,类似于瀑布动,每一列按照内容的高度自动调整位置。 jQuery可以很方便地实现下拉功能。通过监听窗口的滚动事件,当滚动到页面底部时,发送请求获取更多数据,然后将数据添到页面中。这样,用户就可以在滚动页面的过程中无缝地更多内容,提升用户体验。 瀑布布局通常使用CSS和JavaScript来实现。在jQuery中,可以使用瀑布插件如"Masonry"或"Isotope"来实现瀑布布局。这些插件可以根据内容的大小和位置自动调整各个元素的位置,从而实现瀑布效果。 Mustache是一种轻量级的模板引擎,可以将数据和HTML模板进行结合,生成动态的网页内容。在使用jQuery进行下拉瀑布布局时,Mustache可以用于将获取到的数据与指定的HTML模板进行结合,生成可展示的内容。 通过结合使用jQuery、下拉瀑布布局和Mustache,我们可以实现一个功能强大且用户友好的网页。用户可以通过滚动页面来更多内容,而不需要手动点击按钮。的内容可以利用瀑布布局自动调整位置,使页面更美观。而Mustache可以将获取到的数据动态地呈现在指定的HTML模板中,实现内容的动态更新。 总之,jQuery下拉瀑布布局和Mustache模板引擎的结合,可以让我们更便捷地实现前端开发中对于网页和布局的需求。 ### 回答3: jQuery下拉瀑布和Mustache都是常用的前端技术。 jQuery是一款优秀的JavaScript库,可以简化HTML文档的遍历、事件处理、动画效果等操作。在下拉中,可以利用jQuery监听用户滚动事件,当滚动到特定位置时触发新数据的操作。通过Ajax请求获取数据,再通过jQuery插入到页面中,实现无刷新的数据瀑布是一种网页布局方式,类似于瀑布的形态,每一块内容依次排列,高度不一,但是整体效果呈现出自然的瀑布效果。在实现瀑布布局时,可以借助jQuery的animate()函数来设置元素的位置和动画效果,为页面元素创建瀑布布局。 Mustache是一种轻量级的逻辑-less模板引擎,用于渲染模板数据到HTML文档。通过Mustache的语法标签,我们可以在HTML代码中插入占位符,然后再通过jQuery获取到数据,将数据和模板结合,最终生成动态内容,并插入到页面中。 综上所述,使用jQuery下拉瀑布和Mustache可以实现在网页中实现下拉新数据的功能,并使用瀑布布局展示数据,最后通过Mustache模板引擎渲染数据到页面中。这样能够提升用户体验和页面的可视性,实现更畅的数据展示。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

chengbo_eva

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值