Waterfall flow is a very common way of web image interaction, the effect can refer to petal net.

The preparatory work

Let’s take a look at the directory structure first, where app.js is the server startup file, mainly used to provide the interface to return the required image data, and index.html is the waterfall stream page.

├─ app.js ├─ index.html ├─ package.json ├─ Node_modulesCopy the code

The server app.js uses Express to set up the local server, in which the waterfall stream page is returned by default when visiting http://127.0.0.1:3000, and the interface for obtaining pictures is generally pageNo and pageSize. As it only provides simple data services, If the number of images is greater than 300, no data will be returned and only an empty array will be returned.

// 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}/ `)})Copy the code

In the index. HTML page, in order to support IE9 and above browsers, the Promise needs to introduce a third party CDN, and the PAGE Ajax request needs to use axiOS library. In addition, all functions on the page are ordinary functions, and only var is used for variable declaration.

<head>
    <meta charset="UTF-8">
    <title>waterfall</title>
    <script src="promise-polyfill.js"></script>
    <script src="axios.js"></script>
</head>
Copy the code

Centralize the Waterfall block horizontally in the CSS and add a shadow to the internal item element to make it look even better.

<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">Loading...</p>
Copy the code

Tool function

The JS section contains a number of utility class functions, more on each one below.

$

Pages are bound to involve a lot of element selection, so simply packaging a JQuery equivalent is enough, and you don’t need to reference the JQuery library.

function $(selector) {
    var type = selector[0]
    selector = selector.slice(1)

    if (type === The '#') {
        return document.getElementById(selector)
    } else if (type === '. ') {
        return document.getElementsByClassName(selector)
    }
}
Copy the code

getRandomInt

The getRandomInt function is used to get random integers in a specified range, including the boundary values at both ends.

function getRandomInt(min, max) {
    return Math.floor(Math.random() * (max - min + 1)) + min
}
Copy the code

getRandomHeight

GetRandomHeight obtains a random height between 200 and 500. It is not easy to collect hundreds of pictures with inconsistent heights. Use random number to simulate.

function getRandomHeight() {
    return getRandomInt(200.500) + "px"
}
Copy the code

getRandomColor

GetRandomColor gets a random background color, including transparency, between 0.1 and 1.

function getRandomColor() {
    return "rgba(" + getRandomInt(0.255) + "," + getRandomInt(0.255) + "," + getRandomInt(0.255) + "," + getRandomInt(1.10) / 10 + ")"
}
Copy the code

createItem

CreateItem is used to create the div element item, commented in the code because the image address is not available, and the height and background color of the element item are generated from the other utility functions described above.

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
}
Copy the code

request

Request The user requests to obtain an image, where params are pageNo and pageSize.

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)
        })
    })
}
Copy the code

debounce

Debounce function, used to limit the trigger frequency, takes a list of parameters and lifts the array prototype out because it is compatible with 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)
    }
}
Copy the code

The principle of partial

To form a staggered style of the elements inside the waterfall flow, it can only be achieved by positioning, so the outer waterfall needs relative positioning, and the internal elements need absolute positioning.

getCols

Then determine how many columns are displayed on the page, where width is the width of element items and gap is the gap between items. Where n * width + (n-1) * gap is the width of multiple column elements, which should be less than the width of body by default, but the body needs to leave part of the gap, so the default is less than bodywidth-margin * 2. Adjust the equation, and then round by ~~ (similar to parseInt).

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

The basic principle of waterfall flow is that after the first row is filled with elements, the subsequent elements are positioned behind the smallest column, and then filled back. Therefore, the heights array needs to be maintained globally to hold the current height of each column.

getMinIndex

GetMinIndex gets the index of the column with the smallest value in the HEIGHTS array.

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

    return array.indexOf(min)
}
Copy the code

setWaterFallRect

Note that due to the positioning of the outer waterfall block and the inner element, the inner element falls out of the document stream, causing the outer layer to fall in height. Therefore, you need to set the width and height of the outer elements based on the number of columns and heights.

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

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

waterfall

Waterfall function realizes the above function, the first line is full and fills the height value into the heights, and the subsequent elements need to judge the index with the smallest value in the heights array, calculate the left and top positioning values and apply them to the current element. At the end of the for loop, all elements are positioned and then the width and height of the outer waterfall block is updated.

Note that the variable I in the for loop starts with loaded, which is used to count elements that have been laid out. Because lazy loading is required, each time lazy loading of a new element, only the new element is laid out, and the previous element is not laid out, so as to optimize performance.

function waterfall() {
    cols = getCols()
    var items = $(".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()
}
Copy the code

implementation

The basic tool functions and function functions have been completed. First, the whole waterfall flow interface needs to be initialized, where isReq is used as a throttle valve. After lazy loading, the scroll bar is triggered too frequently.

Total Is used to record the total number of images requested. The page number of each successful request is increased by 1. The next request requests the data of the next page.

CreateDocumentFragment is used to add created DOM elements to the document. After all DOM elements have been created and added to the document, the document is inserted into the Waterfall block at one time.

The normal way is to append elements to Waterfall after they are created, but every time you insert them, the page will be rearranged. Since createDocumentFragment exists in memory and is not in the DOM tree, the page will be rearranged only once when you insert the document fragment into the Waterfall block.

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]))
        }

        $("#waterfall").appendChild(frag)

        waterfall()
    })
}
Copy the code

Lazy loading

The window is bound to a scrollbar event that triggers lazyLoad lazy loading every time it scrolls.

Note that the height of the content not shown in the document is documentHeight – Scrolltop-Clientheight. Normally, if this height is less than half of the window height, the new data is loaded.

When this condition is met, if the number of elements completed as Loaded is greater than or equal to the number of images requested, total, it means that the data returned by the server has been loaded completely and there is no need to request data again. Therefore, the scrollbar event is cancelled.

Why don’t you use scroll bars for anti-shaking? The reason is that the init function is throttled so that even if init fires frequently, there will be at most one request for the image.

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) {
            $('#msg').innerText = "There is no more."
            window.removeEventListener("scroll", lazyLoad)
            return
        }

        init()
    }
}
Copy the code

responsive

On this basis, a responsive function is made, that is, the browser window width changes, dynamic switch column number.

When the window width is changed, the entire page elements need to be rearranged, so loaded and heights are reset.

The window width below the minimum body width does not need to be rearranged, that is, at least two columns should be displayed regardless of how the window is changed.

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

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

    loaded = 0
    heights = []
    waterfall()
}
Copy the code

The complete code

Axios and Promise-Polyfill downloads are available locally or for CDN introduction.

// 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">Loading...</p>

    <script>
        (function () {
            function $(selector) {
                var type = selector[0]
                selector = selector.slice(1)

                if (type === The '#') {
                    return document.getElementById(selector)
                } else if (type === '. ') {
                    return document.getElementsByClassName(selector)
                }
            }

            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 = $("#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 = $(".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]))
                    }

                    $("#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) {
                        $('#msg').innerText = "There is no more."
                        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>
Copy the code

rendering

Lazy loading

responsive