METADATA
title: 【91kanmm开发笔记】多栏布局瀑布流的代码实现 date: 2018-06-03 17:30 tags: [前端,91kanmm开发笔记] categories: 技术
坚持做一件事真的是件很困难的事,就像坚持写博客:原本以为闲时写写就能让博客保持较高的更新率,谁会想到这不知不觉中距离上一篇博客的更新已经过去大半个月了。本篇文章整理了上一篇文章挖的多栏布局瀑布流的坑。
本篇文章主要整理 91kanmm 在开发首页(豆瓣美女页)时最重要的一个功能点:
瀑布流布局
的实现。前篇文章有提到在实现的过程中考虑过两种方案:绝对定位和分栏布局。非常火的瀑布流网站 pinterest 通过审查其网站的元素样式,可以发现每个图片都有position: absolute; top: 0px; left: 0px; transform: translateX(0px) translateY(0px); width: 100px; height: 100px;
这类的样式,可以看出它是使用绝对定位的方案实现的。
通过计算得出每个图片(或每个图片所在的item)的位置,通过样式的位移操作使其处在相应的位置。这种方法的优点...除了能够按照顺序排列数据别的也实在想不出了:依靠大量的js计算获取图片的位置,通过大量的位移操作达到期望的显示效果;而且绝对定位的元素脱离了文档流无法撑开父元素的高度,这对于之后元素的布局也不友好,于是还要给每个绝对定位的元素设定高度来撑开父元素的高度;计算元素的高度就又牵扯出了一个问题:需要等图片全部加载完了之后才能获取到准确的高度。总体来说要手动实现绝对定位的瀑布流布局还是比较麻烦的,虽然有现成的插件 Masonry 可以使用,但还是会产生大量的计算和位移,对移动端的性能是个考验。
而对于分栏布局实现瀑布流而言,它的缺点就是绝对定位布局的优点,不过在使用js处理数据的时候多做点操作也可以达到按顺序排列数据的目的;同样的它的优点便是完全克服了绝对定位布局的不足:不脱离文档流的布局,不需要大量的位移操作,虽然会有js的计算但只是简单的遍历操作,一方面每次加载的数据也就几十条,另外一方面当前移动端性能的瓶颈主要在浏览器解析、渲染dom上,这么一点小小的js计算根本不成问题。
基于这种想法,看看在91kanmm中是如何将数据处理为多栏布局适用的。
首先看一下接口(使用koa2+MongoDB搭建的后台接口https://api.91kanmm.com/api/douban/girlsData?beginId=&size=10) 返回的格式,为了阅读方便删除了结构相同的数据仅展示数据结构:
{ "code":10000, "success":true, "data":[ { "_id":"5b0930101d41c862fbfe42e3", "content":"早上只能坐公交车去吃包子,吃不完还要装到爱马仕的包包里,再徒步走到我法拉利停车的地方,上车配着我的白兰地吃完。;", "authorName":"盗图网骗小能手", "avatar":"<https://img3.doubanio.com/icon/up137650427-5.jpg>", "cid":"", "date":"2018-05-25 11:44:57", "title":"穷的快吃不起饭了,难受", "id":"1704983", "location":"常居: 北京", "imgs":[ "<https://wx3.sinaimg.cn/large/0060lm7Tgy1frnjdkzp1lj30qo0qoju4.jpg>" ] } ] }
处理函数如下:
/*将数据处理成页面对应的栏数需求的格式 * @param {Array} data * */ handleDataAdaptColumn(data) { /*变量说明: this.columnCount-显示的图片列数;*/ let len = data.length; let quotient = parseInt(len / this.columnCount); //计算每列需要的数据条数 let remainder = parseInt(len % this.columnCount); //如果不够均分计算多余的条数 let offset = 0; // 偏移量,用于slice()的计算 let returnData = []; //返回值 let count = 1; //计数器,用于将传参切成相应的多个Arrar while (count <= this.columnCount) { let itemArr = []; itemArr = data.slice(offset, offset + quotient); returnData.push(itemArr); offset += quotient; //设置下一次slice的起点 count++; } /*如果余数不为0说明不能均分,就将多出来的数据填到各列中*/ if (remainder !== 0) { /*'element-for-height'类的元素为各列的外层包裹元素,用于获取高度*/ let columnElement = document.getElementsByClassName('element-for-height'); let elementHeightArr = []; /*遍历各列获取其高度,用于将剩余的数据推到高度最短的那一列中。这样可以避免无限加载各列高度差越来越大*/ for (let item of columnElement) { elementHeightArr.push(item.clientHeight); } if (elementHeightArr.length > 0) { let minNumIndex = elementHeightArr.indexOf(Math.min(...elementHeightArr)); //获取高度最小的那一列的下标 let restArr = data.slice(-remainder); //取出不能均分的那部分数据 returnData[minNumIndex].push(...restArr); //推入高度最小的那列数据中 } } return returnData; },
这种方法适用于对图片顺序没有要求的情况;因为我们无法控制爬到图片的尺寸,所以这种方法还会根据当前各列的高度动态调整数据在各列中的分布避免某一列越来越长。
如果需要按顺序排列图片,处理起来反而更简单一点:通过取余操作判断当前数据应该在哪一列将其推进那列就可以了。mock数据格式参考上文:
/*将数据按照顺序处理成页面对应的栏数需求的格式 * @param {Array} data * */ handleDataAdaptColumnBySequence(data) { /*根据列数初始化返回值*/ let cycleIndex = 0; while (cycleIndex < this.columnCount) { returnData[cycleIndex] = []; cycleIndex++; } /*遍历根据余数判断当前item应该推倒哪一列中*/ for (let index in data) { let item = data[index]; let remainder = index % this.columnCount; returnData[remainder].push(item); } return returnData; }
这样一搞最终返回的数据就如下了:
[ [ { "_id":"5b0e00371d41c8703fff1940", "content":"抖音流量变现;张欣尧在700万粉丝的时候报价是36万一条广告。;太厉害了吧;要不大家关注我,我拿钱了分给大家。;", "authorName":"wifi", "avatar":"<https://img3.doubanio.com/icon/up137594156-5.jpg>", "cid":"", "date":"2018-05-29 12:03:20", "title":"我有一个想法", "id":"1708025", "location":"常居: 北京", "imgs":[ "<https://wx1.sinaimg.cn/large/0060lm7Tgy1frs5xktm5cj30dw0hfgp6.jpg>" ] } ], [ { "_id":"5b0e003b1d41c8703fff1941", "content":"只是腿直而已;", "authorName":"翘翘", "avatar":"<https://img3.doubanio.com/icon/up137835389-1.jpg>", "cid":"", "date":"2018-05-29 11:10:12", "title":"不觉得", "id":"1707990", "location":"常居: 江苏苏州", "imgs":[ "<https://wx2.sinaimg.cn/large/0060lm7Tgy1frs5zbws1nj30dw0iignp.jpg>" ] } ], [ { "_id":"5b0e00401d41c8703fff1942", "content":"", "authorName":"mmmmm喵", "avatar":"<https://img1.doubanio.com/icon/up145474362-7.jpg>", "cid":"", "date":"2018-05-29 10:50:36", "title":"嘴唇上面起一个包,本来就厚的嘴唇更厚了!", "id":"1707984", "location":"常居: 上海", "imgs":[ "<https://wx4.sinaimg.cn/large/0060lm7Tgy1frs62raauij30dw0aft9z.jpg>" ] } ] ]
直观一点表示数据结构便是:
[ [{},{},{}], [{},{}] ]
如此一来再遍历数据生成页面就非常方便了,具体的样式请审核 91kanmm 页面元素。这里贴出大概的布局,为阅读方便直接将样式写在各个元素的style中:
<!--外层的容器,设置display和flex-wrap使内部元素能够按照一列一列的方式排列--> <div style="display: flex;flex-wrap: wrap;"> <!--section元素即为生成的一列列元素,通过设置 flex:1; 使各列能均分宽度,再设置一下width是因为在实际使用中移动端会出现加载一定量数据之后有某一列的宽度炸了特别宽或特别窄的问题,桌面端暂未发现这个问题;因此根据列数设置各列的宽度(100% / columnCount),比如两列就是50%,三列是33.33333%,四列是25%...--> <section style="flex: 1;width: 50%;"> <!--该元素完全是为了计算列高度的--> <div class="element-for-height"> <!--这里面就是渲染出来的一个个item图片--> <figure></figure> <figure></figure> <figure></figure> </div> </section> <section style="flex: 1;width: 50%;"> <div class="element-for-height"> <figure></figure> <figure></figure> </div> </section> </div>