Galeria nagrody Google Photography Prize

Ilmari Heikkinen

Strona Google Photography Prize

Niedawno uruchomiliśmy sekcję Galerii na stronie Google Photography Prize. Galeria zawiera nieskończoną listę przewijanych zdjęć pobieranych z Google+. Lista zdjęć jest pobierana z aplikacji AppEngine, której używamy do moderowania listy zdjęć w galerii. Aplikację galerii udostępniliśmy też jako projekt open source w Google Code.

Strona galerii

Backend galerii to aplikacja AppEngine, która korzysta z interfejsu Google+ API do wyszukiwania postów z jednym z hashtagów Google Photography Prize (np. #megpp i #travelgpp). Aplikacja dodaje te posty do listy nieprzefiltrowanych zdjęć. Raz w tygodniu nasz zespół ds. treści sprawdza listę nieprzefiltrowanych zdjęć i zgłasza te, które naruszają nasze wskazówki dotyczące treści. Po kliknięciu przycisku moderowania zdjęcia bez flagi zostaną dodane do listy zdjęć wyświetlanych na stronie galerii.

Backend moderacji

Interfejs Gallery jest tworzony za pomocą biblioteki Google Closure. Sam widżet Galeria jest komponentem zamknięcia. U góry pliku źródłowego informujemy Closure, że ten plik udostępnia komponent o nazwie photographyPrize.Gallery i wymaga użycia części biblioteki Closure używanych 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 odrobinę kodu JavaScriptu, który używa JSONP do pobrania listy zdjęć z aplikacji AppEngine. JSONP to prosty skrót JavaScriptu w wielu domenach, który wstrzykuje skrypt podobny do jsonpcallback("responseValue"). Do obsługi JSONP używamy komponentu goog.net.Jsonp z biblioteki Closure.

Skrypt galerii przegląda listę zdjęć i generuje dla nich elementy HTML, aby wyświetlić je na stronie galerii. Nieskończone przewijanie działa w ten sposób, że powiązane jest ze zdarzeniem przewijania okna i ładuje nową partię zdjęć, gdy przewijanie okna zbliża się do dołu strony. Po załadowaniu nowego segmentu listy zdjęć skrypt galerii tworzy elementy zdjęć i dodaje je do elementu galerii, aby je wyświetlić.

Wyświetlanie listy obrazów

Metoda wyświetlania listy obrazów jest dość podstawowa. Przegląda 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 znajdziesz kilka konwencji stosowanych przez kompilator Closure. Zwróć uwagę na definicje typów w komentarzu JSDoc i widoczność @private. Metody prywatne mają po nazwie znak podkreślenia (_).

/**
 * 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 końca i gdy musimy wczytać nowe obrazy, galeria łączy się ze zdarzeniem przewijania obiektu okna. Aby ukryć różnice w implementacjach przeglądarek, używamy kilku przydatnych funkcji 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() zwraca 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ęć

Aby wczytywać obrazy z serwera, używamy komponentu goog.net.Jsonp. Wykonanie zapytania zajmuje goog.Uri. Po utworzeniu możesz wysłać zapytanie do dostawcy Jsonp z obiektem parametru zapytania i funkcją wywołania zwrotnego pomyślnego wykonania.

/**
 * 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 wyżej, skrypt galerii używa kompilatora Closure do kompilowania i skompresowania kodu. Kompilator Closure jest też przydatny do egzekwowania poprawnego typowania (w komentarzach używasz @type foo notacji JSDoc, aby ustawić typ właściwości). Informuje też, gdy nie masz komentarzy do metody.

Testy jednostkowe

Potrzebowaliśmy też testów jednostkowych dla skryptu galerii, więc dobrze, że biblioteka Closure ma wbudowany framework do testowania jednostkowego. Jest on zgodny z konwencjami jsUnit, więc rozpoczęcie pracy z nim jest proste.

Aby ułatwić sobie pisanie testów jednostkowych, napisałem mały skrypt Ruby, który analizuje plik JavaScript i generuje nieudaną jednostkę testową dla każdej metody i właściwości w komponencie galerii. Biorąc pod uwagę skrypt:

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

Generator testów generuje pusty test dla każdej z tych usług:

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

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

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

Te testy wygenerowane automatycznie ułatwiły mi rozpoczęcie pisania testów kodu, a wszystkie metody i właściwości były objęte domyślnie. Testy, które nie przeszły, wywołały ciekawy efekt psychologiczny. Musiałem przejść przez nie po kolei i napisać prawidłowe testy. W połączeniu z licznikiem pokrycia kodu stanowi świetną zabawę, ponieważ wszystkie testy i pokrywanie są zielone.

Podsumowanie

Gallery+ to projekt typu open source, który wyświetla moderowaną listę zdjęć z Google+ pasujących do hashtagu #. Został on utworzony przy użyciu języka Go i biblioteki Closure. Backend działa w App Engine. Gallery+ jest używany na stronie Google Photography Prize do wyświetlania galerii przesłanych prac. W tym artykule omówiliśmy najciekawsze fragmenty skryptu po stronie klienta. Mój współpracownik Johan Euphrosine z zespołu ds. relacji z deweloperami App Engine pisze drugi artykuł na temat aplikacji backendowej. Backend jest napisany w języku Go, nowym języku Google na serwer. Jeśli chcesz zobaczyć przykład kodu Go w wersji produkcyjnej, bądź na bieżąco.

Pliki referencyjne