Создание галереи 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, следите за обновлениями!