这篇文章主要是介绍网站页面瀑布流布局的实现,主要包括:javascript
瀑布流, 又称瀑布流式布局,是比较流行的一种网站页面布局。视觉表现为宽度相等高度不定的元素组成的良莠不齐的多栏布局,随着页面向下滚动,新的元素附加到最短的一列而不断向下加载。html
瀑布流本质上就是寻找各列之中高度最小的一列,并将新的元素添加到该列后面,只要有新的元素须要排列,就继续寻找全部列中的高度最小列,把后来的元素添加到高度最小列上。java
咱们接下来看下为何要永远寻找最小列?算法
先看图 1 的排列顺序,第一排元素的顶部会处于同一个高度,依次排列在顶端,第一排排满以后,第二排从左往右排列。然而这种排列方式很容易出现其中一列过长或其中一列太短的状况。markdown
为了解决图1中列可能过长或者太短的问题,咱们按照图 2 的方式将元素放在最短的一列进行排列。app
瀑布流滑动的时候会不停的出现新的东西,吸引你不断向下探索,巧妙的利用视觉层级、视线的任意流动来缓解视觉的疲劳,采用这种方案能够延长用户停留视觉,提升用户粘度,适合那些随意浏览,不带目的性的使用场景,就像逛街同样,边走边看,因此比较适合图片、商品、资讯类的场景,不少电商相关的网站都使用了瀑布流进行承载。框架
上图的蘑菇街PC瀑布流效果是在基础瀑布流的基础上作了扩展改造, 在瀑布流顶部某一列或某几列插入其余非瀑布流内容。函数
本文将介绍这种扩展瀑布流的四列实现场景,适用基础场景以下:布局
咱们采用 Vue 框架来实现瀑布流,其一些自带属性使咱们的瀑布流实现更加简单。网站
经过 Vue 的具名插槽(slot),将非瀑布流元素做为父组件的内容传递给瀑布流子组件。
<!-- 父组件 --> <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 ,则继续渲染元素,不然再也不继续渲染。
什么时候请求数据
当已渲染的元素+可视区域能够展现的预估元素个数 > 已请求到的个数 的时候才去继续请求更多数据,防止请求浪费。 若是已加载的元素个数 + 一屏能够展现的元素预估个数 > 全部请求拿到的元素个数 ,则触发下一次请求去获取更多数据。
瀑布流渲染核心思路
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; }); }); } 复制代码
为了灵活使用瀑布流,在设计的时候就作好了扩展准备,经过 HTML 模板代码能够看出来,具名插槽的内容能够放在任意列,并无限制死,因此能够扩展使用到如下各个场景。