這篇文章咱們主要作三件事es6
假設咱們手裏有20張照片,這些照片能夠在保持寬高比的狀況下進行放大或者縮小。選定一個基準高度好比200pxweb
以上,就是木桶佈局的原理。json
但現實場景遠比僅實現基本效果的DEMO更復雜,以 500px 官網 和 百度圖片 爲例,主要考慮如下狀況api
爲了讓產品功能更強大咱們還須要加入即時檢索功能,用戶輸入關鍵字便可當即用木桶佈局的方式展現搜索到底圖片,當頁面滾動到底部時會加載更多數據,當調整瀏覽器尺寸時會從新渲染,效果在這裏。下圖是效果圖瀏覽器
你們一塊兒來理一理思路,看如何實現:服務器
按照這個思路,咱們能夠勉強寫出效果,但確定會遇到不少惱人的細節,好比app
當這些細節處理完成以後,咱們會發現代碼已經被改的面目全非,邏輯複雜,其餘人(可能包括明天的本身)很難看懂。frontend
咱們能夠換一種思路,使用一些方法讓代碼解耦,加強可讀性和擴展性。最經常使用的方法就是使用「發佈-訂閱模式」,或者說叫「事件機制」。發佈訂閱模式的思路本質上是:對於每個模塊,聽到命令後,作好本身的事,作完後發個通知異步
第一,咱們先實現一個事件管理器函數
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 = null return () => { clearTimeout(timer) timer = setTimeout(() => fn.bind(this)(arguments), delay) } } isToBottom() { return document.body.scrollHeight - document.body.scrollTop - document.documentElement.clientHeight < 5 } } new Interaction()
以上代碼邏輯很簡單:
須要注意上述代碼中 Class 的寫法 和 箭頭函數裏 this 的用法,這裏不作過多講解。還須要注意代碼中節流函數 throttle 的實現方式,以及頁面是否滾動到底部的判斷 isToBottom,咱們能夠直接讀代碼來理解,而後本身動手寫 demo 測試。
第三,咱們來實現數據加載模塊
class Loader { constructor() { this.page = 1 this.per_page = 10 this.keyword = '' this.total_hits = 0 this.url = '//pixabay.com/api/' this.bind() } bind() { Event.on('search', e => { this.page = 1 this.keyword = e.detail this.loadData() .then(data => { console.log(this) this.total_hits = data.totalHits Event.trigger('load_first', data) }) .catch(err => console.log(err)) }) Event.on('bottom', e => { if(this.loading) return if(this.page * this.per_page > this.total_hits) { Event.trigger('load_over') return } this.loading = true ++this.page this.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 = false return 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個事件。流程以下:
第4、咱們來實現佈局模塊
class Barrel { constructor() { this.mainNode = document.querySelector('main') this.rowHeightBase = 200 this.rowTotalWidth = 0 this.rowList = [] this.allImgInfo = [] this.bind() } bind() { Event.on('load_first', e => { this.mainNode.innerHTML = '' this.rowList = [] this.rowTotalWidth = 0 this.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 = 0 this.render(this.allImgInfo) }) } render(data) { if(!data) return let mainNodeWidth = parseFloat(getComputedStyle(this.mainNode).width) data.forEach(imgInfo => { imgInfo.ratio = imgInfo.webformatWidth / imgInfo.webformatHeight imgInfo.imgWidthAfter = imgInfo.ratio * this.rowHeightBase if (this.rowTotalWidth + imgInfo.imgWidthAfter <= mainNodeWidth) { this.rowList.push(imgInfo) this.rowTotalWidth += imgInfo.imgWidthAfter } else { let rowHeight = (mainNodeWidth / this.rowTotalWidth) * this.rowHeightBase this.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.webformatURL figureNode.appendChild(imgNode) figureNode.style.height = rowHeight + 'px' figureNode.style.width = rowHeight * imgInfo.ratio + 'px' this.mainNode.appendChild(figureNode) }) } } new Barrel()
對於佈局模塊來講考慮流程很簡單,就是從事件源拿數據本身去作佈局,流程以下:
當監聽到"resize"事件時,清空頁面內容,使用暫存的數據從新佈局
完整代碼在這裏
以上代碼實現了邏輯解耦,每一個模塊僅有單一職責原則,若是新增更能擴展性也很強。
若是你喜歡這篇文章或者以爲有用,點個贊給個鼓勵。