Содержание


MEAN-программирование

объединение MEAN с Meetup.com и микроданными

Comments

Серия контента:

Этот контент является частью # из серии # статей: MEAN-программирование

Следите за выходом новых статей этой серии.

Этот контент является частью серии:MEAN-программирование

Следите за выходом новых статей этой серии.

До сих пор в этой серии статей мы занимались настройкой MEAN-приложения User Group List and Information (UGLI) в автономном режиме. Теперь пора посмотреть, как заставить веб-сайт взаимодействовать с другими сайтами. В предыдущей статье приложение работало с локальными данными. Теперь мы интегрируем свое MEAN-приложение с API Meetup.com, чтобы отображать информацию о предстоящих конференциях. Данные JSON поступают от Meetup, но за то, как выглядит и ведет себя страница, отвечаете вы. Вы также научитесь подмешивать к ней микроданные, чтобы сделать веб-страницу более дружелюбной по отношению к поисковым машинам. Полный пример кода приведен в разделе Загрузки.

Для начала выведем на главную страницу UGLI сведения о предстоящих мероприятиях из Meetup.com.

HTML и микроданные

Если вы зайдете в Денверскую группу пользователей HTML5 на Meetup.com, то увидите веб-страницу приветствия, подобную той, что представлена на рисунке 1.

Рисунок 1. Информация о предстоящей конференции Денверской группы пользователей HTML5 на Meetup.com
Информация о предстоящей конференции Денверской группы пользователей HTML5 на Meetup.com
Информация о предстоящей конференции Денверской группы пользователей HTML5 на Meetup.com

Как видите, на Meetup.com есть все исходные сведения, необходимые для дальнейшей настройки главной страницы UGLI для отображения информации о предстоящих мероприятиях — название предстоящей конференции, время и место ее проведения и т.п., — но дизайн страницы Meetup.com тесно связан с датой. Информация, которую нам нужно извлечь и использовать в своем MEAN-приложении, нашпигована HTML-элементами, как показано в листинге 1. (Я отредактировал HTML-код для наглядности и краткости.)

Листинг 1. Исходный HTML-код на Meetup.com
<ul>
    <li itemscope="" itemtype="http://data-vocabulary.org/Event">

        <span itemprop="eventType" 
              style="display:none;">Meetup</span>
        <h3>
            <a href="http://www.meetup.com/HTML5-Denver-Users-Group/events/160326502/" itemprop="url">
                <span itemprop="summary">"Developing Offline Applications" and "HTML 5 Animations"</span>
            </a>
        </h3>

        <!-- snip -->
    </li>
</ul>

Название мероприятия —"Developing Offline Applications" and "HTML 5 Animations"— вложено в HTML на глубину нескольких уровней. С точки зрения HTML-документа (и веб-браузера) это произвольная строка символов – просто один из нескольких элементов списка (<li>), вложенных в неупорядоченный список (<ul>). Этот элемент списка имеет заголовок третьего уровня (<h3>) и предположительно заголовки второго и первого уровня, определенные ранее где-то в иерархии документа. Внутри заголовка есть гиперссылка (<a href>), которая, в свою очередь, содержит произвольный текст в элементе <span>.

Вся эта HTML-разметка необходима для отображения информации в соответствии с предпочтениями Meetup.com. Наша задача – отобразить информацию в своем приложении совершенно иначе: для этого нужно найти способ отделить данные от изображения.

В листинге 1 имеются два разных уровня семантики. Основной уровень – это то, о чем я только что говорил: элемент списка и гиперссылка. Это семантика документа. Другой уровень – это семантика мероприятия, обозначенная такими ключевыми словами, как eventType, url и summary. Эти ключевые слова не имеют ничего общего со способом представления документа. Они указывают поисковым системам (и любопытным людям, разглядывающим исходный код HTML) на «высший смысл» информации. Эти атрибуты – часть спецификации HTML Microdata. Подробнее о формате микроданных и предшествующих ему форматах метаданных см. в боковой врезке Микроформаты, микроданные и RDFa .)

Все, что находится внутри этого конкретного элемента списка – информация о мероприятии. Поисковой системе это известно, потому разработчик страницы добавил к элементу списка гиперссылку itemtype="http://data-vocabulary.org/Event". Страница содержит множество разнообразных гиперссылок, но та, в которой есть itemprop="url", — это ссылка на само мероприятие. eventType — это произвольная строка Meetup, — но Meetup.com последовательно использует ее на всех своих веб-страницах. Краткое описание (summary) мероприятия идентифицируется элементом <span> с атрибутом itemprop="summary".

Элемент <span> — это один из немногих HTML-элементов, которые браузеры игнорируют при отрисовке страницы. Текст внутри элемента <b> отображается полужирным шрифтом; текст внутри элемента <h2> отображается более крупным шрифтом, чем текст внутри элемента <h1>; текст внутри элемента <a> кликабелен и обычно окрашен в синий цвет с подчеркиванием. Конечно, все эти стили по умолчанию можно переопределить с помощью CSS. Но тег <span> существует исключительно для того, чтобы добавлять свои собственные стили CSS — или, как во фрагменте HTML-кода Meetup.com, приведенном в листинге 1, чтобы поместить строку "Developing Offline Applications" and "HTML 5 Animations" в семантическую метку itemprop="summary".

Полный список элементов метаданных, которые можно добавить в свою разметку MEAN для дальнейшего описания мероприятия, находится на странице с URL-адресом, который Meetup.com использует для первоначального определения мероприятия: http://data-vocabulary.org/Event.

Размещение заполнителя мероприятия на главной странице

Теперь, когда стало понятно, как Meetup.com отображает сведения о мероприятии, можно сделать то же самое для приложения UGLI.

Введите команду mongod, чтобы запустить MongoDB в корне контрольного приложения, затем наберите grunt, чтобы запустить веб-приложение. Набрав в веб-браузере http://localhost: 3000, вы увидите главную страницу, настроенную при чтении предыдущей статьи (как показано на рисунке 2).

Рисунок 2. Главная страница UGLI
Главная страница UGLI
Главная страница UGLI

Хотелось бы вывести на эту страницу предстоящее мероприятие Денверской группы пользователей HTML5. Этим мы и займемся в остальной части статьи. Для начала добавим некоторые статические данные-заполнители, чтобы очертить структуру внешнего вида страницы.

Откройте в текстовом редакторе файл public/modules/core/views/home.client.view.html. Под заголовком добавьте новую разметку для мероприятия, как показано в листинге 2.

Листинг 2. Файл public/modules/core/views/home.client.view.html
<section data-ng-controller="HomeController">
  <div class="jumbotron text-center">
    <!-- snip -->
  </div>

  <div class="row">
    <div>Monday, September 22, 2014</div>
    <h3><a href="http://www.meetup.com/HTML5-Denver-Users-Group/events/160326502/">
    "Developing Offline Applications" and "HTML 5 Animations"</a></h3>
    <div class="col-md-4">
     <h4>When</h4>
     <p>6pm</p>
     <h4>Where</h4>
     <address>
       <span>Rally Software</span><br>
       1550 Wynkoop<br>     
       Denver, CO<br>
     </address>
    </div>

    <div class="col-md-8">
     <p><b>6 pm : "Developing Offline Applications with HTML 5" by Venkat Subramaniam</b></p> 
     <p><b>7 pm: Dinner and Networking</b></p> 
     <p><b>7:30 pm: "HTML 5 Animations - building true richness on the web" by Venkat Subramaniam</b></p>
    </div>
  </div>
</section>

При просмотре обновленной главной страницы в браузере она должна выглядеть как на рисунке 3.

Рисунок 3. Структура главной страницы UGLI
Структура главной страницы UGLI
Структура главной страницы UGLI

Теперь, когда имеется базовая HTML-структура, можно немного улучшить ее по сравнению с оформлением по умолчанию. Для этого необходимо добавить некоторую семантику.

Bootstrap предоставляет встроенные стили по умолчанию для используемых структурных элементов HTML, таких как <div> и <h3>. Он также предоставляет некоторые дополнительные структурные классы, такие как row и col, которые выходят за рамки HTML-элементов по умолчанию.

Начинающие веб-разработчики часто мыслят категориями структуры веб-страницы, а не отображаемой информации. В результате они пишут специальные CSS-классы с именами типа big-red-italic и left-column-header. С точки зрения синтаксиса в этом подходе нет ничего дурного, но мне, например, при долгосрочной поддержке веб-сайта легче работать с семантически значимыми именами, такими как event-date и event-location. Так что, когда клиент возвращается спустя год и просит сделать все промежуточные итоги зелеными, а не красными, у меня есть CSS-класс, который определяет, что отображается (subtotals), а не как (green-body-text). И у меня меньше шансов нечаянно изменить другие элементы на странице, в которых также случайно используется CSS-правило green-body-text.

Вернемся к HTML-коду, который мы только что написали, и добавим некоторые семантически значимые CSS-классы, такие как event, event-date и event-title:

<div class="row center-block event">
<div class="event-date">Monday, September 22, 2014</div>
<h3 class="event-title"><a href="http://www.meetup.com/HTML5-Denver-Users-Group/events/160326502/">
    "Developing Offline Applications" and "HTML 5 Animations"</a></h3>

Структурный класс center-block взят из библиотеки Bootstrap, о которой говорилось в предыдущей статье. Как вы скоро увидите, при уменьшении ширины (width) класса event до 75% center-block гарантирует равномерное распределение пробелов с левой и с правой стороны.

Это очень удобно при смешивании структурных и семантических классов в одном и том же элементе. В долгосрочной перспективе это помогает быстро определить, какие классы характерны для приложения (event-*), а какие носят общий характер (row, center-block).

Когда в HTML добавлены классы, можно определить некоторые специальные правила CSS. Откройте в текстовом редакторе файл public/modules/core/css/core.css. Так как каждый модуль имеет свои собственные CSS, можно придерживаться этого «компонентно-ориентированного» мышления. И это помогает распределять модули между проектами, захватывая все дерево подкаталогов.

Добавим правила CSS-стилей, как показано в листинге 3.

Листинг 3. Файл public/modules/core/css/core.css
.event {
    width: 75%;
}

.event-date {
    font-style: italic;
}

.event-title {
    margin-top: 0;
}

Теперь главная страница выглядит немного опрятнее и изысканнее, как показано на рисунке 4.

Рисунок 4. Главная страница UGLI с CSS-стилями
Главная страница UGLI с CSS-стилями
Главная страница UGLI с CSS-стилями

Наконец, вернемся еще раз и добавим метаданные в виде микроданных. Нельзя ли было пропустить этот шаг и использовать элементы микроданных в правилах CSS, вместо того, чтобы определять их специально? Конечно, можно. Но я предпочитаю держать их отдельно. Ведь они служат для двух разных целей. Для оформления CSS-стилей и для оптимизации поисковых систем (SEO). Представьте себе, что через пять лет пользователь попросит перенести микроданные на некоторую другую, новую спецификацию. Нехорошо, если это вызовет какой-нибудь побочный эффект, который отрицательно скажется на облике веб-сайта. Две эти функции должны быть полностью ортогональны.

Еще раз откройте в текстовом редакторе файл public/modules/core/views/home.client.view.html. Добавьте новую разметку с семантическими микроданными, как показано в листинге 4. (Хороший пример разметки мероприятия с помощью микроданных приведен в статье Rich snippets - Events.)

Листинг 4. Добавление микроданных
<div class="row center-block event" 
   itemscope 
   itemtype="http://data-vocabulary.org/Event">
  <span itemprop="eventType" 
      style="display:none;">Meetup</span> 
  <time class="event-date" 
      itemprop="startDate" 
      datetime="2014-09-22T18:00-06:00">Monday, September 22, 2014</time>
  <h3 class="event-title"><a href="http://www.meetup.com/HTML5-Denver-Users-Group/events/160326502/" 
    itemprop="url"><span itemprop="summary">"Developing Offline Applications" and 
    "HTML 5 Animations"</span></a></h3>
  <div class="col-md-4">
    <h4>When</h4>
    <p>6pm</p>
    <h4>Where</h4>
    <address itemprop="location" 
         itemscope 
         itemtype="http://data-vocabulary.org/?Organization">
      <span itemprop="name">Rally Software</span><br>
      <span itemprop="address" 
          itemscope 
          itemtype="http://data-vocabulary.org/Address">
        <span itemprop="street-address">1550 Wynkoop</span><br>  
        <span itemprop="locality">Denver</span>, <span itemprop="region">CO</span><br>
      </span>
    </address>
  </div>

  <div class="col-md-8" itemprop="description">
    <p><b>6 pm : "Developing Offline Applications with HTML 5" by Venkat Subramaniam</b></p> 
    <p><b>7 pm: Dinner and Networking</b></p> 
    <p><b>7:30 pm: "HTML 5 Animations - building true richness on the web" by Venkat Subramaniam</b></p>
  </div>
</div>

Не слишком ли большая работа направлена на создание веб-сайта, который в конечном итоге выглядит в браузере точно так же, как перед ее началом? Зато вы будете рады, когда благодаря всем этим семантическим данным, которые вам пришлось добавлять, ваш сайт всплывет в верхнюю часть результатов поиска.

Вместо того чтобы разбирать листинг 4 строка за строкой, я укажу несколько интересных моментов.

Как и в примере с Meetup.com в листинге 1, мы добавили eventType в элемент, который не отображается (потому что предназначен исключительно для SEO):

<span itemprop="eventType" 
      style="display:none;">Meetup</span>

Далее, мы добавили дату мероприятия – для восприятия как людьми, так и машинами:

<time class="event-date" 
      itemprop="startDate" 
      datetime="2014-09-22T18:00-06:00">Monday, September 22, 2014</time>

Человек мгновенно поймет, что строка Monday, September 22, 2014 – это дата. Он и 9/22/2014, и 2014-09-22 воспримет как все ту же дату. Компьютер же воспринимает все буквально, и подобные мелкие изменения форматирования приведут впоследствии к большим проблемам. В этом примере мы внесли несколько изменений для устранения неоднозначности ситуации:

  • заменили универсальный HTML-элемент <div> более конкретным <time> (новый элемент HTML5);
  • CSS-класс event-date определяет содержание данных, а не внешний вид;
  • атрибут микроданных itemprop="startDate" определяет эту дату как дату начала (startDate) мероприятия (event);
  • атрибут datetime— часть элемента HTML5 <time>— однозначно указывает время в формате ISO 8601. Таким образом, можно предложить время как для восприятия машиной, так и в опрятном формате для отображения и восприятия человеком.

Самых больших изменений в HTML требует адрес мероприятия, как показано в листинге 5. Мы вложили в схему Event несколько новых схем —Organization и Address.

Листинг 5. Добавление микроданных для определения организации и адреса
<h4>Where</h4>
<address itemprop="location" 
         itemscope 
         itemtype="http://data-vocabulary.org/?Organization">
    <span itemprop="name">Rally Software</span><br>
    <span itemprop="address" 
          itemscope 
          itemtype="http://data-vocabulary.org/Address">
        <span itemprop="street-address">1550 Wynkoop</span><br>  
        <span itemprop="locality">Denver</span>, <span itemprop="region">CO</span><br>
    </span>
</address>

Как сделать браузер поддерживающим микроданные

Теперь, когда мы добавили к своему веб-сайту кое-какие микроданные, как убедиться, что мы сделали это правильно? Самый простой подход – использовать расширение для поддержки микроданных в браузере. Существует целый ряд таких расширений, но при работе с Chrome мне нравится Semantic inspector.

После установки это расширение для браузера обычно остается в тени до тех пор, пока вы не наткнетесь на веб-сайт с микроданными. Когда Semantic inspector находит микроданные на текущей веб-странице, он отображает красный значок m в адресной строке. Вас удивит, как часто всплывает этот значок; вы увидите его на многих популярных сайтах, включая Google, Time.com, Walmart.com и многие другие. Щелкните на этом значке, чтобы увидеть детали, как показано на рисунке 5.

Рисунок 5. Просмотр микроданных с помощью Semantic inspector
Просмотр микроданных с помощью Semantic inspector
Просмотр микроданных с помощью Semantic inspector

Теперь, когда у нас есть основной HTML-каркас, аннотированный атрибутами микроданных, пришло время заполнить его свежими данными JSON из сети. Для этого создадим новый модуль events, содержащий контроллер, представление, модель и сервис.

Создание AngularJS-модуля

В предыдущей статье мы использовали Yeoman для создания полноценного модуля CRUD, включая маршруты Express и модель Mongoose. На этот раз нам не требуется серверная инфраструктура, потому что исходные данные JSON поступают из внешнего веб-приложения. К счастью, авторы генератора MeanJS Yeoman предвидели это требование и предлагают еще один генератор – AngularJS, исключительно для клиентской части приложения.

Введите yo meanjs:angular-module events, чтобы создать новый AngularJS-модуль с именем events. AngularJS-модуль представляет собой логически сгруппированные файлы, относящиеся к данным определенного типа. Как сказано в официальном документе AngularJS, «Модуль можно считать контейнером для различных частей приложения — контроллеров, сервисов, фильтров, директив и т.д.».

В ответ на запрос выберите все элементы списка, как показано в листинге 6.

Листинг 6. Создание модуля
[?] Which folders would you like your module to include? 
 ? config
 ? controllers
 ? css
 ? directives
 ? filters
 ? img
 ? services
 ? tests
 ? views

 create public/modules/events/events.client.module.js

Пока наш модуль – это просто набор пустых каталогов. Не каждый каталог используется для каждого AngularJS-модуля, но хорошо иметь удобные места с понятными названиями, куда можно класть компоненты своего модуля, когда придет время.

Следующий шаг – добавление к модулю контроллера.

Создание AngularJS-контроллера

Когда каркас модуля готов, создать контроллер очень легко. Введите команду yo meanjs:angular-controller events и выберите модуль events, как показано в листинге 7.

Листинг 7. Создание контроллера
[?] Which module does this controller belongs to? 
  articles 
  core 
? events 
  talks 
  users 

create public/modules/events/controllers/events.client.controller.js
create public/modules/events/tests/events.client.controller.test.js

Как видите, генератор Yeoman поместил контроллер в каталог controllers и соответствующий тест в каталог tests указанного модуля.

У вас может возникнуть законный вопрос: «А для чего вообще нужен этот контроллер?» Вспомните, что AngularJS – это основа клиентской части архитектуры модель-представление-контроллер (MVC). В конечном итоге мы придем к представлению (HTML-элементу <div>, созданному в начале этого руководства), заполняемому данными модели (JSON-структуры, заполненной данными о мероприятии из Meetup.com). Как представление получит доступ к модели? Задача контроллера – объединить все вместе и обеспечить представление с нужными данными модели.

Вот простой пример, который демонстрирует, как собираются разные части головоломки MVC. Откройте в текстовом редакторе файл public/modules/events/controllers/events.client.controller.js, показанный в листинге 8.

Листинг 8. Пустой, заглушенный AngularJS-контроллер
'use strict';

angular.module('events').controller('EventsController', ['$scope',
  function($scope) {
    // Логика контроллера Events
    // ...
  }
]);

Чуть позже мы привяжем этот контроллер к определенному элементу DOM. Переменная $scope будет играть жизненно важную роль, передавая модель представлению.

Добавим к $scope переменную title (модель), как показано в листинге 9.

Листинг 9. Добавление к AngularJS-контроллеру переменной $scope
'use strict';

angular.module('events').controller('EventsController', ['$scope',
  function($scope) {
    $scope.title = 'High Performance WebSocket';

  }
]);

Далее, добавим EventsController к DOM-элементу Event (представлению) в файле public/modules/core/views/home.client.view.html:

<div class="row center-block event" 
       itemscope 
       itemtype="http://data-vocabulary.org/Event"
       ng-controller="EventsController">

Как вы уже догадались, этот код связывает контролер с DOM-элементом. Переменная $scope действительна только для этого элемента <div> и его дочерних элементов. Контроллер можно связать с каким угодно числом различных DOM-элементов. Каждый элемент получает свой собственный уникальный экземпляр нового контроллера и свою собственную уникальную переменную $scope.

Далее, добавим к HTML-структуре взамен жестко запрограммированного текста заполнитель {{title}}:

<h3 class="event-title"><a href="http://www.meetup.com/HTML5-Denver-Users-Group/events/160326502/" 
itemprop="url"><span itemprop="summary">{{title}}</span></a></h3>

Просматривая результаты в веб-браузере, вы должны увидеть вместо заполнителя {{title}} текст, переданный через EventsController, как показано на рисунке 6.

Рисунок 6. Заполнители, замененные реальным текстом
Заполнители, замененные реальным текстом
Заполнители, замененные реальным текстом

Теперь, когда у нас есть простой, действующий пример, можно приступить к его расширению. (Другими словами, теперь, когда приложение работает, его можно снова сломать.) К $scope можно добавить сколько угодно переменных, и эти переменные могут быть простыми, одиночными значениями или целыми JSON-объектами.

Чуть позже вы увидите, как сделать HTTP-запрос к Meetup.com, чтобы получить JSON по следующему предстоящему событию. До тех пор заполним $scope упрощенными фиктивными данными, отражающими то, что можно получить в результате фактического Ajax-вызова, как показано в листинге 10.

Листинг 10. Фиктивный ответ JSON
'use strict';

angular.module('events').controller('EventsController', ['$scope',
  function($scope) {
        $scope.title = 'High Performance WebSocket';
        $scope.event = {
          'name': '"Developing Offline Applications" and "HTML 5 Animations"',
          'time': 1411430400000,
          'event_url': 'http://www.meetup.com/HTML5-Denver-Users-Group/events/160326502/',
          'description': '<p><b>6 pm : "Developing Offline 
          Applications with HTML 5" by Venkat Subramaniam</b></p>',
          'venue': {
            'name': 'Rally Software',
            'address_1': '1550 Wynkoop',
            'city': 'Denver',
            'state': 'CO',
          }      
       }
    }
]);

Как видите, переменная $scope.event содержит сложный, вложенный объект JSON. Изменим представление так, чтобы воспользоваться преимуществами этой новой модели данных, как показано в листинге 11.

Листинг 11. Добавление заполнителей в код HTML
<h3 class="event-title"><a href="{{event.event_url}}" itemprop="url"><span 
itemprop="summary">{{event.name}}</span></a></h3>
<div class="col-md-4">
  <h4>When</h4>
  <p>{{event.time}}</p>
  <h4>Where</h4>
  <address itemprop="location" 
           itemscope 
           itemtype="http://data-vocabulary.org/Organization">
    <span itemprop="name">{{event.venue.name}}</span><br>
    <span itemprop="address" 
          itemscope 
          itemtype="http://data-vocabulary.org/Address">
      <span itemprop="street-address">{{event.venue.address_1}}</span><br>   
      <span itemprop="locality">{{event.venue.city}}</span>, 
      <span itemprop="region">{{event.venue.state}}</span><br>
    </span>
  </address>
</div>

<div class="col-md-8" itemprop="description">
  {{event.description}}
</div>

При просмотре результатов в веб-браузере везде, где мы добавили заполнители в шаблонное представление, должны появиться значения из $scope.event, как показано на рисунке 7.

Рисунок 7. Главная страница UGLI с макетом данных JSON
Главная страница UGLI с макетом данных JSON
Главная страница UGLI с макетом данных JSON

Прежде чем создать сервис AngularJS для получения актуальных фактических данных, необходимо решить пару простых задач, связанных с представлением: добавить несколько фильтров AngularJS для форматирования дат и добиться отображения визуализированного HTML-кода — вместо неформатированного, модифицированного (escaped) HTML — в заполнителе {{event.description}}.

Добавление AngularJS-фильтров

AngularJS-фильтры изменяют внешний вид данных — подобно фотографическим или Instagram-фильтрам. Фильтры добавляют к представлению, чтобы изменить внешний вид модели данных, не меняя ее содержания.

Чтобы применить фильтр к заполнителю шаблона, нужно после элемента данных добавить знак конвейера (|) и имя фильтра, например, {{product_code | uppercase}}. AngularJS предлагает несколько встроенных фильтров, в том числе uppercase, lowercase, currency и number. Можно даже написать свои собственные, специальные фильтры.

Один фильтр, который я постоянно использую, — это фильтр date, который позволяет форматировать дату с помощью настраиваемого шаблона.

Например, применим фильтр date к элементу time, который мы создали выше:

<time class="event-date" 
  itemprop="startDate" 
  datetime="{{event.time | date:'yyyy-MM-ddTHH:mm:ss:Z'}}">{{event.time | 
  date:'EEEE, MMMM, d, yyyy'}}</time>

Обратите внимание, что одно и то же поле event.time используется с двумя разными фильтрами. Код EEEE отображает день недели полным словом, например, Monday. Код EEE усекает его до Mon; EE выводит Mo; E отображает М. Код M точно так же работает для названия месяца. Код d – для дней месяца, а код y – для года.

Поле event.time появляется на главной странице несколько раз. Изменим внешний вид элемента When для отображения часа и суффикса AM/PM:

<h4>When</h4>
<p>{{event.time | date:'h a'}}</p>

Обильное использование AngularJS-фильтров в MEAN-приложении подчеркивает один из ключевых принципов MVC-проектирования: данные модели никак не должны быть связаны ни с какими особенностями представления.

Когда элемент event.time правильно отформатирован, остается лишь исправить внешний вид элемента event.description. Для этого нужно сообщить AngularJS, что в этом поле можно безопасно отображать немодифицированный (unescaped) HTML.

Отображение немодифицированного HTML

Почти все данные JSON, с которыми мы работали до сих пор, были чистыми данными — без вложенных HTML-элементов. Поле event.description составляет исключение.

Всякий раз, когда поступает HTML из внешнего источника — надежного или нет — это потенциальный риск для безопасности. Вложенный HTML может нести в себе нежелательные библиотеки JavaScript, способные раскрыть ваши данные для других сайтов.

Чтобы защититься от этого риска, AngularJS автоматически «дезинфицирует» шаблонные данные, модифицируя любые встретившиеся ему HTML-элементы путем замены «реальных» угловых скобок их собратьями в escape-символах &gt; и &lt;. Это не явный фильтр, как те, что мы видели в предыдущем разделе, но идея та же.

В случае поля event.description нужно сообщить AngularJS, что внешний HTML-код можно отображать наряду с локальным. Для этого отредактируем свой шаблон, заменив заполнитель {{event.description}} атрибутом ng-bind-html:

<div class="col-md-8" itemprop="description" ng-bind-html="event.description"></div>

При просмотре главной страницы в браузере видимые, модифицированные HTML-элементы <b> и <p> должны исчезнуть, уступив место визуализированному тексту.

Теперь у нас есть контроллер, модель и представление – остается всего один последний шаг: заменить фиктивное содержимое JSON в контроллере реальными данными, возвращенными в ответ на Ajax-запрос. Для этого нужно добавить в модуль еще один компонент: сервис.

Создание сервиса

Среди прочего, AngularJS-сервис используется для выполнения запросов Ajax – это идеальное решение для взаимодействия с внешним API Meetup.com.

Конечно, Ajax-запросы можно делать и непосредственно из контроллера, но это было бы недальновидно. А что, если сведения о мероприятии понадобятся в других контроллерах? Конечно же, мы не станем копировать исходный код между контроллерами! Вместо этого лучше создать специальный сервис для облегчения совместного использования общих данных несколькими контроллерами.

Введите yo meanjs:angular-service events, чтобы создать сервис events, как показано в листинге 12. Выберите модуль events в ответ на соответствующий запрос.

Листинг 12. Создание AngularJS-сервиса
$ yo meanjs:angular-service events
[?] Which module does this service belongs to? 
  articles 
  core 
events 

  talks 
  users 

create public/modules/events/services/events.client.service.js

Для выполнения HTTP/Ajax-запросов в AngularJS есть встроенный сервис $http. (Все AngularJS-сервисы имеют префикс $). Чтобы использовать $http, нужно ввести его в свой сервис. Когда сервис events заработает, вставим его в EventsController. (AngularJS повсюду использует инъекцию зависимостей).

Помните объект $scope, который мы использовали в EventsController? $scope — это сервис, введенный в контроллер. Как показано в листинге 13, сервис $scope вводится путем его объявления и передачи в функцию в качестве аргумента.

Листинг 13. Введение сервиса $scope
'use strict';

angular.module('events').controller('EventsController', ['$scope',
  function($scope) {
    $scope.title = 'High Performance WebSocket';
  }
]);

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

Теперь, когда вы знаете, как ввести сервис, можно применить эти знания на практике. Откройте файл public/modules/events/services/events.client.service.js, показанный в листинге 14.

Листинг 14. Файл public/modules/events/services/events.client.service.js
'use strict';

angular.module('events').factory('Events', [
    function() {
        // Логика сервиса Events

        // ...

        // Открытый API-интерфейс
        return {
            someMethod: function() {
                return true;
            }
        };
    }
]);

Введем сервис $http, как показано в листинге 15.

Листинг 15. Введение сервиса $http
angular.module('events').factory('Events', ['$http',
    function($http) {
        // Логика сервиса Events
        // ...

        // Открытый API-интерфейс
        return {
            someMethod: function() {
                return true;
            }
        };
    }
]);

Теперь заменим someMethod на getNextEvent и удалим заглушки некоторых базовых функций, как показано в листинге 16.

Листинг 16. Получение JSON из Ajax-вызова
'use strict';

angular.module('events').factory('Events', ['$http',
  function($http) {
    // Открытый API-интерфейс
    return {
      getNextEvent: function() {
        var url = 'http://api.meetup.com/2/events?status=upcoming&order=
        time&limited_events=False&group_urlname=HTML5-Denver-Users-Group&desc=
        false&offset=0&photo-host=public&format=json&page=1&fields=
        &sig_id=13848777&sig=7aa5d53f450ee5449945e8ee89b8cba8968d9e30&callback=JSON_CALLBACK';

        var request = $http.jsonp(url);
        return request;
      }
    };
  }
]);

URL (довольно длинный) возвращает следующие данные о предстоящем мероприятии для Денверской группы пользователей HTML5. (Meetup.com предоставляет удобную песочницу для изучения своего API.) Скопировав URL в адресную строку браузера, вы получите полный ответ JSON. В листинге 17 я отредактировал его для наглядности.

Листинг 17. Ответ JSON от API Meetup.com
{
  "results": [
    {
      "status": "upcoming",
      "visibility": "public",
      "venue": {
        "id": 21506832,
        "name": "Rally Software",
        "state": "CO",
        "address_1": "1550 Wynkoop",
        "city": "Denver"
      },
      "id": "160326502",
      "time": 1411430400000,
      "event_url": "http:\/\/www.meetup.com\/HTML5-Denver-Users-Group\/events\/160326502\/",
      "description": "<p><b>6 pm : \"Developing Offline Applications with HTML 5\" 
      by Venkat Subramaniam<\/b><\/p> ",
      "name": "\"Developing Offline Applications\" and \"HTML 5 Animations\""
    }
  ],
  "meta": {
    "count": 1,
    "total_count": 3,
    "next": "http:\/\/api.meetup.com\/2\/events?status=upcoming&sig_id=13848777&
    order=time&limited_events=False&group_urlname=HTML5-Denver-Users-Group&
    desc=false&sig=7aa5d53f450ee5449945e8ee89b8cba8968d9e30&photo-host=public&offset=1&
    format=json&page=1&fields="
  }
}

Объект JSON внутри массива результатов должен показаться вам знакомым. Усеченный вариант аналогичных данных использовался в качестве заглушки для EventsController. Но обратите внимание, что полный ответ JSON содержит и другую информацию (например, meta), которая не нужна для отображения данных на главной странице. К счастью, прежде чем передать ответ JSON дальше, его можно преобразовать. Добавим логику преобразования в сервис events, как показано в листинге 18.

Листинг 18. Преобразование ответа JSON
    // Открытый API-интерфейс
    return {
      getNextEvent: function() {
        var url = 'http://api.meetup.com/2/events?status=upcoming&order=
        time&limited_events=False&group_urlname=HTML5-Denver-Users-Group&desc=
        false&offset=0&photo-host=public&format=json&page=1&fields=
        &sig_id=13848777&sig=7aa5d53f450ee5449945e8ee89b8cba8968d9e30&callback=JSON_CALLBACK';

        var returnFirstElement = function (data, headers) {
                    return data.results[0];
                };

        var request = $http.jsonp(url, {transformResponse: returnFirstElement});
        return request;
      }
    };
  }

]);

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

Чтобы облегчить процесс разработки, добавим обработчики success и error, как показано в листинге 19. Этот код выводит данные ответа на консоль. Вы можете редактировать или просто игнорировать его.

Листинг 19. Добавление обработчиков success и error
    // Открытый API-интерфейс
    return {
      getNextEvent: function() {
        var url = 'http://api.meetup.com/2/events?status=upcoming&order=
        time&limited_events=False&group_urlname=HTML5-Denver-Users-Group&desc=
        false&offset=0&photo-host=public&format=json&page=1&fields=
        &sig_id=13848777&sig=7aa5d53f450ee5449945e8ee89b8cba8968d9e30&callback=JSON_CALLBACK';
        var returnFirstElement = function (data, headers) {
                    return data.results[0];
                };

        var request = $http.jsonp(url, {transformResponse: returnFirstElement});

        request.success(function(data, status, headers, config) {
            console.log('SUCCESS');
            console.log(data);
        });
        request.error(function(data, status, headers, config) {
            console.log('ERROR');
            console.log(data);
        });

        return request;
      }
    };
  }
]);

Теперь, когда сервис events готов, его можно вставить в контроллер EventsController. Отредактируйте EventsController, как показано в листинге 20.

Листинг 20. Добавление сервиса events в EventsController
'use strict';

angular.module('events').controller('EventsController', ['$scope', 'Events',
  function($scope, Events) {
        $scope.event = undefined;

        Events.getNextEvent().success(function(data){
          $scope.event = data;          
        });
    }
]);

Если все работает как ожидалось, то вы должны увидеть на главной странице полное описание мероприятия, как показано на рисунке 8. Вы поймете, что все работает правильно, если увидите в описании доклада больше подробностей, чем в прежнем макете.

Рисунок 8. Полный, действующий пример
Полный, действующий пример
Полный, действующий пример

Исключение кратковременного появления нестилизованного контента

Вы могли заметить неприятное кратковременное появление нестилизованного контента (Flash of Unstyled Content – FOUC) после первоначальной визуализации главной страницы, пока делается Ajax-запрос к Meetup.com. В противном случае обновите страницу несколько раз, и вы увидите его.

FOUC – не роковая ошибка, но, конечно же, этот эффект не способствует впечатлению профессионального приложения. К счастью, разработчики AngularJS предлагают простое, элегантное решение этой общей проблемы.

Внесем последнее изменение в файл home.client.view.html, чтобы скрыть представление до тех пор, пока не будут получены данные модели, воспользовавшись атрибутом ng-show:

<div class="row center-block event" 
     itemscope 
     itemtype="http://data-vocabulary.org/Event"
     ng-controller="EventsController"     ng-show="event">

Добавление атрибута ng-show в элемент <div> приводит к тому, что весь элемент <div> будет скрыт до тех пор, пока переменная $scope.event не заполнится. Элемент <div> (представление) отображается только тогда, когда Ajax-запрос к Meetup.com возвратит ответ JSON (модель).

Заключение

Приложение UGLI начинает обретать реальную форму. Мы извлекаем JSON-данные из внешнего API и форматируем результирующее представление с помощью микроданных, чтобы поисковые системы и другие автоматические процессы могли получить доступ к той же информации, что и люди, читающие веб-страницы.

В следующей статье я расскажу об авторизации и проверке подлинности с помощью OAuth. Пока же попрактикуйтесь в программировании с использованием стека MEAN.


Ресурсы для скачивания


Похожие темы


Комментарии

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

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Web-архитектура, Open source
ArticleID=1008876
ArticleTitle=MEAN-программирование: объединение MEAN с Meetup.com и микроданными
publish-date=06182015