蘑菇街PC首頁瀑布流實踐

零、介紹

這篇文章主要是介紹網站頁面瀑布流佈局的實現,主要包括:javascript

  1. 瀑布流是什麼
  2. 瀑布流的實現原理
  3. 瀑布流的使用場景
  4. 實現中有哪些問題 & 如何解決
  5. 可擴展的使用場景

1、瀑布流是什麼

​瀑布流, 又稱瀑布流式佈局,是比較流行的一種網站頁面佈局。視覺表現爲寬度相等高度不定的元素組成的良莠不齊的多欄佈局,隨着頁面向下滾動,新的元素附加到最短的一列而不斷向下加載。html

2、瀑布流的實現原理

​瀑布流本質上就是尋找各列之中高度最小的一列,並將新的元素添加到該列後面,只要有新的元素須要排列,就繼續尋找全部列中的高度最小列,把後來的元素添加到高度最小列上。java

圖解基礎瀑布流

​咱們接下來看下爲何要永遠尋找最小列?算法

​先看圖 1 的排列順序,第一排元素的頂部會處於同一個高度,依次排列在頂端,第一排排滿以後,第二排從左往右排列。然而這種排列方式很容易出現其中一列過長或其中一列太短的狀況。app

​爲了解決圖1中列可能過長或者太短的問題,咱們按照圖 2 的方式將元素放在最短的一列進行排列。框架

3、瀑布流的使用場景

​瀑布流滑動的時候會不停的出現新的東西,吸引你不斷向下探索,巧妙的利用視覺層級、視線的任意流動來緩解視覺的疲勞,採用這種方案能夠延長用戶停留視覺,提升用戶粘度,適合那些隨意瀏覽,不帶目的性的使用場景,就像逛街同樣,邊走邊看,因此比較適合圖片、商品、資訊類的場景,不少電商相關的網站都使用了瀑布流進行承載。函數

​上圖的蘑菇街PC瀑布流效果是在基礎瀑布流的基礎上作了擴展改造, 在瀑布流頂部某一列或某幾列插入其餘非瀑布流內容。佈局

​本文將介紹這種擴展瀑布流的四列實現場景,適用基礎場景以下:網站

4、瀑布流的的實現有哪些問題&如何解決

  1. 非瀑布流內容如何插入?
  2. 如何尋找全部列的高度最小者?
  3. 如何渲染瀑布流?

Vue 實現瀑布流

咱們採用 Vue 框架來實現瀑布流,其一些自帶屬性使咱們的瀑布流實現更加簡單。ui

  • 經過 ref 能夠很方便的獲取每列高度,經過比較算法算出高度最小列。
  • 拿到高度最小列以後,將下個要插入的元素數據放到最小列的數據列表(columnList)中,經過操做數據完成元素渲染。
  • 利用 Vue 的具名插槽在瀑布流頂部插入其餘非瀑布內容。
  • 經過 watch 監測元素渲染,判斷是否繼續進行渲染和請求更多元素數據。

非瀑布流內容如何插入

經過 Vue 的具名插槽(slot),將非瀑布流元素做爲父組件的內容傳遞給瀑布流子組件。

  • 父組件經過 HTML 模板上的槽屬性關聯具名插槽,非瀑布流內容做爲具名插槽的內容提供給子組件。
  • 具名插槽有:first-col、second-col、 third-col、 last-col、 merge-slot,分別表明第1、2、3、4、合併列。
  • 子組件經過插槽名字判斷將非瀑布流內容放在哪一列。若是插槽存在,則將其所攜帶的內容插入到置頂位置。
  • 由於合併列的特殊性,若是包含合併列,則將合併列絕對定位到頂部,合併列佔的瀑布流對應的列進行下移。父組件傳合併列相關的參數給子組件:merge(判斷是否包含合併列), mergeHeight(合併列的高度),mergeColunms(合併的是哪 2 列)。

代碼實現

<!-- 父組件 -->
<div class="parent">
    <Waterfall :merge=true :mergeHeight=800 mergeColumns=[2,3]>
        <template slot="first-col">
            <!-- 第一列內容... -->
        </template>
        <template slot="second-col">
            <!-- 第二列內容... -->
        </template>
        <template slot="third-col">
            <!-- 第三列內容... -->
        </template>
        <template slot="last-col">
            <!-- 第四列內容... -->
        </template>
        <template slot="merge-col">
            <!-- 合併內容... -->
        </template>
    </Waterfall>
</div>
複製代碼
<!-- 子組件(waterfall) -->
<div class="child">
    <!-- 第一列 -->
    <div ref="column1" :style="{marginTop: merge && mergeColumns.indexOf(1) > -1 ? mergeHeight + 'px':''}">
        <template v-if="$slots['first-col']">
            <slot name="first-col"></slot>
        </template>
        <template v-for="(item, index) in columnList1">
            <!-- 第一列瀑布流內容... -->
        </template>
    </div>
    <!-- 第二列 -->
    <div ref="column2" :style="{marginTop: merge && mergeColumns.indexOf(2) > -1 ? mergeHeight + 'px':''}">
        <template v-if="$slots['second-col']">
            <slot name="second-col"></slot>
        </template>
        <template v-for="(item, index) in columnList2">
            <!-- 第二列瀑布流內容... -->
        </template>
    </div>
    <!-- 第三列 -->
    <div ref="column3" :style="{marginTop: merge && mergeColumns.indexOf(3) > -1 ? mergeHeight + 'px':''}">
        <template v-if="$slots['third-col']">
            <slot name="third-col"></slot>
        </template>
        <template v-for="(item, index) in columnList3">
            <!-- 第三列瀑布流內容... -->
        </template>
    </div>
    <!-- 第四列 -->
    <div ref="column4" v-if="is4Columns">
        <template v-if="$slots['last-col']">
            <slot name="last-col"></slot>
        </template>
        <template v-for="(item, index) in columnList4">
            <!-- 第四列瀑布流內容... -->
        </template>
    </div>
    <!-- 合併塊非瀑布流內容 -->
    <div class="column-merge" v-if="merge" :style="{left: (mergeColumns[0] - 1)*330 + 'px'}">
        <slot name="merge-col"></slot>
    </div>
</div>
複製代碼

如何尋找全部列的高度最小者

​每一列都定義一個 ref,經過 ref 獲取當前列的高度,若是該列上方有合併塊,則高度要加上合併塊的高度,而後比較 4 列高度取到最小高度,再經過最小高度算出其對應的列數。

代碼實現

// 經過ref獲取每列高度,column1,column2,column3,column4分別表明第1、2、3、四列
let columsHeight = [this.$refs.column1.offsetHeight, this.$refs.column2.offsetHeight, this.$refs.column3.offsetHeight, this.$refs.column4.offsetHeight]

// 若是包含合併塊, 則更新高度,合併塊下的列高要增長合併塊的高度
if(this.merge){
    // 若是有合併列,則合併列下的列高度要加合併內容的高度。
    columsHeight[0] = this.mergeColumns.indexOf(1) > -1 ? columsHeight[0] + this.mergeHeight : columsHeight[0];
    columsHeight[1] = this.mergeColumns.indexOf(2) > -1 ? columsHeight[1] + this.mergeHeight : columsHeight[1];
    columsHeight[2] = this.mergeColumns.indexOf(3) > -1 ? columsHeight[2] + this.mergeHeight : columsHeight[2];
		columsHeight[3] = this.mergeColumns.indexOf(4) > -1 ? columsHeight[3] + this.mergeHeight : columsHeight[3];
}

// 獲取各列最小高度
let minHeight = Math.min.apply(null, columsHeight);

// 經過最小高度,獲得第幾列高度最小
this.getMinhIndex(columsHeight, minHeight).then(minIndex => {
	 // 渲染加載邏輯
});

// 獲取高度最小索引函數
getMinhIndex(arr, value){
    return new Promise((reslove) => {
        let minIndex = 0;
        for(let i in arr){
            if(arr[i] == value){
                minIndex = i;
                reslove(minIndex);
            }
        }
    });
}

複製代碼

如何渲染瀑布流

​瀑布流經常使用在無限下拉加載或者加載數據量很大、且包含不少圖片元素的情景,因此一般不會一次性拿到全部數據,也不會一次性將拿到的數據所有渲染到頁面上,不然容易形成頁面卡頓影響用戶體驗,因此什麼時候進行渲染、什麼時候繼續請求數據就很關鍵。

什麼時候渲染

​選擇渲染的區域爲滾動高度 + 可視區域高度的 1.5 倍,既能夠防止用戶滾動到底部的時候白屏,也能夠防止渲染過多影響用戶體驗。若是最小列的高度 - 滾動高度 < 可視區域高 * 1.5 ,則繼續渲染元素,不然再也不繼續渲染。

什麼時候請求數據

​當已渲染的元素+可視區域能夠展現的預估元素個數 > 已請求到的個數 的時候纔去繼續請求更多數據,防止請求浪費。 若是已加載的元素個數 + 一屏能夠展現的元素預估個數 > 全部請求拿到的元素個數 ,則觸發下一次請求去獲取更多數據。

瀑布流渲染核心思路

  • 監測滾動,判斷是否符合渲染條件,若是符合條件則開始渲染。
  • 定義一個渲染索引 renderIndex,每渲染一個元素後 renderIndex + 1, 實時監測 renderIndex 的變化, 判斷是否符合渲染和數據請求條件。
  • 拿到最小高度列索引後,將下一個元素插入到該列中,並觸發 renderIndex + 1 進行下一輪渲染判斷。

代碼實現

data() {
    return {
        columnList1: [], // 第一列元素列表
        columnList2: [],
        columnList3: [],
        columnList4: [],
        renderIndex: -1, // 渲染第幾個item
        isRendering: false, // 是否正在渲染
        itemList: [], // 全部元素列表
        isEnd: false
    };
}

watch: {
    renderIndex(value) {

        // 當前滾動條高度
        const scrollTop = document.documentElement.scrollTop || window.pageYOffset || document.body.scrollTop;

        // 最小列高度 - 滾動高度 < 可視區域高的的1.5倍
        if (renderMinTop - scrollTop < winHeight * 1.5) {
            this.renderWaterfall();
        }

        // 已加載的元素個數 + 一屏能夠展現元素預估個數 > 全部請求拿到的元素個數
        if (loadedItemNum + canShowItemNum > this.itemList.length && !this._requesting && !this.isEnd) {
            // 請求瀑布流數據
            this.getData();
        }
    }
}


scroll() {

    // 當前滾動條高度
    const scrollTop = document.documentElement.scrollTop || window.pageYOffset || document.body.scrollTop;

    // 底部檢測高度
    const bottomDetectionTop = this.$refs.bottomDetection.offsetTop;

    const tempLastScrollTop = lastScrollTop; // lastScrollTop:上次一滾動高度
    lastScrollTop = scrollTop;

    if (tempLastScrollTop === -1) {
        this.renderWaterfall();
    }

    // 若是是向下滾動則判斷是否須要繼續渲染
    if (scrollTop > tempLastScrollTop) {
        if (bottomDetectionTop - tempLastScrollTop < winHeight * 1.5 && !this.isRendering) {
            this.renderWaterfall();
        }
    }

}

renderWaterfall() {

    // 若是尚未數據、全部數據已經渲染完成、正在渲染則不進行渲染計算操做
    if (this.itemList.length === 0 || this.renderIndex >= this.itemList.length - 1 || this.isRendering) {
        if (this.renderIndex === this.feedList.length - 1 && !this._requesting && !this.isEnd) {
            this.getData();
        }
        return;
    }
    this.isRendering = true;

    /*** *** 獲取最小高度代碼 ***/

    this.getMinhIndex(columnsHeight, minHeight).then(minIndex => {

        const key = `columnList${minIndex + 1}`;
        let itemData = this.itemList[this.renderIndex + 1];
        this[key] = this[key].concat(itemData);
        this.$nextTick(() => {
            this.renderIndex = this.renderIndex + 1;
            this.isRendering = false;
        });
    });
}
複製代碼

5、可擴展的使用場景

​ 爲了靈活使用瀑布流,在設計的時候就作好了擴展準備,經過 HTML 模板代碼能夠看出來,具名插槽的內容能夠放在任意列,並無限制死,因此能夠擴展使用到如下各個場景。

相關文章
相關標籤/搜索