Галерея фотопризов Google

Ilmari Heikkinen

Веб-сайт премии Google Photography Prize

Недавно мы запустили раздел «Галерея» на сайте Google Photography Prize . В галерее отображается бесконечный прокручиваемый список фотографий, полученных из Google+. Он получает список фотографий из приложения AppEngine , которое мы используем для модерации списка фотографий в галерее. Мы также выпустили приложение галереи как проект с открытым исходным кодом в Google Code .

Страница галереи

Серверной частью галереи является приложение AppEngine, которое использует API Google+ для поиска публикаций с одним из хэштегов Google Photography Prize (например, #megpp и #travelgpp). Затем приложение добавляет эти публикации в свой список немодерируемых фотографий. Раз в неделю наша команда по контенту просматривает список немодерируемых фотографий и помечает те, которые нарушают наши правила в отношении контента. После нажатия кнопки «Умерить» непомеченные фотографии добавляются в список фотографий, отображаемых на странице галереи.

Сервер модерации

Интерфейс галереи построен с использованием библиотеки Google Closure . Виджет «Галерея» сам по себе является компонентом Closure. В верхней части исходного файла мы сообщаем Closure, что этот файл предоставляет компонент с именем photographyPrize.Gallery и требует частей библиотеки Closure, используемых приложением:

goog.provide('photographyPrize.Gallery');

goog.require('goog.debug.Logger');
goog.require('goog.dom');
goog.require('goog.dom.classes');
goog.require('goog.events');
goog.require('goog.net.Jsonp');
goog.require('goog.style');

На странице галереи есть фрагмент JavaScript, который использует JSONP для получения списка фотографий из приложения AppEngine. JSONP — это простой хак JavaScript с перекрестным происхождением, который внедряет скрипт, похожий на jsonpcallback("responseValue") . Для обработки данных JSONP мы используем компонент goog.net.Jsonp в библиотеке Closure.

Скрипт галереи просматривает список фотографий и генерирует для них HTML-элементы, чтобы отобразить их на странице галереи. Бесконечная прокрутка работает путем подключения к событию прокрутки окна и загрузки новой партии фотографий, когда прокрутка окна приближается к нижней части страницы. После загрузки нового сегмента списка фотографий скрипт галереи создает элементы для фотографий и добавляет их в элемент галереи для их отображения.

Отображение списка изображений

Метод отображения списка изображений довольно прост. Он просматривает список изображений, генерирует HTML-элементы и кнопки +1. Следующим шагом будет добавление сгенерированного сегмента списка в основной элемент галереи галереи. Вы можете увидеть некоторые соглашения компилятора Closure в приведенном ниже коде, обратите внимание на определения типов в комментарии JSDoc и видимость @private. Частные методы имеют подчеркивание (_) после их имени.

/**
 * Displays images in imageList by putting them inside the section element.
 * Edits image urls to scale them down to imageSize x imageSize bounding
 * box.
 *
 * @param {Array.<Object>} imageList List of image objects to show. Retrieved
 *                                   by loadImages.
 * @return {Element} The generated image list container element.
 * @private
 */
photographyPrize.Gallery.prototype.displayImages_ = function(imageList) {
  
  // find the images and albums from the image list
  for (var j = 0; j < imageList.length; j++) {
    // change image urls to scale them to photographyPrize.Gallery.MAX_IMAGE_SIZE
  }

  // Go through the image list and create a gallery photo element for each image.
  // This uses the Closure library DOM helper, goog.dom.createDom:
  // element = goog.dom.createDom(tagName, className, var_childNodes);

  var category = goog.dom.createDom('div', 'category');
  for (var k = 0; k < items.length; k++) {
    var plusone = goog.dom.createDom('g:plusone');
    plusone.setAttribute('href', photoPageUrl);
    plusone.setAttribute('size', 'standard');
    plusone.setAttribute('annotation', 'none');

    var photo = goog.dom.createDom('div', {className: 'gallery-photo'}, ...)
    photo.appendChild(plusone);

    category.appendChild(photo);
  }
  this.galleryElement_.appendChild(category);
  return category;
};

Обработка событий прокрутки

Чтобы увидеть, когда посетитель прокрутил страницу вниз и нам нужно загрузить новые изображения, галерея подключается к событию прокрутки объекта окна. Чтобы скрыть различия в реализациях браузеров, мы используем несколько удобных служебных функций из библиотеки Closure: goog.dom.getDocumentScroll() возвращает объект {x, y} с текущей позицией прокрутки документа, goog.dom.getViewportSize() возвращает размер окна, а goog.dom.getDocumentHeight() — высоту HTML-документа.

/**
 * Handle window scroll events by loading new images when the scroll reaches
 * the last screenful of the page.
 *
 * @param {goog.events.BrowserEvent} ev The scroll event.
 * @private
 */
photographyPrize.Gallery.prototype.handleScroll_ = function(ev) {
  var scrollY = goog.dom.getDocumentScroll().y;
  var height = goog.dom.getViewportSize().height;
  var documentHeight = goog.dom.getDocumentHeight();
  if (scrollY + height >= documentHeight - height / 2) {
    this.tryLoadingNextImages_();
  }
};

/**
 * Try loading the next batch of images objects from the server.
 * Only fires if we have already loaded the previous batch.
 *
 * @private
 */
photographyPrize.Gallery.prototype.tryLoadingNextImages_ = function() {
  // ...
};

Загрузка изображений

Для загрузки изображений с сервера мы используем компонент goog.net.Jsonp . Для запроса требуется goog.Uri . После создания вы можете отправить запрос поставщику Jsonp с объектом параметра запроса и функцией обратного вызова для успешного выполнения.

/**
 * Loads image list from the App Engine page and sets the callback function
 * for the image list load completion.
 *
 * @param {string} tag Fetch images tagged with this.
 * @param {number} limit How many images to fetch.
 * @param {number} offset Offset for the image list.
 * @param {function(Array.<Object>=)} callback Function to call
 *        with the loaded image list.
 * @private
 */
photographyPrize.Gallery.prototype.loadImages_ = function(tag, limit, offset, callback) {
  var jsonp = new goog.net.Jsonp(
      new goog.Uri(photographyPrize.Gallery.IMAGE_LIST_URL));
  jsonp.send({'tag': tag, 'limit': limit, 'offset': offset}, callback);
};

Как упоминалось выше, скрипт галереи использует компилятор Closure для компиляции и минификации кода. Компилятор Closure также полезен для обеспечения правильной типизации (вы используете нотацию @type foo JSDoc в своих комментариях, чтобы установить тип свойства), а также сообщает вам, когда у вас нет комментариев для метода.

Модульные тесты

Нам также потребовались модульные тесты для сценария галереи, поэтому удобно, что в библиотеку Closure встроена среда модульного тестирования. Он соответствует соглашениям jsUnit, поэтому с ним легко начать.

Чтобы помочь себе в написании модульных тестов, я написал небольшой Ruby-скрипт, который анализирует файл JavaScript и генерирует неудачный модульный тест для каждого метода и свойства в компоненте галереи. Учитывая сценарий типа:

Foo = function() {}
Foo.prototype.bar = function() {}
Foo.prototype.baz = "hello";

Генератор тестов генерирует пустой тест для каждого из свойств:

function testFoo() {
  fail();
  Foo();
}

function testFooPrototypeBar = function() {
  fail();
  instanceFoo.bar();
}

function testFooPrototypeBaz = function() {
  fail();
  instanceFoo.baz;
}

Эти автоматически сгенерированные тесты позволили мне легко начать писать тесты для кода, и все методы и свойства были охвачены по умолчанию. Неудачные тесты создают приятный психологический эффект: мне приходилось проходить тесты один за другим и писать правильные тесты. В сочетании с измерителем покрытия кода это забавная игра, позволяющая сделать тесты и покрытие полностью зелеными.

Краткое содержание

Gallery+ — это проект с открытым исходным кодом, предназначенный для отображения модерируемого списка фотографий Google+, соответствующих #хэштегу. Он был построен с использованием Go и библиотеки Closure. Серверная часть работает на App Engine. Gallery+ используется на веб-сайте Google Photography Prize для отображения галереи представленных работ. В этой статье мы рассмотрели самые важные моменты сценария внешнего интерфейса. Мой коллега Йохан Евфрозин из группы по связям с разработчиками App Engine пишет вторую статью, в которой рассказывается о серверном приложении. Бэкэнд написан на Go, новом серверном языке Google. Так что, если вам интересно увидеть рабочий пример кода Go, следите за обновлениями!

Ссылки