这篇文章我们主要做三件事

  1. 讲解木桶布局的原理
  2. 把这个效果做成个UI 精美、功能完善的小项目
  3. 通过这个项目,演示如何去思考、如何去优化代码

木桶布局原理

假设我们手里有20张照片,这些照片可以在保持宽高比的情况下进行放大或者缩小。选定一个基准高度比如200px

  • 拿第1张照片,锁定宽高比高度压缩到200px,放到第一行
  • 拿第2张照片,高度压缩到200px,放到第一行,图片1的后面
    ...
  • 拿第5张照片,高度压缩到200px,放到第一行。oh,不好,空间不够,放不下了
  • 把前面水平依次排放好的4个图片当成一个整体,等比拉伸,整体宽度正好撑满容器
  • 第5张照片从下一行开始,继续...

以上,就是木桶布局的原理。

木桶布局项目

但现实场景远比仅实现基本效果的DEMO更复杂,以 500px 官网 和 百度图片 为例,主要考虑以下情况

  • 图片从服务器通过接口异步获取
  • 要结合懒加载实现滚动加载更多图片
  • 当屏幕尺寸发生变化后需要重新布局

为了让产品功能更强大我们还需要加入即时检索功能,用户输入关键字即可立即用木桶布局的方式展示搜索到底图片,当页面滚动到底部时会加载更多数据,当调整浏览器尺寸时会重新渲染,效果在这里。下图是效果图

大家一起来理一理思路,看如何实现:

  1. 输入框绑定事件,当输入框内容改变时,向接口发送请求获取数据
  2. 得到数据后使用木桶布局的方式渲染到页面上
  3. 当滚动到底部时获取新的页数对应的数据
  4. 得到数据后继续渲染到页面上
  5. 当浏览器窗口变化时,重新渲染

按照这个思路,我们可以勉强写出效果,但肯定会遇到很多恼人的细节,比如

  1. 当用户输入内容时,如果每输入一个字符就发送请求,会导致请求太多,如何做节流?
  2. 对于单次请求的数据,在使用木桶布局渲染时最后一行数据如何判断、如何展示?
  3. 对于后续滚动异步加载的新的数据,如何布局到页面?特别是如何处理与上一次请求渲染到页面上的最后一行数据的衔接?
  4. 当屏幕尺寸调整时,如何处理?是清空重新获取数据?还是使用已有数据重新渲染?
  5. 数据到来的时机和用户操作是否存在关联?如何处理?比如上次数据到来之前用户又发起新的搜索
  6. ......

当这些细节处理完成之后,我们会发现代码已经被改的面目全非,逻辑复杂,其他人(可能包括明天的自己)很难看懂。

优化代码

我们可以换一种思路,使用一些方法让代码解耦,增强可读性和扩展性。最常用的方法就是使用「发布-订阅模式」,或者说叫「事件机制」。发布订阅模式的思路本质上是:对于每一个模块,听到命令后,做好自己的事,做完后发个通知

第一,我们先实现一个事件管理器

class Event {static on(type, handler) {return document.addEventListener(type, handler)}static trigger(type, data) {return document.dispatchEvent(new CustomEvent(type, {detail: data}))}
}
// useage
Event.on('search', e => {console.log(e.detail)})
Event.trigger('search', 'study frontend in jirengu.com')

如果对 ES6不熟悉,可以先看看语法介绍参考这里,大家也可以使用传统的模块模式来写参考这里。当然,我们还可以不借助浏览器内置的CustomEvent,手动写一个发布订阅模式的事件管理器,参考这里 。

第二,我们来实现交互模块

class Interaction {constructor() {this.searchInput = document.querySelector('#search-ipt')this.bind()}bind() {this.searchInput.oninput = this.throttle(() => {Event.trigger('search', this.searchInput.value)}, 300)document.body.onresize = this.throttle(() =>    Event.trigger('resize'), 300)document.body.onscroll = this.throttle(() => {if (this.isToBottom()) {Event.trigger('bottom')}},3000)}throttle(fn, delay) {let timer = nullreturn () => {clearTimeout(timer)timer = setTimeout(() => fn.bind(this)(arguments), delay)}}isToBottom() {return document.body.scrollHeight - document.body.scrollTop - document.documentElement.clientHeight < 5}
}
new Interaction()  

以上代码逻辑很简单:

  1. 当用户输入内容时,节流,并且发送事件"search"
  2. 当用户滚动页面时,节流,检测是否滚动到页面底部,如果是则发起事件"bottom"
  3. 当窗口尺寸变化时,节流,发起事件"resize"

需要注意上述代码中 Class 的写法 和 箭头函数里 this 的用法,这里不做过多讲解。还需要注意代码中节流函数 throttle 的实现方式,以及页面是否滚动到底部的判断 isToBottom,我们可以直接读代码来理解,然后自己动手写 demo 测试。

第三,我们来实现数据加载模块

class Loader {constructor() {this.page = 1this.per_page = 10this.keyword = ''this.total_hits = 0this.url = '//pixabay.com/api/'this.bind()}bind() {Event.on('search', e => {this.page = 1this.keyword = e.detailthis.loadData().then(data => {console.log(this)this.total_hits = data.totalHitsEvent.trigger('load_first', data)}).catch(err => console.log(err))})Event.on('bottom', e => {if(this.loading) returnif(this.page * this.per_page > this.total_hits) {Event.trigger('load_over')return}this.loading = true++this.pagethis.loadData().then(data => Event.trigger('load_more', data)).catch(err => console.log(err))})}loadData() {return fetch(this.fullUrl(this.url, {key: '5856858-0ecb4651f10bff79efd6c1044',q: this.keyword,image_type: 'photo',per_page: this.per_page,page: this.page})).then((res) => {this.loading = falsereturn res.json()})}fullUrl(url, json) {let arr = []for (let key in json) {arr.push(encodeURIComponent(key) + '=' +   encodeURIComponent(json[key]))}return url + '?' + arr.join('&')}
}
new Loader()

因为加载首页数据与加载后续数据二者的流程是有差异的,所有对于 Loader 模块,我们根据定义了3个事件。流程如下:

  1. 当监听到"search"时,获取第一页数据,把页数设置为1,发送事件"load_first"并附上数据
  2. 当监听到"bottom"时,根据数据判断数据是否加载完了。如果加载完了发送"load_over"事件;否则把页数自增,加载数据,发送"load_more"事件并附上数据

第四、我们来实现布局模块

class Barrel {constructor() {this.mainNode = document.querySelector('main')this.rowHeightBase = 200this.rowTotalWidth = 0this.rowList = []this.allImgInfo = []this.bind()
}
bind() {Event.on('load_first', e => {this.mainNode.innerHTML = ''this.rowList = []this.rowTotalWidth = 0this.allImgInfo = [...e.detail.hits]this.render(e.detail.hits)})Event.on('load_more', e => {this.allImgInfo.push(...e.detail.hits)this.render(e.detail.hits)})Event.on('load_over', e => {this.layout(this.rowList, this.rowHeightBase)})Event.on('resize', e => {this.mainNode.innerHTML = ''this.rowList = []this.rowTotalWidth = 0this.render(this.allImgInfo)})
}
render(data) {if(!data) returnlet mainNodeWidth = parseFloat(getComputedStyle(this.mainNode).width)data.forEach(imgInfo => {imgInfo.ratio = imgInfo.webformatWidth / imgInfo.webformatHeightimgInfo.imgWidthAfter = imgInfo.ratio * this.rowHeightBaseif (this.rowTotalWidth + imgInfo.imgWidthAfter <= mainNodeWidth) {this.rowList.push(imgInfo)this.rowTotalWidth += imgInfo.imgWidthAfter} else {let rowHeight = (mainNodeWidth / this.rowTotalWidth) * this.rowHeightBasethis.layout(this.rowList, rowHeight)this.rowList = [imgInfo]this.rowTotalWidth = imgInfo.imgWidthAfter}})}layout(row, rowHeight) {row.forEach(imgInfo => {var figureNode = document.createElement('figure')var imgNode = document.createElement('img')imgNode.src = imgInfo.webformatURLfigureNode.appendChild(imgNode)figureNode.style.height = rowHeight + 'px'figureNode.style.width = rowHeight * imgInfo.ratio + 'px'this.mainNode.appendChild(figureNode)})}
}new Barrel()

对于布局模块来说考虑流程很简单,就是从事件源拿数据自己去做布局,流程如下:

  1. 当监听到"load_first"事件时,把页面内容清空,然后使用数据重新去布局
  2. 当监听到"load_more"事件时,不清空页面,直接使用数据去布局
  3. 当监听到"load_over"事件时,单独处理最后一行剩下的元素

当监听到"resize"事件时,清空页面内容,使用暂存的数据重新布局

完整代码在这里

以上代码实现了逻辑解耦,每个模块仅有单一职责原则,如果新增更能扩展性也很强。

如果你喜欢这篇文章或者觉得有用,点个赞给个鼓励。

代码修炼之路-木桶布局相关推荐

  1. 代码修炼之路——木桶布局

    这篇文章主要围绕以下三件事展开. 讲解木桶布局的原理. 把这个效果做成个UI 精美.功能完善的小项目. 通过这个项目,演示如何去思考.如何去优化代码. 木桶布局原理 假设我们手里有20张照片,这些照片 ...

  2. html木桶布局,CSS3如何实现图片木桶布局?(附代码)

    本篇文章给大家通过代码示例介绍一下使用CSS3实现图片木桶布局的方法.有一定的参考价值,有需要的朋友可以参考一下,希望对大家有所帮助. 高度相同,而宽度不一样的布局,称之为木桶布局.它有几个鲜明的特点 ...

  3. 王者荣耀的技术修炼之路

    5 月 11 - 13 日,Unite 2017 Shanghai 在上海国际会议中心举行,在案例分享专场上,腾讯王者荣耀项目技术总监邓君为我们带来了<王者技术修炼之路>的主题演讲. 以下 ...

  4. 传承or创新 ?解密分布式数据库自研修炼之路

    一直以来,数据库的核心研发团队都十分神秘,作为隐藏在幕后的隐士高人,他们对数据库研发的心得是什么?他们又对数据库的未来发展有什么看法呢?本文就由巨杉数据库核心技术研发团队的"老司机" ...

  5. html木桶布局,木桶布局 实现

    百度图片 图片来自 百度图片 像这样高度一样,而宽度不同的布局方式称之为木桶布局.它有几个鲜明的特点: 每行的图片高度一致:每行的图片都是占满的. 如何实现木桶布局 之 整体思路 我们需要先拥有一些素 ...

  6. 木桶布局 原理与实现

    项目中有一些图片布局需要按木桶布局排列,而前端工程师是个新手,不会用JS实现,只能在后端处理,直接返回处理好的图片尺寸,达到木桶布局的效果. 木桶布局就是将图片按行.等高排列,并且保证每一行图片排列正 ...

  7. 代码精进之路读后感(三)

    继续拜读范老师的代码精进之路,越读越觉得虽然短小但是很精悍,别想歪,我们说的是正经事 第三篇范老师讲了讲什么是优秀的程序员,我觉得就是我啊,会打代码还会吹牛逼扯犊子,还会说几句相声扯几嗓子小曲,别打了 ...

  8. 码斗士的修炼之路 -- 如何保持并提升战斗力

    转自:http://www.cnblogs.com/multiplesoftware/archive/2011/05/19/2050670.html 那日,我与一友人漫步.他资质过人,少言寡欲, 刚二 ...

  9. 一起学设计模式 - 一起开始设计模式的修炼之路

    文章目录 一起学设计模式 - 一起开始设计模式的修炼之路 1.为什么要学设计模式 2.设计模式的六大原则 2.1 单一职责原则(Single responsibility principle) 2.2 ...

最新文章

  1. 每天一个linux命令(11):nl命令
  2. hdu 5055(贪心)
  3. 【NYOJ-35】表达式求值——简单栈练习
  4. linux修改文件句柄数生效_修改Linux的open files参数是,立即生效,无需重启
  5. 问题之sqlyou的使用
  6. 【PAT - 甲级1095】Cars on Campus (30分)(模拟)
  7. Java Web实现信息管理
  8. 利用代码分别实现jdk动态代理和cglib动态代理_代理模式实现方式及优缺点对比...
  9. SQL Serve 查询所有可用的数据库语句
  10. 不要在给自己不学习找借口了,否则…
  11. hr签核系统可以用python做吗_数字与签核参考流程
  12. 前端使用 geetest 行为验证 web-部署教程
  13. Jupyter notebook切换python版本
  14. 摇骰子、抽奖转盘酒桌游戏 人生重启模拟器小程序源码分享-开通流量主躺着赚钱
  15. 【入门】QSS基础入门笔记
  16. 游戏感:虚拟感觉的游戏设计师指南——第十九章 游戏感的未来
  17. ASP.NET实现日期转为大写的汉字
  18. php-模板方式模式实现
  19. 昇腾AI室外移动机器人原理与应用(二 初识室外移动机器人)
  20. mysql tablespace is missing for table_Mysql报错:Tablespace is missing for table ‘db_rsk/XXX”

热门文章

  1. C++程序员的职业寿命比Java长?Java程序员同意吗?
  2. ubuntu16 安装redis并开启远程接入
  3. 关于电子电路的一些硬件资料分享
  4. [Git] Mac通过brew升级git
  5. Cadence和Synopsys工具介绍
  6. R语言中的回归诊断-- car包
  7. 艾为数字ic面试题_数字IC笔试题(2017年)
  8. python的初次接触(python3.7——安装教程)
  9. python调用程序call_Python下的subprocess.call()使用和注意事项
  10. 个人博客系统开发总结之 lucene全文检索