На многих новых сайтах всё чаще встречается вывод списка новостей или других сущностей в виде бесконечно подгружающейся ленты. На некоторых сайтах подгрузка выполняется автоматически (на twitter.com или vk.com), на других – вручную, то есть в конце списка вместо стандартного переключателя страниц имеется кнопка «Показать ещё». Освежим в памяти работу с ClistView и попробуем реализовать подобный функционал на своём сайте.

Довольно часто такую бесконечную «стену» делают где ни попадя, не задумываясь, нужна она или нет. Но мы не будем касаться здесь этической стороны.

Итак, нам нужно подгружать записи используя Ajax. Сначала отвлечёмся на разбор работы Ajax обновлений стандартного списка ClistView.

Вывод списка записей с ClistView

В самом простом случае с использованием встроенных средств Yii Frmework мы можем выводить список постов с Ajax переходами по страницам так:

Контроллер controllers/PostController.php:

class PostController extends Controller
{
    public function actionIndex()
    {
        $criteria = new CDbCriteria;
        $criteria->order = 't.create_datetime DESC';
 
        $dataProvider = new CActiveDataProvider('Post', array(
            'criteria'=>$criteria,
            'pagination'=>array(
                'pageSize'=>10,
            ),
        ));
 
        $this->render('index', array(
            'dataProvider'=>$dataProvider,
        ));            
    }
}

Представление views/post/index.php:

<?php
$this->pageTitle = 'Блог';
$this->breadcrumbs = array(
    'Блог',
);
?>
<h1>Блог</h1>
<?php $this->widget('zii.widgets.CListView', array(
    'dataProvider'=>$dataProvider,
    'itemView'=>'_view',
    'ajaxUpdate'=>true,
    'template'=>"{items}\n{pager}",
)); 
?>

Элемент списка views/post/_view.php:

<?php
<article>
<h2><?php echo Chtml::link($data->getUrl(), $data->title); ?></h2>
<?php echo $data->short; ?>
</article>

Здесь у нас всего одно действие actionIndex() и одно представление index.php для него. Но в блоге могут быть вывод записей из категории, записей по тегу, по дате и т.д. Все они будут использовать свои шаблоны index.phpcategory.phptag.phpdate.php, но одинаковый общий список. Целесообразно по принципу шаблонов WordPress вынести формирование списка в отдельный файл _loop.php.

Все шаблоны типа index.php теперь ссылаются на файл списка _loop.php:

views/post/index.php:

<?php
$this->pageTitle = 'Блог';
$this->breadcrumbs = array(
    'Блог',
);
?>
<h1>Блог</h1>
<?php $this->renderPartial('_loop', array('dataProvider'=>$dataProvider)); ?>

views/post/_loop.php:

<?php $this->widget('zii.widgets.CListView', array(
    'dataProvider'=>$dataProvider,
    'itemView'=>'_view',
    'ajaxUpdate'=>true,
    'template'=>"{items}\n{pager}",
)); ?>

Теперь все наши списки нормально выводятся и переход по страницам происходит по Ajax. Но в этом простейшем способе кроются некоторые подводные камни.

Снижение нагрузки при обновлении по ajax

Ajax запрос обращается по ссылкам, которые прописаны в ячейках списка номеров страниц, то есть фактически к тому же actionIndex(), который независимо от способа запроса рендерит полную страницу сайта в строке $this->render('index', array(...)). Обработчик Ajax ответа только выделяет из всего HTML-содержимого код своего списка. Эта тема уже поднималась на Habrahabr. Теперь мы можем решить эту проблему.

При Ajax запросе наш контроллер должен возвращать не всю страницу в шаблоне, а только код списка. Так как мы вынесли список в отдельный файл _loop.php, ничто не мешает генерировать только его вызывая $this->renderPartial('_loop', array(...)):

class PostController extends Controller
{
    public function actionIndex()
    {
        $criteria = new CDbCriteria;
        $criteria->order = 't.create_datetime DESC';
 
        $dataProvider = new CActiveDataProvider('Post', array(
            'criteria'=>$criteria,
            'pagination'=>array(
                'pageSize'=>10,
            ),
        ));
 
        if (Yii::app()->request->isAjaxRequest){
            $this->renderPartial('_loop', array(
                'dataProvider'=>$dataProvider,
            ));
            Yii::app()->end();
        } else {
            $this->render('index', array(
                'dataProvider'=>$dataProvider,
            ));
        }
    }
}

Теперь при Ajax запросе будет возвращаться только список без генерации всего шаблона сайта. Этот способ подойдёт и для CGridView, где из файла admin.php таблицу можно вынести в файл_grid.php.

Добавляем подгружающуюся ленту

Действия нашего контроллера уже способны оптимизировать свою работу при Ajax запросах. Рассмотрим теперь непосредственно организацию ленты. Нам нужно:

  • Создать на странице блок <div id="listView"></div>;
  • В блоке вывести стандартный список первых 10 новостей со навигацией по страницам;
  • Если включен JavaScript и страниц больше одной, то скрыть навигатор и отобразить кнопку «Показать ещё»;
  • Запомнить в JavaScript номер текущей страницы
  • Навесить на эту кнопку обработчик, который бы по щелчку увеличивал номер текущей страницы на единицу и загружал новую порцию записей и добавлял в блок #listView;
  • Если записи закончились (page>=pageCount), то скрыть кнопку.

Если JavaScript у пользователя отключен, то наш скрипт не сработает и останется стандартный навигатор.

Также нам нужно добавить индикацию процесса загрузки (можно, например, показывать/скрывать анимированное изображение или дабавлять/удалять класс-примесь кнопке) и защиту от частых нажатий (выставлять флаг на всё время згрузки).

Представление views/post/_loop.php с учётом этого может быть примерно таким:

<div id="listView">
 
    <?php $this->widget('zii.widgets.CListView', array(
        'dataProvider'=>$dataProvider,
        'itemView'=>'_view',
        'ajaxUpdate'=>false,
        'template'=>"{items}\n{pager}",        
        'pager'=>array(
            'htmlOptions'=>array(
                'class'=>'paginator'
            )
        ),
    )); ?>
 
</div>
 
<?php if ($dataProvider->totalItemCount > $dataProvider->pagination->pageSize): ?>
 
    <p id="loading" style="display:none"><img src="<?php echo Yii::app()->request->baseUrl; ?>/images/loading.gif" alt="" /></p>
    <p id="showMore">Показать ещё</p>
 
    <script type="text/javascript">
    /*<![CDATA[*/
        (function($)
        {
            // скрываем стандартный навигатор
            $('.paginator').hide();
 
            // запоминаем текущую страницу и их максимальное количество
            var page = parseInt('<?php echo (int)Yii::app()->request->getParam('page', 1); ?>');
            var pageCount = parseInt('<?php echo (int)$dataProvider->pagination->pageCount; ?>');
 
            var loadingFlag = false;
 
            $('#showMore').click(function()
            {
                // защита от повторных нажатий
                if (!loadingFlag)
                {
                    // выставляем блокировку
                    loadingFlag = true;
 
                    // отображаем анимацию загрузки
                    $('#loading').show();
 
                    $.ajax({
                        type: 'post',
                        url: window.location.href,
                        data: {
                            // передаём номер нужной страницы методом POST
                            'page': page + 1,
                            '<?php echo Yii::app()->request->csrfTokenName; ?>': '<?php echo Yii::app()->request->csrfToken; ?>'
                        },
                        success: function(data)
                        {
                            // увеличиваем номер текущей страницы и снимаем блокировку
                            page++;                            
                            loadingFlag = false;                            
 
                            // прячем анимацию загрузки
                            $('#loading').hide();
 
                            // вставляем полученные записи после имеющихся в наш блок
                            $('#listView').append(data);
 
                            // если достигли максимальной страницы, то прячем кнопку
                            if (page >= pageCount)
                                $('#showMore').hide();
                        }
                    });
                }
                return false;
            })
        })(jQuery);
    /*]]>*/
    </script>
 
<?php endif; ?>

Здесь мы отключили за ненадобностью встроенную поддержку ajax, так как будем делать переходы в своём скрипте.

Это представление уже не надо возвращать по Ajax. Оно должно выводиться единожды и подгружать только голый список. Для этого добавим представление loopAjax.php с кодом подгружаемого списка без прочих лишних элементов:

views/post/_loopAjax.php

<?php $this->widget('zii.widgets.CListView', array(
    'dataProvider'=>$dataProvider,
    'itemView'=>'_view',
    'ajaxUpdate'=>false,
    'template' => "{items}",
)); ?>

Кроме переименования _loop в _loopAjax в контроллере необходимо произвести ещё несколько изменений. Все они касаются передачи номера необходимой страницы контроллеру.

Можно заметить, что номер страницы из нашего скрипта передаётся посредством переменнойpage в POST запросе (а не в GET), а в классе CPagination, используемом CActiveDataProvider, для определения номера текущей страницы используется именно GET.

В зависимости от используемых роутов при различных построениях ЧПУ номер страницы в URL может находиться в любом месте, например

http://site.com/blog?page=2
http://site.com/blog/page-2
http://site.com/page/2
http://site.com/blog/2
http://site.com/page-2

Наш JavaScript код не может воспользоваться функцией Yii::app()->createUrl() для генерации ссылок, поэтому номер необходимой страницы проще передать в POST запросе, отправленном на текущий адрес window.location.href и в контроллере произвести подмену$_GET['page']=$_POST['page'].

Кроме того, нужно явно указать имя параметра page в поле pageVar, иначе по умолчанию он будет использовать параметр по имени нашей модели, то есть Post_page.

class PostController extends Controller
{
    public function actionIndex()
    {        
        $this->processPageRequest('page');
 
        $criteria = new CDbCriteria;
        $criteria->order = 't.create_datetime DESC';
 
        $dataProvider = new CActiveDataProvider('Post', array(
            'criteria'=>$criteria,
            'pagination'=>array(
                'pageSize'=>10,
                'pageVar' =>'page',
            ),
        ));
 
        if (Yii::app()->request->isAjaxRequest){
            $this->renderPartial('_loopAjax', array(
                'dataProvider'=>$dataProvider,
            ));            
            Yii::app()->end();
        } else {
            $this->render('index', array(
                'dataProvider'=>$dataProvider,
            ));
        }
    }    
 
    protected function processPageRequest($param='page')
    {
        if (Yii::app()->request->isAjaxRequest && isset($_POST[$param]))
            $_GET[$param] = Yii::app()->request->getPost($param);
    }
}

Подмену значения $_GET['page'] мы производим в методе processPageRequest(). Его можно при желании либо поднять в базовый контроллер, либо (чтобы избавиться от лишней строки$this->processPageRequest('page');) вынести в отдельный фильтр.

После всех этих манипуляций мы получим «бесконечную» ленту, подгружающую записи и становящуюся всё длиннее и длиннее по щелчку мыщи. Для автоматической загрузки новых записей при прокрутке страницы нужно обработчик щелчка $('#showMore').click() заменить на обработчик прокрутки страницы $('html').scroll() с проверкой на появление нашего блока#showMore в видимой части окна.

Для большего удобства можно вынести код кнопки и обработчика в отдельный настраиваемый виджет, которому бы передавались, например, $dataProvider и CSS-идентификатор стандартного навигатора по страницам.

Взято отсюда

Комментарии

comments powered by Disqus