Галерея Google Photography Prize

Сайт конкурса Google Photography Prize

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

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

Серверная часть галереи – это приложение AppEngine, которое использует Google+ API для поиска записей с одним из хештегов 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. Галерея+ используется на сайте Google Photography Prize для показа галереи отправленных работ. В этой статье мы рассмотрели самые интересные части скрипта для интерфейса. Мой коллега Йохан Эуфросин (Johan Euphrosine) из команды App Engine Developer Relations пишет вторую статью, посвященную серверной части приложения. Она написана на Go – новом серверном языке Google. Если вы хотите увидеть пример кода Go, оставайтесь с нами.

Ссылки