淺談單頁應用中前端分頁的實現方案

簡介

分頁是開發中最多見的需求之一。
對於分頁,咱們討論的最多的是後端的數據庫分頁,這關乎到咱們應用程序的性能,也是分頁這個需求的核心。
而前端要作的,是把後端返回的數據呈如今頁面上,工做被認爲是簡單瑣碎的。
在單頁應用中,咱們有不少中分頁方案,最多見的是無限滾動、上一頁 & 下一頁和頁碼。
本文將談談這三種分頁方式。前端

通用

不管使用哪一種分頁方案,咱們都須要處理一些通用的需求,如: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)));
}

下文的示例代碼中會直接調用這些函數,再也不重複定義。後端

無限滾動

不管對從前端仍是後端來講,無限滾動都是我認爲最簡單的分頁方案。
對後端來講,按照 pagelimit 直接查出範圍,而後返回一個數組給前端便可,不須要像其餘方案那樣還要查詢總數。
對前端來講,直接根據後端返回的數據進行拼接便可,當後端返回一個空數組時,能夠認爲已經到最後一頁,這時候就不須要再發請求到後端了。數組

// 後端返回的數據結構
// 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

  • 容易丟失瀏覽進度

目前有一些方案能夠解決這些缺點:性能問題能夠經過動態渲染來解決,而丟失瀏覽進度則能夠經過簡單的新開窗口來解決。

上一頁 & 下一頁

這種分頁方式和無限滾動比起來,會複雜一點點。
最主要是由於後端須要查詢總數,而後根據當前頁數來計算是否能夠查詢上一頁或下一頁。
固然,計算這部分能夠在後端作,也能夠在前端作。

後端計算

若是在後端計算,那麼後端要作的事情就有:

  • 查詢總數

  • 計算 hasPrevhasNext

  • 查詢元素列表

而前端方面則相對簡單:

  • 根據後端返回的 hasPrevhasNext 來判斷是否須要顯示上一頁/下一頁按鈕

  • 移除容器內的全部元素,再插入新的元素(即用新元素替換舊元素)

// 後端返回數據結構
// 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);
        });
}

這個方案實現起來比較簡單,但缺點是每次分頁都須要查詢總頁數,浪費資源。

前端計算

若是是前端計算的話,那麼後端要作的事情就相對簡單,只要再提供一個查詢總數的接口便可。
而前端方面,須要作更多的事情,同時要考慮當前端數據丟失時(如用戶刷新頁面)的處理方案。

  • 第一次加載頁面時須要調用一次查詢總數的接口,同時調用獲取元素的接口

  • 返回數據後計算 hasPrevhasNext,用來判斷是否須要顯示上一頁/下一頁按鈕

  • 移除容器內的全部元素,再插入新的元素(即用新元素替換舊元素)

// 後端返回數據結構
// 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...

參考資料

https://github.com/xitu/gold-...

相關文章
相關標籤/搜索