阅读 669
解析图片的瀑布流(含懒加载)原理,并搭配服务端交互数据

解析图片的瀑布流(含懒加载)原理,并搭配服务端交互数据

  瀑布流是一种很常见的网页图片交互方式,效果可以参考 花瓣网

准备工作

  首先来查看一下目录结构,其中app.js为服务端启动文件,主要用来提供接口,返回所需的图片数据,index.html为瀑布流页面。

├── app.js
├── index.html
├── package.json
├── node_modules/
复制代码

  服务端app.js利用express搭建本地服务器,其中访问http://127.0.0.1:3000默认返回瀑布流页面,获取图片接口一般是以pageNopageSize的分页模式,由于仅是提供简单的数据服务,根据请求参数返回图片列表即可,不必太多纠结逻辑,注意图片数量一般有限,假定大于300则不再返回数据只返回空数组。

// app.js
const express = require("express")
const fs = require("fs")
const app = new express()
const port = 3000

app.get("/", (req, res) => {
  fs.readFile("./index.html", "UTF-8", (err, data) => {
    if (err) return "404 not found"

    res.send(data)
  })
})

app.get("/imgs", (req, res) => {
  const { pageSize, pageNo } = req.query
  const lists = []
  const total = 300

  for (var i = 0; i < pageSize; i++) {
    lists.push("http://127.0.0.1/images/img.png")
  }

  res.send({
    pageNo,
    pageSize,
    total,
    lists: pageNo * pageSize > total ? [] : lists
  })
})

app.listen(port, () => {
  console.log(`app is running at http://127.0.0.1:${port}/`)
})
复制代码

  index.html页面内,为了支持IE9及以上浏览器,Promise需引入第三方CDN,同时页面ajax请求需要用到axios库,另外页面所有函数均为普通函数,变量声明也仅用var,别问为什么,问就是兼容IE

<head>
    <meta charset="UTF-8">
    <title>waterfall</title>
    <script src="promise-polyfill.js"></script>
    <script src="axios.js"></script>
</head>
复制代码

  CSS中将waterfall块水平居中,内部item元素加了阴影,效果上会更加好一点。

<style>
    body {
        margin: 0;
        min-width: 600px;
    }

    #waterfall {
        margin: 16px auto;
        position: relative;
    }

    .item {
        width: 230px;
        border-radius: 10px;
        position: absolute;
        box-shadow: rgba(0, 0, 0, 0.24) 0px 3px 8px;
    }

    #msg {
        font-size: 18px;
        font-weight: bold;
        text-align: center;
        margin: 0;
        height: 80px;
        line-height: 80px;
        color: #3d3d3d;
    }
</style>

<div id="waterfall"></div>
<p id="msg">正在加载中...</p>
复制代码

工具函数

  js部分包括很多工具类函数,接下来逐个详述。

getRandomInt

  getRandomInt函数用于获取指定范围内的随机整数,包括两端的边界值在内。

function getRandomInt(min, max) {
  return Math.floor(Math.random() * (max - min + 1)) + min
}
复制代码

getRandomHeight

  getRandomHeight获取随机高度,介于200500之间,几百张高度不一致的图片不太好收集,利用随机数模拟即可。

function getRandomHeight() {
  return getRandomInt(200, 500) + "px"
}
复制代码

getRandomColor

  getRandomColor获取随机背景色,包括透明度,介于为0.11之间。

function getRandomColor() {
  return "rgba(" + getRandomInt(0, 255) + ", " + getRandomInt(0, 255) + ", " + getRandomInt(0, 255) + ", " + getRandomInt(1, 10) / 10 + ")"
}
复制代码

createItem

  createItem用于创建div元素项,由于图片地址不可用,所以代码中注释了,元素项的高度和背景色根据上述其它工具函数生成。

function createItem(src) {
  var div = document.createElement("div")

  // var img = document.createElement('img')
  // img.src = src
  // div.appendChild(img)

  div.className = "item"
  div.style.background = getRandomColor()
  div.style.height = getRandomHeight()

  return div
}
复制代码

request

  request用户请求获取图片,其中paramspageNopageSize

function request(params) {
  return new Promise(function(resolve, reject) {
    axios({
      url: "http://127.0.0.1:3000/imgs",
      params: params
    }).then(function(res) {
      resolve(res.data)
    })
  })
}
复制代码

debounce

  debounce防抖函数,用于限制触发频率,取个参数列表还把数组原型抬出来了,因为要兼容IE

function debounce(fn, delay) {
  delay = delay || 100
  var timer = null

  return function() {
    var args = Array.prototype.slice.apply(arguments)

    if (timer) {
      clearTimeout(timer)
      timer = null
    }

    timer = setTimeout(function() {
      fn.apply(this, args)
    }, delay)
  }
}
复制代码

原理部分

  瀑布流内部的元素要形成交错的样式风格,只能通过定位实现,因此外层waterfall需要相对定位,内部元素需要绝对定位。

getCols

  然后再确定页面具体显示几列,其中width为元素项宽,gap为项与项之间的间隙。其中n * width + (n - 1) * gap为多列元素项所占宽度,应默认小于body的宽度,但是body左右需要留部分间隙,因此默认小于bodyWidth - margin * 2。调整等式,再通过~~(类似parseInt)取整即可。

function getCols() {
  // n * width + (n - 1) * gap <= bodyWidth - margin * 2
  return ~~((document.body.offsetWidth - 32 + gap) / (width + gap))
}
复制代码

  瀑布流的最根本原理就是,首行铺满元素后,后续元素均定位在高度最小的列的后面,依次往后定位铺满。因此全局下需要维护heights数组,用于存放每一列的当前高度。

在这里插入图片描述

getMinIndex

  getMinIndex用于获取heights数组中值最小的列的索引。

function getMinIndex(array) {
  var min = Math.min.apply(null, array)

  return array.indexOf(min)
}
复制代码

setWaterFallRect

  注意由于外层waterfall块和内层元素定位的原因,内层元素脱离文档流,造成外层高度塌陷了。因此需要根据列数和heights共同设置外层元素的宽高。

function setWaterFallRect() {
  var wf = document.querySelector("#waterfall")
  var max = Math.max.apply(null, heights)

  wf.style.height = max + "px"
  wf.style.width = width * cols + (cols - 1) * gap + "px"
}
复制代码

waterfall

  waterfall函数即实现上述功能,首行铺满同时填充高度值到heights中,后续的元素需要判断heights数组中值最小的索引,计算出lefttop定位值并应用于当前元素。for循环结束所有的元素项布局定位完成,此时再更新外层waterfall块的宽高。

  注意for循环中变量i初始值为loadedloaded用于对已经完成布局定位的元素计数。因为需要配合懒加载,每次懒加载新增元素时,都只对新增的元素进行布局定位,而之前的元素则不再布局,以此来优化性能。

function waterfall() {
  cols = getCols()
  var items = document.querySelectorAll("#waterfall .item")

  for (var i = loaded; i < items.length; i++) {
    var item = items[i]
    var height = item.offsetHeight

    if (i < cols) {
      item.style.top = 0
      item.style.left = i * (width + gap) + "px"
      heights.push(height)
    } else {
      var minIndex = getMinIndex(heights)
      var top = heights[minIndex] + gap

      item.style.top = top + "px"
      item.style.left = minIndex * (width + gap) + "px"
      heights[minIndex] = top + height
    }

    loaded++
  }

  setWaterFallRect()
}
复制代码

实现部分

  基础的工具函数和功能函数都已经完成,首先需要初始化整个瀑布流界面,其中isReq用作节流阀,后面接入懒加载时,滚动条触发过于频繁,若接口处于请求过程中,则不再请求。

  total用于记录请求的图片总数,每次请求成功分页码加1,下次请求则请求下一页的数据。

  createDocumentFragment用于将创建的DOM元素加入到文档片中,待所有的DOM创建完成并加入到文档片中时,再将文档片一次性插入到waterfall块中。

  常规的方式是创建完元素就appendwaterfall中,但是每次插入都会造成页面重排,而由于createDocumentFragment存在于内存中,不在DOM树中,因此将文档片插入到waterfall块中时页面仅仅重排一次。

function init() {
  if (isReq) return
  isReq = true

  request(params).then(function(res) {
    var lists = res.lists
    var frag = document.createDocumentFragment()

    total = res.total
    isReq = false
    params.pageNo++

    for (var i = 0; i < lists.length; i++) {
      frag.appendChild(createItem(lists[i]))
    }

    document.querySelector("#waterfall").appendChild(frag)

    waterfall()
  })
}
复制代码

懒加载

  window绑定滚动条事件,每次滚动都会触发lazyLoad懒加载。

  注意文档未显示的内容高度为documentHeight - scrollTop - clientHeight,一般此部分高度小于窗口高度的一半就加载新的数据。

  满足此条件的同时,若完成布局的元素数量loaded大于或者等于请求的图片数量total,即表示服务端返回的数据已经全部加载完成,不用再请求数据,因此注销滚动条事件。

  为什么次处不用做滚动条防抖呢?原因在于init函数做了节流处理,即便是init频繁触发,获取图片的请求也最多只会有一个。

window.addEventListener("scroll", lazyLoad)

function lazyLoad() {
  var scrollTop = document.documentElement.scrollTop || document.body.scrollTop
  var documentHeight = document.documentElement.scrollHeight
  var clientHeight = window.innerHeight

  // documentHeight - scrollTop - clientHeight < 0.5 * clientHeight
  if (documentHeight - scrollTop < 1.5 * clientHeight) {
    if (loaded >= total) {
      document.querySelector("#msg").innerText = "没有更多了"
      window.removeEventListener("scroll", lazyLoad)
      return
    }

    init()
  }
}
复制代码

响应式

  在此基础上再做一个响应式功能,即浏览器窗口宽度改变,动态切换列数。

  窗口宽度改变后,整个页面的元素项需要重新布局,因此loadedheights都要重置。

  窗口宽度低于body的最小宽度无需重新布局,即无论窗口如何改变,至少显示两列。

window.addEventListener("resize", debounce(resize, 50))

function resize() {
  if (document.body.offsetWidth < 600) return

  loaded = 0
  heights = []
  waterfall()
}
复制代码

完整代码

  axiospromise-polyfill 下载本地或CDN引入都可。

// index.html
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>waterfall</title>
    <script src="promise-polyfill.js"></script>
    <script src="axios.js"></script>
    <style>
        body {
            margin: 0;
            min-width: 600px;
        }

        #waterfall {
            margin: 16px auto;
            position: relative;
        }

        .item {
            width: 230px;
            border-radius: 10px;
            position: absolute;
            box-shadow: rgba(0, 0, 0, 0.24) 0px 3px 8px;
        }

        #msg {
            font-size: 18px;
            font-weight: bold;
            text-align: center;
            margin: 0;
            height: 80px;
            line-height: 80px;
            color: #3d3d3d;
        }
    </style>
</head>

<body>
    <div id="waterfall"></div>
    <p id="msg">正在加载中...</p>

    <script>
        (function () {
            function getRandomInt(min, max) {
                return Math.floor(Math.random() * (max - min + 1)) + min
            }

            function getRandomHeight() {
                return getRandomInt(200, 500) + "px"
            }

            function getRandomColor() {
                return "rgba(" + getRandomInt(0, 255) + ", " + getRandomInt(0, 255) + ", " + getRandomInt(0, 255) + ", " + getRandomInt(1, 10) / 10 + ")"
            }

            function createItem(src) {
                var div = document.createElement('div')

                // var img = document.createElement('img')
                // img.src = src
                // div.appendChild(img)

                div.className = 'item'
                div.style.background = getRandomColor()
                div.style.height = getRandomHeight()

                return div
            }

            function request(params) {
                return new Promise(function (resolve, reject) {
                    axios({
                        url: 'http://127.0.0.1:3000/imgs',
                        params: params
                    }).then(function (res) {
                        resolve(res.data)
                    })
                })
            }

            function debounce(fn, delay) {
                delay = delay || 100
                var timer = null

                return function () {
                    var args = Array.prototype.slice.apply(arguments)

                    if (timer) {
                        clearTimeout(timer)
                        timer = null
                    }

                    timer = setTimeout(function () {
                        fn.apply(this, args)
                    }, delay)
                }
            }

            function getCols() {
                // n * width + (n - 1) * gap <= bodyWidth - margin * 2
                return ~~((document.body.offsetWidth - 32 + gap) / (width + gap))
            }

            function getMinIndex(array) {
                var min = Math.min.apply(null, array)

                return array.indexOf(min)
            }

            function setWaterFallRect() {
                var wf = document.querySelector("#waterfall")
                var max = Math.max.apply(null, heights)

                wf.style.height = max + 'px'
                wf.style.width = width * cols + (cols - 1) * gap + 'px'
            }

            function waterfall() {
                cols = getCols()
                var items = document.querySelector("#waterfall .item")

                for (var i = loaded; i < items.length; i++) {
                    var item = items[i]
                    var height = item.offsetHeight

                    if (i < cols) {
                        item.style.top = 0
                        item.style.left = i * (width + gap) + 'px'
                        heights.push(height)
                    } else {
                        var minIndex = getMinIndex(heights)
                        var top = heights[minIndex] + gap

                        item.style.top = top + 'px'
                        item.style.left = minIndex * (width + gap) + 'px'
                        heights[minIndex] = top + height
                    }

                    loaded++
                }

                setWaterFallRect()
            }

            function init() {
                if (isReq) return
                isReq = true

                request(params).then(function (res) {
                    var lists = res.lists
                    var frag = document.createDocumentFragment()

                    total = res.total
                    isReq = false
                    params.pageNo++

                    for (var i = 0; i < lists.length; i++) {
                        frag.appendChild(createItem(lists[i]))
                    }

                    document.querySelector("#waterfall").appendChild(frag)

                    waterfall()
                })
            }

            function lazyLoad() {
                var scrollTop = document.documentElement.scrollTop || document.body.scrollTop
                var documentHeight = document.documentElement.scrollHeight
                var clientHeight = window.innerHeight

                // documentHeight - scrollTop - clientHeight < 0.5 * clientHeight
                if (documentHeight - scrollTop < 1.5 * clientHeight) {
                    if (loaded >= total) {
                        document.querySelector('#msg').innerText = "没有更多了"
                        window.removeEventListener("scroll", lazyLoad)
                        return
                    }

                    init()
                }
            }

            function resize() {
                if (document.body.offsetWidth < 600) return

                loaded = 0
                heights = []
                waterfall()
            }

            var width = 230

            var gap = 16

            var loaded = 0

            var cols = 0

            var params = {
                pageNo: 1,
                pageSize: 20
            }

            var total = 0

            var heights = []

            var isReq = false

            init()

            window.addEventListener("scroll", lazyLoad)

            window.addEventListener('resize', debounce(resize, 50))
        })()
    </script>
</body>

</html>
复制代码

效果图

懒加载

在这里插入图片描述

响应式

在这里插入图片描述

🎉 写在最后

🍻伙伴们,如果你已经看到了这里,觉得这篇文章有帮助到你的话不妨点赞👍或 Star ✨支持一下哦!

手动码字,如有错误,欢迎在评论区指正💬~

你的支持就是我更新的最大动力💪~

GitHubBlog掘金CSDN 同步更新,欢迎关注😉~

文章分类
前端
文章标签