分頁是開發中最多見的需求之一。
對於分頁,咱們討論的最多的是後端的數據庫分頁,這關乎到咱們應用程序的性能,也是分頁這個需求的核心。
而前端要作的,是把後端返回的數據呈如今頁面上,工做被認爲是簡單瑣碎的。
在單頁應用中,咱們有不少中分頁方案,最多見的是無限滾動、上一頁 & 下一頁和頁碼。
本文將談談這三種分頁方式。前端
不管使用哪一種分頁方案,咱們都須要處理一些通用的需求,如:node
解析 url,提取當前頁面的參數git
根據返回數據生成自定義 DOMgithub
移除某個 Node 節點中的全部子元素數據庫
往某個 Node 節點中插入元素列表json
// 解析 url 中的查詢字符串 // 如 http://host/items?page=5 中提取 page=5 中的 5 function parsePage() { var searchString = window.location.search.substr(1).split('&').filter(v => v.indexOf('page') !== -1)[0]; var page = Number(searchString.split('=')[1]); return isNaN(page) ? 1 : page; } // 生成自定義 DOM // generateItemView :: Object -> DOM Node function generateItemView(object) { /* implementation */ } // 移除 Node 中全部子節點 function removeItems(node) { while (node.firstChild) { node.removeChild(node.firstChild); } } // 往 Node 中插入元素列表 function insertItems(node, items) { items.forEach(item => node.appendChild(generateItemView(item))); }
下文的示例代碼中會直接調用這些函數,再也不重複定義。後端
不管對從前端仍是後端來講,無限滾動都是我認爲最簡單的分頁方案。
對後端來講,按照 page
和 limit
直接查出範圍,而後返回一個數組給前端便可,不須要像其餘方案那樣還要查詢總數。
對前端來講,直接根據後端返回的數據進行拼接便可,當後端返回一個空數組時,能夠認爲已經到最後一頁,這時候就不須要再發請求到後端了。數組
// 後端返回的數據結構 // GET /items?page=5 { items: [...] }
// 前端處理 function getItems(page) { fetch(`/items?page=${page}`) .then(res => res.json()) .then(res => { if (res.items.length > 0) { insertItems( document.getElementById('container'), res.items ); } else { alert('No more data'); } }); }
無限滾動雖然實現起來簡單,用戶體驗也不錯,但有一些致命的缺點:數據結構
容易出現性能問題app
容易丟失瀏覽進度
目前有一些方案能夠解決這些缺點:性能問題能夠經過動態渲染來解決,而丟失瀏覽進度則能夠經過簡單的新開窗口來解決。
這種分頁方式和無限滾動比起來,會複雜一點點。
最主要是由於後端須要查詢總數,而後根據當前頁數來計算是否能夠查詢上一頁或下一頁。
固然,計算這部分能夠在後端作,也能夠在前端作。
若是在後端計算,那麼後端要作的事情就有:
查詢總數
計算 hasPrev
和 hasNext
查詢元素列表
而前端方面則相對簡單:
根據後端返回的 hasPrev
和 hasNext
來判斷是否須要顯示上一頁/下一頁按鈕
移除容器內的全部元素,再插入新的元素(即用新元素替換舊元素)
// 後端返回數據結構 // GET /items?page=5 { // hasPrev 和 hasNext 都須要後端去查詢總數,而後計算出來 hasPrev: true, hasNext: true, items: [...] }
// 前端處理 function getItems(page) { fetch(`/items?page=${page}`) .then(res => res.json()) .then(res => { res.hasPrev ? document.getElementById('prevButton').style.display = 'block' : document.getElementById('prevButton').style.display = 'none'; res.hasNext ? document.getElementById('nextButton').style.display = 'block' : document.getElementById('nextButton').style.display = 'none'; var container = document.getElementById('container'); removeItems(container); insertItems(container, res.items); }); }
這個方案實現起來比較簡單,但缺點是每次分頁都須要查詢總頁數,浪費資源。
若是是前端計算的話,那麼後端要作的事情就相對簡單,只要再提供一個查詢總數的接口便可。
而前端方面,須要作更多的事情,同時要考慮當前端數據丟失時(如用戶刷新頁面)的處理方案。
第一次加載頁面時須要調用一次查詢總數的接口,同時調用獲取元素的接口
返回數據後計算 hasPrev
和 hasNext
,用來判斷是否須要顯示上一頁/下一頁按鈕
移除容器內的全部元素,再插入新的元素(即用新元素替換舊元素)
// 後端返回數據結構 // GET /itemsCount { total: 100 } // GET /items?page=5 { items: [...] }
// 前端處理 var total = 0; var limit = 10; window.onload = getItemsCount(getItems); // 獲取總數 function getItemsCount(callback) { fetch('/itemsCount') .then(res => res.json()) .then(res => { total = res.total; callback.call(null, parsePage()); }); } function getItems(page) { fetch(`/items?page=${page}`) .then(res => res.json()) .then(res => { var hasPrev = page != 1; var hasNext = page != Math.ceil(total / limit); hasPrev ? document.getElementById('prevButton').style.display = 'block' : document.getElementById('prevButton').style.display = 'none'; hasNext ? document.getElementById('nextButton').style.display = 'block' : document.getElementById('nextButton').style.display = 'none'; var container = document.getElementById('container'); removeItems(container); insertItems(container, res.items); }); }
這種方案可讓後端甩鍋給前端,前端的活又變多拉!
最後咱們談談頁碼分頁。
這個方案和「上一頁 & 下一頁」的方案很相似,不一樣的地方在於這個方案須要根據當前頁面和總數來生成頁碼。
生成頁碼是這個方案最麻煩的地方。舉個簡單的例子,假設咱們的數據有 50 頁,咱們不可能把全部頁碼都顯示出來,須要生成一組不連續的頁碼。
咱們能夠採用下面的形式來顯示頁面:
// ------------------------------ // 我我的比較喜歡用 -1 來表示省略的區域 // 在生成 DOM 的時候,能夠用省略號來展現 // ------------------------------ // 假設當前是第 1 頁 [1, 2, 3, -1, 50] // 假設當前是第 3 頁 [1, 2, 3, 4, 5, -1, 50] // 假設當前是第 25 頁 [1, -1, 23, 24, 25, 26, 27, -1, 50] // 假設當前是第 48 頁 [1, -1, 46, 47, 48, 49, 50] // 假設當前是第 50 頁 [1, -1, 48, 49, 50]
生成頁碼的原則一般都是:
第一頁和最後一頁必須展現
其餘頁面按需展現,一般是當前頁面的先後兩頁(即 x +- 2)
當頁數少於 10 頁的時候,直接顯示出全部頁碼(爲何是 10 頁?其實在知足前兩個原則的狀況下,只要 7 頁省略號就會正常顯示了。但頁數較少的狀況下顯示省略號感受怪怪的。)
var lastPage = Math.ceil(total / limit); // 根據當前頁生成動態頁碼 function genPages() { if (lastPage <= 10) { return Array(lastPage).fill().map((v, i) => i + 1); } // dynamicPages 爲除第一頁和最後一頁以外的頁碼,-1 表示省略號 var dynamicPages; if (page === 1) { dynamicPages = [2, 3, -1]; } else if (page === 2) { dynamicPages = [2, 3, 4, -1]; } else if (page === 3) { dynamicPages = [2, 3, 4, 5, -1]; } else if (page === lastPage - 2) { dynamicPages = [-1, page - 2, page - 1, page, page + 1]; } else if (page === lastPage - 1) { dynamicPages = [-1, page - 2, page - 1, page]; } else if (page === lastPage) { dynamicPages = [-1, page - 2, page - 1]; } else { dynamicPages = [-1, page - 2, page - 1, page, page + 1, page + 2, -1]; } dynamicPages.unshift(1); dynamicPages.push(lastPage); return dynamicPages; }
生成動態頁碼這部分的邏輯,不管放在前端仍是後端都影響不大,能夠按照本身須要去選擇。
至於其餘部分的細節,和「上一頁 & 下一頁」相似,這裏就再也不重複了。
http://scarletsky.github.io/2...