Personal Maps: главная страница и структура клиентской части приложения. Часть 3.

Владимир | | Ajax, AngularJS, HTML, JavaScript, PHP, Web разработка, Yii.

В этой части мы начнём разрабатывать клиентскую часть приложения.

Фактически это будет одностраничное приложение, которое получает и передаёт данные с помощью REST сервисов. Основное преимущество такого подхода заключается в том, что при работе пользователя не происходит перезагрузки страницы. Если сервер окажется недоступен, пользователь увидит сообщение об ошибке и сможет продолжить работу, когда связь восстановится.

Кроме того, наше приложение показывает объекты на карте Google, и если его разрабатывать в «классическом» стиле, то переход от одного объекта к другому будет вызывать перезагрузку карты. А это, не смотря на кэширование компонентов карты браузером, негативно скажется на скорости работы интерфейса.

Структура главной страницы приложения

main_page_structure

Как видите, страница собирается из нескольких частей.

Основой является шаблон views/layouts/yiistrap.php (весь файл вы можете посмотреть на GitHub, в тексте я буду приводить только фрагменты). Фактически это немного изменённый шаблон HTML5 Boilerplate.

В нашем приложении он используется в качестве основы для всех страниц и содержит верхнее навигационное меню и футер. Меню формируется с помощью стандартного виджета zii.widgets.CMenu.

<header class="navbar navbar-fixed-top navbar-inverse">
        <div class="navbar-inner">
            <div class="container">
            <span class="brand"><?php echo CHtml::encode(Yii::app()->name); ?></span>
            <?php $this->widget('zii.widgets.CMenu',array(
                'items'=>array(
                    array('label'=>Yii::t('app', 'PLACES'), 'url'=>array('/places/index'), 'visible'=>!Yii::app()->user->isGuest),
                    array('label'=>Yii::t('app', 'USERS'), 'url'=>array('/users/index'), 'visible'=>Yii::app()->user->checkAccess('admin')),
                    array('label'=>Yii::t('app', 'ABOUT'), 'url'=>array('/site/page', 'view'=>'about')),
                    array('label'=>Yii::t('app', 'CONTACT'), 'url'=>array('/site/contact')),
                    array('label'=>Yii::t('app', 'LOGIN'), 'url'=>array('/site/login'), 'visible'=>Yii::app()->user->isGuest),
                    array('label'=>Yii::t('app', 'LOGOUT').' ('.Yii::app()->user->name.')', 'url'=>array('/site/logout'), 'visible'=>!Yii::app()->user->isGuest)
                ),
                'activeCssClass'=>'active',
                'htmlOptions'=>array(
                    'class'=>'nav',
                ),
            )); ?>
            </div>
        </div>
</header>

Также обратите внимание на тег base в заголовке страницы

<base href="<?php echo Yii::app()->createAbsoluteUrl('/'); ?>/">

Все URL, указанные в AJAX запросах приложения, относительные. И значение тега base позволяет правильно сформировать URL независимо от того установлено приложение в корне сервера или какой-нибудь папке.

Для того, чтобы Yii использовал данный шаблон его по-умолчанию, нужно указать его размещение в классе Controller protected/components/Controller.php

public $layout='//layouts/yiistrap';

Центральную часть страницы формирует представление views/places/index.php.

Для того, чтобы его загрузить, создадим контроллер protected/controllers/PlacesController.php

class PlacesController extends RestController
{
    public function actionIndex() {
        if (Yii::app()->user->checkAccess('user')) {
            $this->render('index');
        }
        else {
            $this->redirect(array('site/login'));
        }
    }
    …
}

Когда пользователь открывает страницу /places/index, Yii вызывает метод actionIndex данного контроллера. В этом методе мы выполняем проверку прав пользователя (управление доступом рассмотрим в одной из следующих частей) и если она прошла успешно, отображаем представление, которое выглядит следующим образом:

<?php
Yii::app()->clientScript->registerScriptFile('//maps.googleapis.com/maps/api/js?sensor=false', CClientScript::POS_END);
Yii::app()->clientScript->registerScriptFile(Yii::app()->baseUrl.'/js/markdown.js', CClientScript::POS_END);
Yii::app()->clientScript->registerScriptFile('//ajax.googleapis.com/ajax/libs/angularjs/1.0.7/angular.min.js', CClientScript::POS_END);
Yii::app()->clientScript->registerScriptFile(Yii::app()->baseUrl.'/js/ui-bootstrap-tpls-0.4.0.min.js', CClientScript::POS_END);
Yii::app()->clientScript->registerScriptFile(Yii::app()->baseUrl.'/js/angular-translate.min.js', CClientScript::POS_END);
Yii::app()->clientScript->registerScript(
    'langScript'
    , '
    var lang = "'.Yii::app()->getLanguage().'";
    var translations = '.CJSON::encode(Yii::app()->messages->getAllMessages('frontend', Yii::app()->getLanguage())).';'
    , CClientScript::POS_HEAD
);
Yii::app()->clientScript->registerScriptFile(Yii::app()->baseUrl.'/js/app.js', CClientScript::POS_END);
Yii::app()->clientScript->registerScriptFile(Yii::app()->baseUrl.'/js/services/places.js', CClientScript::POS_END);
Yii::app()->clientScript->registerScriptFile(Yii::app()->baseUrl.'/js/controllers/list.js', CClientScript::POS_END);
Yii::app()->clientScript->registerScriptFile(Yii::app()->baseUrl.'/js/controllers/form.js', CClientScript::POS_END);
Yii::app()->clientScript->registerScriptFile(Yii::app()->baseUrl.'/js/directives/pm-google-map.js', CClientScript::POS_END);
Yii::app()->clientScript->registerScript(
    'requiredScript'
    , 'angular.bootstrap(document, ["personalmaps"]);'
    , CClientScript::POS_END
);
?>
<div class="row-fluid">
    <div class="span9 map" pm-google-map>
    </div>
    <div class="span3" ng-view></div>
</div>

Здесь, с помощью метода registerScriptFile мы загружаем все необходимые JS библиотеки и затем инициализируем приложение AngularJS (строки 19-23).

Теперь обратите внимание на разметку и сравните её со схемой размещения компонентов в начале статьи.

Одним из преимуществ AngularJS является то, что просто глядя на разметку вы можете определить, где какой компонент используется. В данном случае на странице размещены два блока, для первого установлен атрибут pm-google-map, т.е. к данному блоку Angular автоматически подключит директиву pmGoogleMap.

Для второго блока установлен атрибут ng-view, т.е. используется стандартная директива ngView. Эта директива связана с маршрутизатором (router) приложения и при изменении URL автоматически рендерит в данном блоке заданный шаблон. Т.е. с помощью этой директивы осуществляет переключение между списком объектов (шаблон partials/list.html) и формой редактирования объекта (partials/form.html).

На данном этапе у нас всё готово для того, чтобы перейти непосредственно к разработке клиентской (JavaScript) части приложения.

Обратите внимание, что мы будем разрабатывать клиентскую и серверную часть независимо друг от друга. Сначала создадим клиентский код, протестируем его. Затем напишем REST сервисы.

Структура клиентской части

Прежде всего, рассмотрим компоненты, которые входят в приложение. Их всего четыре:

  • 2 контроллера (для формы редактирования объекта и списка объектов);
  • директива (для подключения карты);
  • сервис (предназначен для выполнения операций с объектами – сохранение, удаление, редактирование и т.д.).

Инициализация приложения и настройка маршрутизатора выполняется в файле app.js. Также в состав приложения входят два html шаблона (form.html, list.html).

Размещение файлов следующее:

js/
	controllers/
		form.js
		list.js
	directives/
		pm-google-map.js
	services/
		places.js
	app.js
partials/
		form.html
		list.html

Как видите, скрипты сгруппированы по их типу – контроллеры, директивы, сервисы и шаблоны. На мой взгляд, это удобный вариант организации, но он не единственный. Например, если приложение очень небольшое, можно весь код разместить в одном файле. Или, наоборот, большие приложение имеет смысл разбить на модули (если это возможно), каждый из которых будет содержать свои сервисы, директивы и контроллеры.

Рассмотрим взаимодействием этих компонентов между собой

application_components

При разработке архитектуры очень важно правильно разбить приложение на компоненты и сделать их максимально независимыми.

Т.е. нужно избегать создания прямых связей между компонентами. Под связью подразумевается любое использование одного компонента другим компонентом. Каждая такая связь с высокой вероятностью означает, что любые изменения в компоненте потребуют соответствующих изменений во всех связанных компонентах.

Решается эта задача с помощью дополнительных объектов, которые изолируют части приложения друг от друга. В нашем приложении таких объектов два: сервис (Places), который выполняет всю работу по синхронизации данных с серверной частью и встроенный объект AngularJS $rootScope, который используется в качестве диспетчера событий (этот объект автоматически создаётся при инициализации приложения).

Компоненты приложения (в нашем случае – контроллеры и дерективы) «знают» только о сервисе и «не знают» друг о друге. Т.е. они могут вызывать методы сервиса, но не могут вызывать методы друг друга.

Аналогично работают и события. Компоненты «подписываются» на события объекта $rootScope. При этом работоспособность компонента в целом не зависит от того, возникло конкретное событие или нет. Если событие возникло, то будет вызван обработчик, если не возникало, то не будет. Если компонент инициирует событие, то не важно, «подписался» кто-то это событие или нет. Такой подход даёт возможность добавлять, удалять и изменять компоненты не рискуя, что эти изменения вызовут каскад ошибок в остальных компонентах.

Примечание. В данном приложении мы используем $rootScope, это scope «верхнего уровня», который доступен всем компонентам приложения. Т.к. наше приложение небольшое, использование $rootScope в качестве диспетчера событий вполне оправданно. Если приложение состоит из нескольких модулей, то вы можете использовать объекты $scope, которые AngularJS создает для каждого контроллера. В результате у вас каждый модуль будет работать со своим диспетчером событий. В документации есть хороший пример, демонстрирующий разные методы инициализации событий.

В качестве примера рассмотрим процесс получения и отображения списка созданных пользователем объектов.

При инициализации приложения сервис отправляет AJAX-запрос через REST API, а контроллеры и директива «подписываются» на событие places:updated.

Когда приходит ответ от сервера, сервис инициирует событие places:updated. В результате вызываются обработчики компонентов, которые «подписались» на это событие. Т.е. фактически компоненты получают сообщение об обновлении списка пользовательских объектов. Если компонент должен каким-то образом отреагировать, он вызывает метод сервиса getAll и получает обновлённый массив с данными.

Таким образом, сервис ничего не должен знать о том какие компоненты получают от него данные, он только предоставляет API для работы с ними и отправляет соответствующие события. А компоненты знают только о сервисе и событиях, но не друг о друге.

В следующей части мы подробно рассмотрим создания сервиса, а также его тестирование.

Если есть вопросы или замечания, пишите в комментариях.

Содержание