Galeria nagrody Google Photography Prize

Ilmari Heikkinen

Witryna Google Photography Prize

Niedawno uruchomiliśmy sekcję Galeria w witrynie nagrody Google Photography Prize. Galeria pokazuje nieskończoną przewijaną listę zdjęć pobranych z Google+ i pobiera listę zdjęć z aplikacji AppEngine, której używamy do moderowania listy zdjęć w galerii. Ponadto w Google Code opublikowaliśmy aplikację galerii jako projekt open source.

Strona galerii

Backend galerii to aplikacja AppEngine, która używa interfejsu API Google+ do wyszukiwania postów zawierających jeden z hashtagów nagrody Google Photography Prize (np. #megpp i #travelgpp). Aplikacja doda te posty do listy niemoderowanych zdjęć. Raz w tygodniu nasz zespół ds. treści przegląda listę niemoderowanych zdjęć i oznacza te, które naruszają nasze wytyczne dotyczące treści. Po kliknięciu przycisku Moderuj zdjęcia, które nie są oznaczone, są dodawane do listy zdjęć wyświetlanych na stronie galerii.

Backend moderacji

Interfejs Galerii został utworzony przy użyciu biblioteki Google Closure. Sam widżet Galerii jest komponentem Zamknięcie. U góry pliku źródłowego informujemy o tym, że ten plik zawiera komponent o nazwie photographyPrize.Gallery i wymaga elementów 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 fragment kodu JavaScript, który wykorzystuje JSONP do pobierania listy zdjęć z aplikacji AppEngine. JSONP to prosty kod JavaScript z innych domen, który wstawia skrypt podobny do jsonpcallback("responseValue"). Do obsługi plików JSONP używamy komponentu goog.net.Jsonp w bibliotece Closure.

Skrypt galerii przegląda listę zdjęć i generuje dla nich elementy HTML, które umożliwiają wyświetlenie ich na stronie galerii. Przewijanie nieskończone polega na łączeniu się ze zdarzeniem przewijania okna i wczytywaniu nowej grupy zdjęć, gdy przewijanie okna znajduje się blisko dołu strony. Po wczytaniu nowego segmentu listy zdjęć skrypt galerii tworzy elementy zdjęć i dodaje je do elementu galerii, aby je wyświetlać.

Wyświetlam listę obrazów

Metoda wyświetlania listy obrazów to dość proste zadanie. 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 galerii. W poniższym kodzie znajdziesz niektóre konwencje kompilatora Closure, zwróć uwagę na definicje typów w komentarzu JSDoc i widoczność @private. Metody prywatne mają po nazwie podkreślenie (_).

/**
 * 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 zobaczyć, kiedy użytkownik przewinął stronę na sam dół i musimy wczytać nowe obrazy, galeria łączy się ze zdarzeniem przewijania obiektu window. Aby przedstawić różnice w implementacjach przeglądarek, wykorzystamy kilka przydatnych funkcji z biblioteki Closure: goog.dom.getDocumentScroll() zwraca obiekt {x, y} z bieżącym położeniem 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() {
  // ...
};

Wczytuję obrazy

Do wczytywania obrazów z serwera używamy komponentu goog.net.Jsonp. Wysłanie zapytania wymaga goog.Uri. Po utworzeniu możesz wysłać do dostawcy Jsonp zapytanie z obiektem parametru zapytania i udaną funkcją wywołania zwrotnego.

/**
 * 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 już wspomnieliśmy, do skompilowania i minimalizowania kodu skrypt galerii używa kompilatora Closure. Kompilator Closure przydaje się również do egzekwowania poprawnego pisania (do ustawiania typu właściwości w komentarzach używasz notacji JSDoc @type foo). Informuje też o tym, że nie ma komentarzy do metody.

Testy jednostkowe

Potrzebowaliśmy też testów jednostkowych skryptu galerii, dzięki czemu biblioteka Closure ma wbudowaną platformę testowania jednostkowego. Jest on zgodny z konwencjami jsUnit, dzięki czemu nie ma problemu od początku.

Aby pomóc mi w pisaniu testów jednostkowych, napisałam mały skrypt Ruby, który analizuje plik JavaScript i generuje błędy w testach jednostkowych każdej metody i właściwości w komponencie galerii. Biorąc pod uwagę scenariusz:

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 pozwoliły mi z łatwością rozpocząć pisanie testów kodu, a wszystkie metody i właściwości zostały domyślnie uwzględnione. Nieudane testy wywołują miły efekt psychologiczny, polegający na tym, że musiałam przejść jeden test po kolei i napisać właściwe testy. W połączeniu z miernikiem zasięgu kodu to świetna zabawa, dzięki której testy i zasięg są bardziej zielone.

Podsumowanie

Gallery+ to projekt typu open source wyświetlający moderowaną listę zdjęć z Google+ pasujących do #hashtagu. Została ona utworzona za pomocą Go i biblioteki Closure. Backend działa w App Engine. Aplikacja Gallery+ jest wyświetlana na stronie Nagrody fotograficznej Google do wyświetlania galerii przesłanych prac. W tym artykule omówiliśmy poszczególne elementy skryptu frontendu. Mój współtwórca Johan Euphrosine z zespołu App Engine Developer Relations pisze drugi artykuł o aplikacji backendu. Backend został napisany w Go, czyli w nowym języku Google po stronie serwera. Jeśli więc chcesz zobaczyć przykładowy kod w języku Go w wersji produkcyjnej, nie przegap tego!

Odniesienia