前言
前几天约到了小鹅通的面试。开场还是先自我介绍,之后面试官问和别人做项目的时候有什么亮点。当时回答的时候感觉就是被面试官牵着走,一方面是自己表达能力太差了无法打动面试官~,另一方面还是自己实力太不行了。躺了一天后,决定好好写篇文章了梳理梳理扫掉自己玉玉的心情。
瀑布流
瀑布流布局(Masonry Layout)是一种常见的网页布局模式,它的特点是不同高度的项目按照从左到右、从上到下的顺序排列,形成类似于砖墙或瀑布的效果。这种布局方式特别适合展示图片、卡片式内容等,因为它能够有效地利用空间,并且提供了一种动态而吸引人的视觉效果。对于触屏设备非常友好,通过上滑动浏览,交互方式更符合直觉。话不多说我们看看小红书的页面来真实感受下瀑布流布局吧!!
纯css手写
我们以vue3为例来实现下瀑布流吧!!
<template>
<div class="page-main">
<div class="card">
<div class="card-item" v-for="(item,index) in cardList" :key="index" :style=" [{background: item.color},{height: item.height},{lineHeight: item.height}]">
<p class="text">{{index}}</p>
</div>
</div>
</div>
</template>
<script setup>
import {ref} from 'vue'
const cardList = ref([ // 模拟数据
{ color: '#FCCF0A', height: '120px' },
{ color: '#FF9F1C', height: '180px' },
{ color: '#3A86FF', height: '150px' },
{ color: '#8338EC', height: '100px' },
{ color: '#38B000', height: '200px' },
{ color: '#FF006E', height: '130px' },
{ color: '#FFBE0B', height: '160px' },
{ color: '#FB5607', height: '110px' },
{ color: '#FF006E', height: '140px' },
{ color: '#3A86FF', height: '170px' },
{ color: '#8338EC', height: '90px' },
{ color: '#38B000', height: '190px' },
])
</script>
<style lang="scss" scoped>
.page-main{
background: #ffffff;
min-height:100vh;
padding: 0 30px;
.card{
column-count: 3; // 定义三列
column-gap: 20px; // 列与列的距离为20px
.card-item{
text-align: center;
width: 216px;
border-radius: 16px;
grid-row-start: auto;
margin-bottom: 20px;
break-inside: avoid; // 不被截断
}
}
}
</style>
首先呢这段代码先是用v-for循环渲染列表数据,遍历数组或对象。话不多说看看下边的DOM结构吧!!
然后在js里边写了如下代码对cardList数组进行响应式定义
import {ref} from 'vue'
const cardList = ref([ // 模拟数据
{ color: '#FCCF0A', height: '120px' },
{ color: '#FF9F1C', height: '180px' },
{ color: '#3A86FF', height: '150px' },
{ color: '#8338EC', height: '100px' },
{ color: '#38B000', height: '200px' },
{ color: '#FF006E', height: '130px' },
{ color: '#FFBE0B', height: '160px' },
{ color: '#FB5607', height: '110px' },
{ color: '#FF006E', height: '140px' },
{ color: '#3A86FF', height: '300px' },
{ color: '#8338EC', height: '90px' },
{ color: '#38B000', height: '190px' },
])
想要实现瀑布流css是重头大戏!!我们来看看吧!
<style lang="scss" scoped>
.page-main{
background: #ffffff;
min-height:100vh;
padding: 0 300px;
.card{
column-count: 3; // 定义三列
column-gap: 20px; // 列与列的距离为20px
.card-item{
text-align: center;
width: 216px;
border-radius: 16px;
grid-row-start: auto;
margin-bottom: 20px;
break-inside: avoid; // 不被截断
}
}
}
</style>
从代码可以看出该样式是嵌套型的非常美观,Vue 支持多种预处理器,其中最常用的是 SCSS 和 Sass。你可以在 标签中指定 lang="scss" 或 lang="sass",然后使用嵌套语法。首先呢嵌套样式它允许你在父级选择器内部定义子级选择器的样式。这种方式不仅使代码更具可读性,还能提高开发效率和维护性。嵌套样式模仿了 HTML 的层级结构,使得样式规则与 DOM 结构更加一致。我们可以通过嵌套直观地看到哪些样式属于哪个元素,减少了查找和理解代码的时间。在原生 CSS 中,如果我们有多个嵌套的子元素,通常需要重复书写父级选择器。这不仅增加了代码量,还容易出错。使用嵌套样式可以避免这种冗余。 我们来解析下css代码吧!!
page-main background: #ffffff;:设置 page-main 的背景颜色为白色。这使得页面的主容器有一个干净的白色背景。
min-height: 100vh; :确保 page-main的最小高度为视口高度(即整个屏幕的高度)。这使得页面内容至少占满整个屏幕的高度,适用于需要全屏显示的页面布局。
padding: 0 300px, :为 page-main 设置左右内边距,使其内容与页面边缘保持 300px 的距离。注意,这个值非常大,可能会导致页面内容在较小的屏幕上显示不完全。建议根据实际需求调整这个值,例如改为 padding: 0 30px;以确保在不同设备上都能有合理的内边距。
对于 .card
column-count: 3; :使用 CSS 的 column-count 属性将 .card 内的内容分成三列。这个属性会将内容按顺序从上到下、从左到右填充每一列。适用于创建多列布局,如卡片列表或文章列表。 column-gap: 20px; :设置列与列之间的间距为 20px,确保每列之间有一定的间隔,避免内容过于拥挤。这个值可以根据设计需求进行调整,以达到最佳的视觉效果。
对于.card-item
text-align: center; :将 .card-item 内的文本居中对齐,使卡片内容在视觉上更加平衡。这对于标题、按钮等元素尤其有用,确保它们在卡片中居中显示。
width: 216px; :设置每个 .card-item 的宽度为 216px。这个宽度是固定的,确保每个卡片的大小一致。你可以根据设计需求调整这个值,或者使用百分比宽度来实现响应式设计。 border-radius: 16px; :为 .card-item 添加圆角边框,使其看起来更柔和,提升用户体验。圆角的半径为 16px,可以根据设计需求调整。
margin-bottom: 20px; :为每个 .card-item 设置底部外边距,使卡片之间有一定的垂直间距,避免内容过于紧凑。这个值可以根据设计需求进行调整,以达到最佳的视觉效果。
break-inside: avoid; :防止 .card-item在列中被分页或打断,确保每个卡片作为一个整体显示。这对于多列布局非常重要,避免卡片内容被拆分到不同的列中,保持卡片的完整性。
flex布局+计算元素高度
我们可以这样想一下,如果我们将页面元素摆放设置为4列,如果只有四个元素(高度不一样)那么正好一列一个元素。如果出现了第5个元素呢?那么就应该把第五个元素放在高度最低的元素下面。顺着这个思路我们就能实现瀑布流。话不多说看下代码!!
<template>
<div class="page-main">
<div class="card">
<div class="coloum1" >
<div class="card-item" v-for="(item,index) in cardList1" :key="index" :style="[{background: item.color},{height: item.height},{lineHeight: item.height}]" :class="{visibles: isVisibility}">
<p class="text">{{item.num}}</p>
</div>
</div>
<div class="coloum2">
<div class="card-item" v-for="(item,index) in cardList2" :key="index" :style="[{background: item.color},{height: item.height},{lineHeight: item.height}]" :class="{visibles: isVisibility}">
<p class="text">{{item.num}}</p>
</div>
</div>
<div class="coloum3">
<div class="card-item" v-for="(item,index) in cardList3" :key="index" :style="[{background: item.color},{height: item.height},{lineHeight: item.height}]" :class="{visibles: isVisibility}">
<p class="text">{{item.num}}</p>
</div>
</div>
</div>
</div>
</template>
<script setup>
import {ref,onMounted, reactive,nextTick} from 'vue'
const cardList = reactive([ // 测试数据
{
num: '0',
color: '#FCCF0A',
height: '100px',
},
...测试数据
])
const isVisibility = ref(true)
// 由于渲染时候对数据的两次赋值,则会出现一次闪现,需要防抖
onMounted(()=>{
equallyCard()
nextTick(()=>{
caLFlex()
}).then(()=>{
isVisibility.value = true
})
})
const cardList1 = ref([]) // 各列的数据
const cardList2 = ref([])
const cardList3 = ref([])
function equallyCard(){
// 平分数据,确保页面上遍历卡片dom的真实顺序与平分的一致 document.querySelectorAll('.card-item'))
let num = parseInt(cardList.length/3)
cardList.forEach((item,index)=>{
if(index<num){
cardList1.value.push(item)
return
}
if(index<2*num){
cardList2.value.push(item)
return
}
cardList3.value.push(item)
})
}
function caLFlex(){
let arr1 = [] // 第一列的值
let arr2 = [] // 第二列的值
let arr3 = [] // 第二列的值
let heightArry_1 = [] // 第一列的卡片高度
let heightArry_2 = [] // 第二列的卡片高度
let heightArry_3 = [] // 第二列的卡片高度
Array.from(document.querySelectorAll('.card-item')).forEach((item,index) =>{
if(index === 0){ // 第一行中的元素无需判断,直接加到新的数组中
heightArry_1.push(item.offsetHeight)
arr1.push(cardList[index])
return
}
if(index === 1){
heightArry_2.push(item.offsetHeight)
arr2.push(cardList[index])
return
}
if(index === 2){
heightArry_3.push(item.offsetHeight)
arr3.push(cardList[index])
return
}
const heightTotal_1 = heightArry_1.length ? Array.from(heightArry_1).reduce(( acc, cur ) => acc + cur) : 0 // 第一列的总高度
const heightTotal_2 = heightArry_2.length ? Array.from(heightArry_2).reduce(( acc, cur ) => acc + cur) : 0 // 第二列的总高
const heightTotal_3 = heightArry_3.length ? Array.from(heightArry_3).reduce(( acc, cur ) => acc + cur) : 0 // 第三列的总高度
// 找到最小值
let minNumber = Math.min(heightTotal_1,heightTotal_2,heightTotal_3)
switch (minNumber) {
case heightTotal_1:
heightArry_1.push(item.offsetHeight)
arr1.push(cardList[index])
break
case heightTotal_2:
heightArry_2.push(item.offsetHeight)
arr2.push(cardList[index])
break
case heightTotal_3:
heightArry_3.push(item.offsetHeight)
arr3.push(cardList[index])
break
}
})
// 重新将数据赋值给各列数组
cardList1.value = arr1
cardList2.value = arr2
cardList3.value = arr3
}
<style lang="scss" scoped>
.page-main{
background: #ffffff;
height:100vh;
overflow: hidden;
padding: 0 30px;
.card{
display: flex;
flex-direction: row;
justify-content: space-around;
.card-item{
visibility: hidden;
margin-bottom: 20px;
text-align: center;
width: 216px;
border-radius: 16px;
}
.visibles {
visibility: visible;
}
}
}
</style>
我们来解析下代码吧!! 从template的代码里边可以看到我们定义了三列,并且呢每一列都用v-for来遍历数组。 我们再来看看js代码这里边有一个equallyCard函数 这个函数通过forEach遍历将可以将cardList卡片分成三份(三个list...)值得注意的是并不是等分。本质就是先按照序列来排前1/3放在list1,1/3——2/3放在list2,最后2/3放在list3。但是这种排列也只是随机排。因此我们可以调用caLFlex函数。这个目的是通过计算每列的总高度,将卡片分配到最短的一列中,以实现视觉上的平衡布局。
好啦今天的分享就到这里啦!