Tworzenie galerii Google Photography Prize
Niedawno uruchomiliśmy sekcję Galeria na stronie Google Photography Prize. Galeria wyświetla listę zdjęć pobranych z Google+ z możliwością przewijania nieskończonego. Lista zdjęć jest pobierana z aplikacji AppEngine, której używamy do moderowania listy zdjęć w galerii. Udostępniliśmy też aplikację galerii jako projekt open source w Google Code.
Zaplecze galerii to aplikacja AppEngine, która używa interfejsu Google+ API do wyszukiwania postów z jednym z hasztagów Google Photography Prize (np. #megpp i #travelgpp). Aplikacja dodaje te posty do listy zdjęć, które nie zostały jeszcze sprawdzone przez moderatora. Raz w tygodniu nasz zespół ds. treści przegląda listę niemoderowanych zdjęć i oznacza te, które naruszają nasze wskazówki dotyczące treści. Po kliknięciu przycisku moderowania nieoznaczone zdjęcia zostaną dodane do listy zdjęć wyświetlanych na stronie galerii.
Frontend galerii
Interfejs Galerii został utworzony przy użyciu biblioteki Google Closure. Sam widżet galerii jest komponentem Closure. U góry pliku źródłowego informujemy Closure, że ten plik udostępnia komponent o nazwie photographyPrize.Gallery, i wymagamy części biblioteki Closure używanej przez aplikację:
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');
Strona galerii zawiera trochę kodu JavaScript, który używa JSONP do pobierania listy zdjęć z aplikacji AppEngine. JSONP to prosty hakerski kod JavaScriptu, który wstrzykuje skrypt wyglądający jak jsonpcallback("responseValue"). Do obsługi JSONP używamy komponentu goog.net.Jsonp w bibliotece Closure.
Skrypt galerii przegląda listę zdjęć i generuje elementy HTML, aby wyświetlić je na stronie galerii. Przewijanie nieskończone działa przez podłączenie się do zdarzenia przewijania okna i wczytywanie nowej partii zdjęć, gdy przewijanie okna zbliża się do dołu strony. Po wczytaniu nowego segmentu listy zdjęć skrypt galerii tworzy elementy dla zdjęć i dodaje je do elementu galerii, aby je wyświetlić.
Wyświetlanie listy obrazów
Metoda wyświetlania listy obrazów jest dość prosta. Przechodzi przez listę obrazów, generuje elementy HTML i przyciski +1. Następnym krokiem jest dodanie wygenerowanego segmentu listy do głównego elementu galerii. W poniższym kodzie możesz zobaczyć niektóre konwencje kompilatora Closure. Zwróć uwagę na definicje typów w komentarzu JSDoc i widoczność @private. Metody prywatne mają podkreślenie (_) po nazwie.
/**
* 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;
};
Obsługa zdarzeń przewijania
Aby sprawdzić, kiedy użytkownik przewinął stronę do dołu i trzeba wczytać nowe obrazy, galeria podłącza się do zdarzenia przewijania obiektu okna. Aby zniwelować różnice w implementacjach przeglądarek, używamy kilku przydatnych funkcji użytkowych z biblioteki Closure: goog.dom.getDocumentScroll() zwraca obiekt {x, y} z bieżącą pozycją przewijania dokumentu, goog.dom.getViewportSize() zwraca rozmiar okna, a goog.dom.getDocumentHeight() – wysokość dokumentu 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() {
// ...
};
Wczytywanie zdjęć
Do wczytywania obrazów z serwera używamy komponentu goog.net.Jsonp. Do wysłania zapytania potrzebny jest goog.Uri. Po utworzeniu możesz wysłać zapytanie do dostawcy Jsonp z obiektem parametru zapytania i funkcją wywołania zwrotnego po powodzeniu.
/**
* 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);
};
Jak wspomniano powyżej, skrypt galerii używa kompilatora Closure do kompilowania i minimalizowania kodu. Kompilator Closure jest też przydatny do wymuszania prawidłowego wpisywania (w komentarzach używasz notacji JSDoc @type foo, aby ustawić typ właściwości) i informuje, gdy nie masz komentarzy do metody.
Testy jednostkowe
Potrzebowaliśmy też testów jednostkowych skryptu galerii, więc dobrze się składa, że biblioteka Closure ma wbudowaną platformę do testów jednostkowych. Jest zgodny z konwencjami jsUnit, więc łatwo zacząć z niego korzystać.
Aby ułatwić sobie pisanie testów jednostkowych, napisałem krótki skrypt w Ruby, który analizuje plik JavaScriptu i generuje nieudany test jednostkowy dla każdej metody i właściwości w komponencie galerii. W przypadku skryptu takiego jak:
Foo = function() {}
Foo.prototype.bar = function() {}
Foo.prototype.baz = "hello";
Generator testów generuje pusty test dla każdej właściwości:
function testFoo() {
fail();
Foo();
}
function testFooPrototypeBar = function() {
fail();
instanceFoo.bar();
}
function testFooPrototypeBaz = function() {
fail();
instanceFoo.baz;
}
Te automatycznie wygenerowane testy ułatwiły mi rozpoczęcie pisania testów kodu, a wszystkie metody i właściwości zostały domyślnie uwzględnione. Nieudane testy wywołują ciekawy efekt psychologiczny, ponieważ muszę przejść przez nie jeden po drugim i napisać odpowiednie testy. W połączeniu z miernikiem pokrycia kodu jest to zabawna gra, w której chodzi o to, aby wszystkie testy i pokrycie były zielone.
Podsumowanie
Gallery+ to projekt open source, który wyświetla moderowaną listę zdjęć z Google+ pasujących do #hashtaga. Został on utworzony przy użyciu języka Go i biblioteki Closure. Backend działa w App Engine. Galeria+ jest używana na stronie internetowej Google Photography Prize do wyświetlania galerii zgłoszeń. W tym artykule omówiliśmy najważniejsze elementy skryptu frontendu. Mój kolega Johan Euphrosine z zespołu ds. relacji z deweloperami App Engine pisze drugi artykuł o aplikacji backendowej. Backend jest napisany w Go, nowym języku Google po stronie serwera. Jeśli więc chcesz zobaczyć przykład kodu Go w środowisku produkcyjnym, zapraszamy do oglądania.
Odniesienia
- Google Photography Prize
- Strona projektu Gallery+
- Biblioteka Closure
- Closure Compiler