在Vue.js中使用JS 實現瀑布流

前言

  • 最近在學習瀑布流的實現方法,網上找了許多實現的方法,但發現與本身的需求不太符合,因而對代碼進行了一些改動。
  • 我的需求:html

    1. 橫向加載
    2. 服務器返回的信息中沒有圖片尺寸
    3. 每加載完一張圖片就設置它的位置,而不是等全部圖片都顯示完再佈局
  • 性能確定會比較慢,最好的方法仍是在服務器返回圖片尺寸大小或比例。
  • 爲了方便操做,採用了在html中引用Vue.js的方法實現。

效果預覽

  • 爲了更直觀的展現,這裏設置了3G網絡並禁用了緩存

rcHzlj.gif

  • 正常網速

r6suRg.gif

主要改動地方

  • 根據盒子寬度設置列數 => 根據列數設置box的寬度
    改動緣由是根據列數設置寬度我以爲會更直觀方便,並且右側不會留下空白
// 瀑布流佈局
waterFall() {

    ...

    const columns = 3  // 列數
    const gap = 10;  // 間隔
    const itemWidth = ~~((this.getClient().width / columns - gap))

    ...
},
// 獲取頁面寬度
getClient() {
    const SCROLL_WIDTH = 20
    return {
        /*
            ** window.innerWidth - SCROLL_WIDTH: 頁面寬度(包括滾動條)- 比滾動條多一點的寬度
            ** 不使用document.body.clientWidth 和 document.documentElement.clientWidth緣由:
            ** 頁面首次加載時的寬度沒有被滾動條擠壓的,這樣會致使滾動加載時獲取的頁面寬度與首次的寬度不一致
            ** 通常瀏覽器滾動條寬度爲17px,減去20是保守估計併爲右側留必定空間
            ** 移動端滾動條是懸浮在頁面在上,不會形成擠壓,所以在移動端中能夠不減去滾動條寬度
        */
        width: window.innerWidth - SCROLL_WIDTH
        // width: window.innerWidth || document.body.clientWidth || document.documentElement.clientWidth
    }
},
// 對box進行佈局
reflow(el, itemWidth, columns, gap) {
    
    el.style.width  = itemWidth + 'px'

    ...
}
  • HTML文檔加載完畢後再統一佈局 => 加載完一張圖片就佈局一次
    (window.onload => image.onload)
// 瀑布流佈局
waterFall() {

    const columns = 3  // 列數
    const gap = 10;  // 間隔
    const itemWidth = ~~((this.getClient().width / columns - gap))

    const box = document.getElementById("big-box")
    const items = box.children

    for (let i = this.loadCount; i < items.length; i++, this.loadCount++) {

        // 獲取圖片元素
        const img = items[i].getElementsByTagName('img')[0]
        // 圖片有緩存時直接佈局(主要在窗口尺寸變化時調用)
        if(img.complete) {
            this.reflow(items[i], itemWidth, columns, gap)
        }
        // 圖片無緩存時先對加載速度快的圖片進行佈局
        else {
            img.onload = () => {
                this.reflow(items[i], itemWidth, columns, gap)
            }
        }

    }
},
// 對box進行佈局
reflow(el, itemWidth, columns, gap) {
    el.style.width  = itemWidth + 'px'

    // 第一行
    if (this.arr.length < columns) {
        el.style.top = 0;
        el.style.left = (itemWidth + gap) * this.arr.length + 'px'
        this.arr.push(el.offsetHeight)
    }
    // 其餘行
    else {
        // 最小的列高度
        const minHeight = Math.min(...this.arr)
        // 當前高度最小的列下標
        const index = this.arr.indexOf(minHeight)

        el.style.top = minHeight + gap + 'px'
        el.style.left = (itemWidth + gap) * index + 'px'

        this.arr[index] = this.arr[index] + el.offsetHeight + gap
    }
}

完整代碼

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>

    <title>JS 實現瀑布流(Vue)</title>
    <style>
        .container {
            position: relative;
        }

        .box {
            position: absolute;
            width: 0;  /* 設置0是爲了佈局時才顯示圖片,防止看到圖片都在第一張的位置上堆疊 */
        }

        .box img {
            width: 100%;
        }
    </style>

</head>

<body>
    <div id="app">
        <div id="big-box" class="container">
            <div class="box" v-for="(item, index) in pic_list" :key="index">
                <img :src="item">
            </div>
        </div>
    </div>

    <script>
        new Vue({
            el: '#app',
            name: 'WaterFall',
            data() {
                return {
                    isReSize: false,  // 窗口尺寸是否發生變化
                    lock: true,  // 鎖
                    pic_list: [],
                    arr: [],  // 存放每一列的最小高度 
                    loadCount: 0  // 已經佈局好的元素下標
                }
            },
            methods: {
                 // 瀑布流佈局
                waterFall() {

                    const columns = 3  // 列數
                    const gap = 10;  // 間隔
                    const itemWidth = ~~((this.getClient().width / columns - gap))
                    // console.log(this.getClient().width, itemWidth)

                    const box = document.getElementById("big-box")
                    const items = box.children
                    // console.log(items)

                    // 窗口尺寸發生變化時,所有box從新佈局
                    if(this.isReSize) {
                        this.loadCount = 0
                        this.arr = []
                        this.isReSize = false
                    }

                    for (let i = this.loadCount; i < items.length; i++, this.loadCount++) {

                        // 獲取圖片元素
                        const img = items[i].getElementsByTagName('img')[0]
                        // 圖片有緩存時直接佈局(主要在窗口尺寸變化時調用)
                        if(img.complete) {
                            this.reflow(items[i], itemWidth, columns, gap)
                        }
                        // 圖片無緩存時先對加載速度快的圖片進行佈局
                        else {
                            img.onload = () => {
                                this.reflow(items[i], itemWidth, columns, gap)
                            }
                        }

                    }
                    console.log('-------------------------------')
                },
                // 對box進行佈局
                reflow(el, itemWidth, columns, gap) {
                    el.style.width  = itemWidth + 'px'

                    // 第一行
                    if (this.arr.length < columns) {
                        el.style.top = 0;
                        el.style.left = (itemWidth + gap) * this.arr.length + 'px'
                        this.arr.push(el.offsetHeight)
                    }
                    // 其餘行
                    else {
                        // 最小的列高度
                        const minHeight = Math.min(...this.arr)
                        // 當前高度最小的列下標
                        const index = this.arr.indexOf(minHeight)
                        // console.log(index, minHeight)

                        el.style.top = minHeight + gap + 'px'
                        el.style.left = (itemWidth + gap) * index + 'px'

                        this.arr[index] = this.arr[index] + el.offsetHeight + gap
                    }
                    // console.log(JSON.parse(JSON.stringify(this.arr)))
                },
                // 獲取頁面寬度
                getClient() {
                    const SCROLL_WIDTH = 20
                    return {
                        /*
                         ** window.innerWidth - SCROLL_WIDTH: 頁面寬度(包括滾動條)- 比滾動條多一點的寬度
                         ** 不使用document.body.clientWidth 和 document.documentElement.clientWidth緣由:
                         ** 頁面首次加載時的寬度沒有被滾動條擠壓的,這樣會致使滾動加載時獲取的頁面寬度與首次的寬度不一致
                         ** 通常瀏覽器滾動條寬度爲17px,減去20是保守估計併爲右側留必定空間
                         ** 移動端滾動條是懸浮在頁面在上,不會形成擠壓,所以在移動端中能夠不減去滾動條寬度
                        */
                        width: window.innerWidth - SCROLL_WIDTH
                        // width: window.innerWidth || document.body.clientWidth || document.documentElement.clientWidth
                    }
                },
                 // 延遲函數,防止短期內執行屢次
                wait(func, time=300) {
                    if(this.lock) {
                        this.lock = false
                        setTimeout(() => {
                            func()
                            this.lock = true
                        }, time)
                    }
                },
                async getPic() {
                    // const {data: res} = await axios.get('http://127.0.0.1:5000/pics')

                    // 經過訪問本地資源模擬獲取圖片路徑
                    res = [
                        "/static/img/1.jpg",
                        "/static/img/2.jpg",
                        "/static/img/3.jpg",
                        "/static/img/4.jpg",
                        "/static/img/5.jpg",
                        "/static/img/6.jpg",
                        "/static/img/7.png",
                        "/static/img/8.png",
                        "/static/img/9.jpg",
                        "/static/img/10.jpg",
                        "/static/img/11.jpg",
                        "/static/img/12.jpg",
                        "/static/img/13.jpg",
                        "/static/img/14.jpg",
                        "/static/img/15.jpg",
                        "/static/img/16.jpg",
                        "/static/img/17.png",
                        "/static/img/18.jpg",
                        "/static/img/19.jpg",
                        "/static/img/20.png",
                    ]

                    this.pic_list.push(...res)
                    await this.$nextTick()
                    this.waterFall()
                }
            },
            mounted() {
                this.getPic()
                // 窗口尺寸變化事件
                window.onresize = () => {
                    this.isReSize = true
                    this.wait(this.waterFall)
                }
                 // 窗口滾動事件
                window.onscroll = () => {
                    this.wait(() => {
                        // 是否滾動到底部
                        const IS_BOTTOM = document.documentElement.scrollHeight - document.documentElement.scrollTop <= document.documentElement.clientHeight
                        if(IS_BOTTOM) {
                            this.getPic()
                            console.log('到底了')
                        }
                    })
                }
            }
        })
    </script>
</body>

</html>

存在的問題

  • 目前圖片的尺寸獲取和位置設置都是在image的onload方法中執行的,效率會比較慢
  • 窗口尺寸改變時會對全部的圖片進行從新佈局(能夠考慮加個數組按圖片加載順序記錄下標)
  • 圖片未所有加載完就滾動到底部觸發可能會致使數據缺失?(未驗證)
    能夠自行加個所有加載完才執行的判斷。

參考文章

相關文章
相關標籤/搜索