Google Photography Prize-Galerie

Ilmari Heikkinen

Website des Google Photography Prize

Vor Kurzem haben wir den Galeriebereich auf der Website des Google Photography Prize eingeführt. Die Galerie zeigt eine unendliche Scrollliste mit Fotos, die von Google+ abgerufen werden. Die Liste der Fotos stammt aus einer AppEngine-Anwendung, die wir zum Moderieren der Liste der Fotos in der Galerie verwenden. Außerdem haben wir die Galerie-App als Open-Source-Projekt auf Google Code veröffentlicht.

Die Galerie-Seite

Das Backend der Galerie ist eine AppEngine-Anwendung, die die Google+ API verwendet, um nach Beiträgen mit einem der Hashtags des Google Photography Prize zu suchen (z.B. #megpp und #travelgpp). Die App fügt diese Beiträge dann der Liste der nicht moderierten Fotos hinzu. Einmal pro Woche geht unser Inhaltsteam die Liste der unmoderierten Fotos durch und meldet diejenigen, die gegen unsere Inhaltsrichtlinien verstoßen. Nachdem Sie auf die Schaltfläche „Moderieren“ geklickt haben, werden die nicht gemeldeten Fotos der Liste der Fotos auf der Galerieseite hinzugefügt.

Das Moderations-Backend

Das Frontend der Galerie wird mit der Google Closure-Bibliothek erstellt. Das Galerie-Widget selbst ist eine Closure-Komponente. Am Anfang der Quelldatei teilen wir Closure mit, dass diese Datei eine Komponente namens photographyPrize.Gallery bereitstellt, und fordern die von der Anwendung verwendeten Teile der Closure-Bibliothek an:

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');

Die Galerie-Seite enthält ein wenig JavaScript, das JSONP verwendet, um die Liste der Fotos aus der App Engine-Anwendung abzurufen. JSONP ist ein einfacher ursprungsübergreifender JavaScript-Hack, der ein Script einfügt, das wie jsonpcallback("responseValue") aussieht. Für die JSONP-Verarbeitung verwenden wir die Komponente goog.net.Jsonp in der Closure-Bibliothek.

Das Galerie-Script geht die Liste der Fotos durch und generiert HTML-Elemente, um sie auf der Galerie-Seite anzuzeigen. Das unendliche Scrollen funktioniert, indem das Scroll-Ereignis des Fensters abgefangen wird und eine neue Gruppe von Fotos geladen wird, wenn das Scrollen des Fensters nahe am Ende der Seite ist. Nachdem das neue Fotolistensegment geladen wurde, erstellt das Galerieskript Elemente für die Fotos und fügt sie dem Galerieelement hinzu, um sie anzuzeigen.

Liste der Images anzeigen

Die Methode zur Anzeige der Bildliste ist ziemlich einfach. Die Funktion geht die Bildliste durch und generiert HTML-Elemente und +1-Schaltflächen. Im nächsten Schritt wird das generierte Listensegment dem Hauptgalerieelement der Galerie hinzugefügt. Im folgenden Code sehen Sie einige Closure Compiler-Konventionen. Beachten Sie die Typdefinitionen im JSDoc-Kommentar und die @private-Sichtbarkeit. Private Methoden haben einen Unterstrich (_) nach ihrem Namen.

/**
 * 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;
};

Scroll-Ereignisse verarbeiten

Um zu sehen, wann der Besucher die Seite bis zum Ende gescrollt hat und neue Bilder geladen werden müssen, wird die Galerie mit dem Scroll-Ereignis des Fensterobjekts verknüpft. Um Unterschiede in den Browserimplementierungen auszugleichen, verwenden wir einige praktische Dienstfunktionen aus der Closure-Bibliothek: goog.dom.getDocumentScroll() gibt ein {x, y}-Objekt mit der aktuellen Scrollposition des Dokuments zurück, goog.dom.getViewportSize() gibt die Fenstergröße zurück und goog.dom.getDocumentHeight() die Höhe des HTML-Dokuments.

/**
 * 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() {
  // ...
};

Bilder werden geladen

Zum Laden der Bilder vom Server verwenden wir die Komponente goog.net.Jsonp. Für die Abfrage wird ein goog.Uri benötigt. Nach dem Erstellen können Sie eine Abfrage mit einem Suchparameterobjekt und einer Erfolgs-Callback-Funktion an den JSONP-Bereitsteller senden.

/**
 * 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);
};

Wie bereits erwähnt, wird im Galerie-Script der Closure-Compiler verwendet, um den Code zu kompilieren und zu reduzieren. Der Closure-Compiler ist auch nützlich, um die korrekte Typisierung zu erzwingen. Sie verwenden die JSDoc-Notation @type foo in Ihren Kommentaren, um den Typ einer Property festzulegen. Außerdem werden Sie informiert, wenn Sie keine Kommentare für eine Methode haben.

Einheitentests

Wir benötigten auch Unit-Tests für das Galerie-Script. Es ist also praktisch, dass die Closure-Bibliothek ein integriertes Unit-Test-Framework hat. Es folgt den jsUnit-Konventionen, sodass der Einstieg leicht ist.

Um mir beim Schreiben der Unit-Tests zu helfen, habe ich ein kleines Ruby-Skript geschrieben, das die JavaScript-Datei parst und für jede Methode und Eigenschaft in der Galeriekomponente einen fehlschlagenden Unit-Test generiert. Angenommen, Sie haben ein Skript wie dieses:

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

Der Testgenerator generiert für jede der Properties einen leeren Test:

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

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

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

Diese automatisch generierten Tests haben mir den Einstieg in das Schreiben von Tests für den Code erleichtert. Alle Methoden und Properties wurden standardmäßig abgedeckt. Die fehlgeschlagenen Tests haben einen positiven psychologischen Effekt, da ich die Tests einzeln durchgehen und ordentliche Tests schreiben musste. In Kombination mit einem Codeabdeckungsmesser ist es ein lustiges Spiel, die Tests und die Abdeckung alle grün zu machen.

Zusammenfassung

Gallery+ ist ein Open-Source-Projekt, mit dem eine moderierte Liste von Google+ Fotos angezeigt wird, die mit einem #Hashtag übereinstimmen. Es wurde mit Go und der Closure-Bibliothek erstellt. Das Backend wird in App Engine ausgeführt. Gallery+ wird auf der Website des Google Photography Prize verwendet, um die Galerie der Einreichungen anzuzeigen. In diesem Artikel haben wir die wichtigsten Teile des Frontend-Skripts besprochen. Mein Kollege Johan Euphrosine vom App Engine Developer Relations-Team schreibt einen zweiten Artikel über die Backend-Anwendung. Das Backend ist in Go geschrieben, der neuen serverseitigen Sprache von Google. Wenn Sie also ein Produktionsbeispiel für Go-Code sehen möchten, bleiben Sie dran!

Verweise