Постраничная навигация в Backbone.js
Backbone.js

Постраничная навигация в Backbone.js

Оригинал: Pagination and Backbone.js, Benjamin Sterling

В статье Бенджамина Стерлинга рассматривается способ создания клиентской постраничной навигации для проекта на Backbone.js. Код в статье несколько неряшливый, содержит много ляпов, но при переводе я оставила его «как есть». Сам подход, описанный в статье, довольно интересный, способ действительно хорошо работаетдля больших коллекций. Думаю, читатель сможет найти для себя что-то полезное.

Недавно мне нужно было создать постраничную навигацию для проекта на Backbone.js, над которым я работаю. То, что я сделал, работало довольно хорошо, но я не был на 100% доволен результатом. И так как у меня была неделя отдыха, я решил переписать ее в виде миксина и почистить код. Придется теперь снова внедрить ее проект, но об этом я буду беспокоиться позже.

Давайте рассмотрим мое решение. Некоторые предостережения: я уверен, что есть, вероятно лучший вариант для некоторых участков кода, поэтому, пожалуйста, посоветуйте, что может быть улучшено. Мой код предполагает, что все данные уже загружены в коллекцию. Максимальный размер коллекции, с которой я работал, достигал 28000 элементов. Хорошо это или плохо? Из всех статей, которые я читал, я так и не смог найти однозначного ответа.

Структура JSON, с которой я имел дело:


{
    "status": true,
    "tags": [
	{
	    "id":1,
	    "name":"A"
	}, ...
    ]
}

Модель, здесь нет ничего особенного:


(function (models) {
    models.Tag = Backbone.Model.extend({});
})(App.models);

Коллекция, опять же, довольно простая. Я устанавливаю в качестве модели App.models.Tag, url для доступа к данным — tags_all.php и переопределяю метод parse, чтобы он возвращал массив тегов.

В последней строке мы подмешиваем модуль Pagination (?: не уверен, какое название десь будет более правильным. Модуль, класс?).


(function (collections, pagination, model) {
    collections.Tags = Backbone.Collection.extend({
        model : model,
        url : 'tags_all.php',		


	  /**
	   * @param resp the response returned by the server
	   * @returns (Array) tags
	   */
	  parse : function (resp) {
		var tags = resp.tags;
            return tags;
        }
    });
    _.extend(collections.Tags.prototype, pagination);
})(App.collections, App.mixins.Pagination, App.models.Tag);

Далее код миксина Pagination. Комментарии я напишу после кода.


(function (mixins) {
    /**
     * @class
     * Pagination
     */
    mixins.Pagination = {
        /**  how many items to show per page */
        perPage: 20,

	/** page to start off on */
	page: 1,

        nextPage: function () {
            var self = this;

            self.page = ++self.page;
            self.pager();
        },

        previousPage: function () {
            var self = this;

            self.page = --self.page || 1;
            self.pager();
        },

        goTo: function (page) {
            var self = this;

            self.page = parseInt(page,10);
            self.pager();
        },

        howManyPer: function (perPage) {
            var self = this;

            self.page = 1;
            self.perPage = perPage;
            self.pager();
        },

        setSort: function (column, direction) {
            var self = this;

            self.pager(column, direction);
        },		


        pager: function (sort, direction) {
            var self = this,
                start = (self.page-1)*this.perPage,
                stop  = start+self.perPage;

            if (self.orgmodels === undefined) {
                self.orgmodels = self.models;
            }

            self.models = self.orgmodels;

            if (sort) {
                self.models = self._sort(self.models, sort);
            }

            self.reset(
                self.models.slice(start,stop)
            );
        },

        _sort: function (models, sort) {
            models = models.sort(function(a,b) {
                var a = a.get(sort),
                b = b.get(sort);

                if (direction === 'desc') {
                    if (a > b) {
                        return -1;
                    }

                    if (a < b) {
                        return 1;
                    }
                } else {
                    if (a < b) {
                        return -1;
                    }

                    if (a > b) {
                        return 1;
                    }
                }

                return 0;
            });	
            return models;
        },

        info: function () {
            var self = this,
                info = {},
                totalRecords = (self.orgmodels) ? self.orgmodels.length : self.length,
                totalPages = Math.ceil(totalRecords/self.perPage);			
            info = {
                totalRecords: totalRecords,
                page: self.page,
                perPage: self.perPage,
                totalPages: totalPages,
                lastPage: totalPages,
                lastPagem1: totalPages-1,
                previous: false,
                next: false,
                page_set: [],
                startRecord: (self.page - 1) * self.perPage + 1,
                endRecord: Math.min(totalRecords, self.page * self.perPage)
            };

            if (self.page > 1) {
                info.prev = self.page - 1;
            }

            if (self.page < info.totalPages) {
                info.next = self.page + 1;
            }

            info.pageSet = self.setPagination(info);
            self.information = info;

            return info;
	},

	setPagination: function (info) {
            var pages = [];

            // How many adjacent pages should be shown on each side?
            var ADJACENT = 3;
            var ADJACENTx2 = ADJACENT*2;
            var LASTPAGE = Math.ceil(info.totalRecords/info.perPage);
            var LPM1 = -1;			

            if (LASTPAGE > 1) {
                //not enough pages to bother breaking it up
                if (LASTPAGE < (7 + ADJACENTx2)) {
                    for (var i=1,l=LASTPAGE; i <= l; i++) {
                        pages.push(i);
                    }
                }
                // enough pages to hide some
                else if (LASTPAGE > (5 + ADJACENTx2)) {
                    //close to beginning; only hide later pages
                    if (info.page < (1 + ADJACENTx2)) {
                        for (var i=1, l=4+ADJACENTx2; i < l; i++) {
                            pages.push(i);
                        }
                    }
                    //in middle; hide some front and some back
                    else if(LASTPAGE - ADJACENTx2 > info.page && info.page > ADJACENTx2) {
                        for (var i = info.page - ADJACENT; i <= info.page + ADJACENT; i++) {
                            pages.push(i);	
                        }
                    }
                    //close to end; only hide early pages
                    else{
                        for (var i = LASTPAGE - (2 + ADJACENTx2); i <= LASTPAGE; i++) {
                            pages.push(i);
                        }
                    }
                }
            } 
            return pages;
        }
    };
})(App.mixins);

Большая часть кода не нуждается в комментариях, поэтому я не буду разбирать каждую строчку. Вы можете увидеть в коде некоторые методы, setSort и _sort, которые пока не используются, но я расскаже о них в другом посте.

Первые четыре метода переключают состояние постраничной навигации. Каждый из них вызывает метод pager.

Метод pager создает копию изначальной коллекции. Затем мы передаем start и stop в качестве параметров функции Array.slice, результат которой помещаем в колекцию с помощью метода reset. При этом сработает событие reset коллекции и все представления, которые отслеживают это событие, будут обновлены.

Последние два метода — info и setPagination. Метод info возвращает объект, содержащий информацию о постраничной навигации, которая может использоваться представлениями, к примеру, нашим представлением постраничной навигации. Метод setPafination вызывается в методе info и здесь я должен сказать две вещи. Во-первых, я более или менее позаимствовал код из PHP-скрипта постраничной навигации и не могу найти его снова, иначе привел бы. Во-вторых, цель метода setPagination в том, чтобы позволить использовать нестандартную структуру постраничной навигации. Он не является чистым и я должен бы сделать его более очевидным. Буду раз любым предложениям.

Итак, что делает метод setPagination? Он возвращает массив, соответствующий страницам, которые вы хотите видеть в навигации — это станет понятнее, когда вы увидите соответствующее представление, но позвольте мне попытаться объяснить. В коде, который представлен выше, если мы находимся на 8-ой странице, постраничная навигация будет выглядеть следующим образом: 5 6 7 8 9 10 11, но вы не ограничены этой структурой.

В проекте, куда я буду внедрять этот код, будет использоваться следующая структура: 1-5 6 7 8 9 10 11-15.

Надеюсь, понятно объяснил.

Далее показано представление постраничной навигации и индексная страница. Финальняа структура постраничной навигации будет авглядеть так: «First Previous 5 6 7 8 9 10 11 Next Last Show 20 | 50 | 100 141 — 160 of 28091 shown». Опять же, код не нуждается в пояснениях, название каждого метод а соответствует тому, что он делает.


(function (views) {
    views.Pagination = Backbone.View.extend({
        events: {
            'click a.first'        : 'gotoFirst',
            'click a.prev'         : 'gotoPrev',
            'click a.next'         : 'gotoNext',
            'click a.last'         : 'gotoLast',
            'click a.page'         : 'gotoPage',
            'click .howmany a'     : 'changeCount'
        },
        tagName: 'aside',
        initialize: function () {
            _.bindAll (this, 'render');

            var self = this;
            self.tmpl = _.template($('#tmpPagination').html());
            self.collection.bind('reset', this.render);
            $(self.el).appendTo('body');
        },
        render: function () {
            var self;
            self = this;
            var html = this.tmpl(self.collection.info());
            $(this.el).html(html);
        },

        gotoFirst: function (e) {
            e.preventDefault();
            var self = this;
            self.collection.goTo(1);
        },		

        gotoPrev: function (e) {
            e.preventDefault();
            var self = this;
            self.collection.previousPage();
        },		

        gotoNext: function (e) {
            e.preventDefault();
            var self = this;
            self.collection.nextPage();
        },	

        gotoLast: function (e) {
            e.preventDefault();
            var self = this;
            self.collection.goTo(self.collection.information.lastPage);
        },

        gotoPage: function (e) {
            e.preventDefault();
            var self = this;
            var page = $(e.target).text();
            self.collection.goTo(page);
        },		

        changeCount: function (e) {
            e.preventDefault();
            var self = this;
            var per = $(e.target).text();
            self.collection.howManyPer(per);
        }
    });
})(App.views);

Я использовал шаблон underscore.js. Стоит обратить внимание на цикл _.each. Это как раз то место, где выводятся номера страниц, например, «5 6 7 8 9 10 11».


<!DOCTYPE HTML>
<html lang="en-US">
    <head>
        <meta charset="UTF-8">
        <title></title>
        <script src="jquery-1.7.1.min.js"></script>
        <script src="underscore.js"></script>
        <script src="json2.js"></script>
        <script src="backbone.js"></script>
        <script type="text/javascript">
            var App = {
                collections: {},
                models: {},
                views: {},
                mixins: {},
                init: function() {
                    var collection = new App.collections.Tags();
                    App.views.tags = new App.views.Tags({collection:collection});
                    new App.views.Pagination({collection:collection});
                }
            };
        </script>
        <script src="assets/app/mixins/pagination.js"></script>
        <script src="assets/app/models/tag.js"></script>
        <script src="assets/app/collections/tags.js"></script>
        <script src="assets/app/views/tags.js"></script>
        <script src="assets/app/views/pagination.js"></script>
        <script type="text/javascript">
            $(App.init);
	</script>
    </head>
    <body>
        <script type="text/html" id="tmpPagination">
            <span class="cell last pages">
                <% if (page != 1) { %>
		    <a href="#" class="first">First</a>
                    <a href="#" class="prev">Previous</a>
                <% } %>
                <% _.each (pageSet, function (p) { %>
                    <% if (page == p) { %>
			<span class="page selected"><%= p %></span>
                    <% } else { %>
			<a href="#" class="page"><%= p %></a>
                    <% } %>
                <% }); %>
                <% if (lastPage != page) { %>
                    <a href="#" class="next">Next</a>
                    <a href="#" class="last">Last</a>
                <% } %>
        </span>			

        <span class="cell howmany">
	    Show
	    <a href="#" class="selected">20</a>
	    |
            <a href="#" class="">50</a>
            |
            <a href="#" class="">100</a>
        </span>		

        <span class="cell first records">
            <span class="current"><%= startRecord %></span>
	    -
            <span class="perpage"><%= endRecord %></span>
            of
            <span class="total"><%= totalRecords %></span>
            shown
            </span>
        </script>
    </body>
</html>

И наконец, представление, отображающее список тегов:


(function (views) {
    views.Tags = Backbone.View.extend({
        tagName: 'ul',
        initialize: function () {
            _.bindAll (this, 'render', 'addAll', 'addOne');
            var self = this;
            self.collection.fetch({
                success: function () {
                    self.collection.pager();
                },
                silent:true
            });
            self.collection.bind('reset', self.addAll);
            $(self.el).appendTo('body');
        },


        addAll: function () {
            var self = this;
            $(self.el).empty();
            self.collection.each (self.addOne);
        },


        addOne: function (model) {
            var self = this;
            var view = new Tag({model:model});
            view.render();
            $(self.el).append(view.el);
        }
    });

    var Tag = Backbone.View.extend({
        tagName: 'li',
        render: function () {
            $(this.el).html(this.model.get('name'));
        }
    });
})(App.views);

Заключение

Как и во всем остальном, есть сотни способ реализовать постраничну навигацию, и я показал только один. В моем примере все данные изначально загружены в коллекцию, но я уверен, что его можно переписать так, чтобы запрашивать с сервера только част данных по мере необходимости. Что бы вы сделали по-другому? Есть ли что-то неправильное в моей реализации?
Рассылка
Подпишитесь на рассылку и получайте дайджест новостей и статей.
Никакого спама!
Подписаться