vue实现瀑布流布局

1,573 阅读2分钟

一、瀑布流布局的实现原理

1、确定一行容纳几列元素

2、寻找各列所有元素高度之和的最小者、并将新元素添加到该列上

3、继续寻找各列所有元素高度之和的最小者,继续将新元素添加到该列上,直到所有元素排列完成

二、demo分析

示例中,我们明确几个需求:

1、实现一个瀑布流列表、列表具有下拉刷新、上拉加载下一页功能

1、瀑布流一行容纳两列元素

2、元素可以随着窗口的改变去自适应

三、vue组件设计:

1、demo列表组件:我们可以把业务需求封装成一个组件、使用vant下拉刷新和上拉加载ui组件完成、具体细节根据具体业务

2、瀑布流列表组件:把瀑布流两列布局的逻辑用一个组件完成

瀑布流布局实现难点第一个就是计算各个子项的位置,在css里我们把包裹子项的父元素position:relative,子项的高度根据子项的内容自适应、那么如何确定子项的高度呢?有两种方式

  • 最常见的是:一般大多数瀑布流是有封面图+标题+简介,封面图是异步加载的,可以通过监听封面图加载完成的onload事件获取封面图的高度,然后得到子项元素的高度。
  • 服务端可以直接返回图片的宽高,前端直接拿到封面图宽高,这样做的好处是可以避免列表滑动过快,页面偶尔出现大片空白,demo采用的是第2种方式

第二个难点是上拉加载瀑布流数据需要保证上拉加载下一页数据、在处理下一页数据的位置布局渲染时、之前已经渲染过的位置布局不被重新渲染、如果保证呢?我们需要找到数据列表中下一页数据前的最后一条数据

<template>  
<ul class="feed-list" :style="{ height: `${containerHeight}px` }">    
<li v-for="(item, index) in data" :key="item.id + '-' + index">     
 <feed-item :data="item" @on-click="onClick"/>    
</li>  
</ul>
</template>
<script>
import FeedItem from './feed-item'
export default {  
name: 'FeedList',  
props: {    data: Array  },  
components: {    FeedItem  },  
data () {    
return {      
leftColHeight: 0,      
rightColHeight: 0,      
lastRepositionItem: null,      
resizeTimer: null,      
resizeDelay: 200,      
repositionStartIndex: 0    
}  
},  
computed: {    
hasData () {      
return this.data && this.data.length > 0    
},    
lastItem () {      
if (this.hasData) {        
return this.data[this.data.length - 1]      } 
else {        return null      }    
},    
containerHeight () {      
return Math.max(this.leftColHeight, this.rightColHeight)    
}  
},  
created () {    
this.repositionIfNeed()    
this.setUpPageResizeListener()  
},  
watch: {    
data () {      
this.repositionIfNeed()    
}  
},  
methods: {    
repositionIfNeed () {      
if (this.hasData) {        
// 目的是找到该从第几个元素开始定位        
// 如果是翻页,数据量增加,那从上一次定位元素的下一个开始        
// 如果数据全变了,比如切换tab, 则从头开始定位        
this.repositionStartIndex = this.data.findIndex(item => item === this.lastRepositionItem) + 1        
if (this.repositionStartIndex === 0) {          
this.clearHeight()        
}        
this.$nextTick(this.reposition)      
} else {        
this.onNoData()      
}    
},    
reposition () {      
const container = this.$el     
const children = [...container.children]      
const elementsNeedReposition = children.slice(this.repositionStartIndex)      
for (const elem of elementsNeedReposition) {        
const totalHeight = this.getItemHeight(elem)        
if (this.leftColHeight <= this.rightColHeight) {          
this.appendToLeftCol(elem)          
this.leftColHeight += totalHeight        
} else {          
this.appendToRightCol(elem)          
this.rightColHeight += totalHeight        
}      
}      
this.lastRepositionItem = this.lastItem      
this.$nextTick(this.onRepositionComplete)    },    
getItemHeight (elem) {      
const height = elem.offsetHeight     
const marginTop = parseFloat(getComputedStyle(elem).marginTop)      
const totalHeight = height + marginTop      
return totalHeight    
},    
appendToLeftCol (elem) {      
elem.style.left = 0      
elem.style.right = 'initial'      
elem.style.top = `${this.leftColHeight}px`      
elem.style.opacity = '1'    
},    
appendToRightCol (elem) {      
elem.style.right = 0      
elem.style.left = 'initial'      
elem.style.top = `${this.rightColHeight}px`      
elem.style.opacity = '1'    
},    
onRepositionComplete () {      
    this.$emit('complete')    
},    
setUpPageResizeListener () {      
    window.addEventListener('resize', this.debouncedHandler)   
},    
debouncedHandler () {      
clearTimeout(this.resizeTimer)      
this.resizeTimer = setTimeout(this.onResize, this.resizeDelay)    
},    
onResize () {      
this.lastRepositionItem = null      
this.clearHeight()      
this.repositionIfNeed()    
},    
onNoData () {      
this.lastRepositionItem = null      
this.clearHeight()    
},    
clearHeight () {      
this.leftColHeight = 0      
this.rightColHeight = 0    
},    
onClick (item) {     
 const { type } = item    
}  
}}
</script>
<style lang="less">  
.feed-list{    
position: relative;    
>li{      
position: absolute;      
width: calc(50% - 5px);      
opacity: 0;      
&:nth-child(n + 3) {        
margin-top: 10px;     
}    
}  
}
</style>

3、组成瀑布流列表子项组件-处理单个子项的业务逻辑

<template>  
<div class="feed-item" @click="onClick">    
<div class="top">      
<div class="img" v-lazy:background-image="data.url" :style="imageSize"></div>    
</div>    
<div class="info">      
<div class="name">{{ name }}</div>      
<div class="desc" v-if="simpleTitle">{{ simpleTitle }}</div>      
</div>  
</div>
</template>
<script>
/** * @description 获取url参数 */
function getUrlParam (name, url = location.search) {  
const urlReg = new RegExp(`(?:&|\\?)${name}=(.*?)(?=&|/|#|$)`, 'g')  
const res = []  
let match = urlReg.exec(url)  
while (match) {    
res.push(decodeURIComponent(match[1]))    
match = urlReg.exec(url)  
}  
// 如果有多个结果返回数组,否则直接返回获取的字符串  
return res.length > 1 ? res : res[0]
}
// 图片最小/大 高/宽比
const minHeightWidthRatio = 1
const maxHeightWidthRatio = 1.5
export default {  
name: 'FeedItem',  
props: {    data: Object  },  
computed: {    
img () {      return this.data.img    },    
name () {      return this.data.name    },    
// 副标题   
simpleTitle () {      return this.data.simpleTitle    },    
imageSize () {      
let width = 0      
let height = 0      
if (this.img) {        
width = getUrlParam('y-img-width', this.img)        
height = getUrlParam('y-img-height', this.img)      
} else {       
width = 100        
height = 100      
}      
const ratio = this.getHeightWidthRatio({ height, width })      
return {        
paddingBottom: `${(ratio * 100).toFixed(2)}%`      
}    
}  
},  
methods: {    
onClick () {      
this.$emit('on-click', this.data)    
},    
getHeightWidthRatio ({ height, width }) {      
let ratio = height / width      
ratio = Math.max(ratio, minHeightWidthRatio)      
ratio = Math.min(ratio, maxHeightWidthRatio)      
return ratio    
}  
}}
</script>
<style lang="less">  
@import '~@/styles/mixins.less';  
@import '~@/styles/variables.less';  
.feed-item{    
background:#fff;    
border-radius:8px;    
overflow: hidden;    
.lazyload-placeholder();    
.img{      
background-size: cover;      
background-position: center;    
}  
}
</style>