Galerie des Google Photography Prize

Ilmari Heikkinen

Website des Google Photography Prize

Vor Kurzem haben wir den Bereich „Galerie“ auf der Website des Google Fotografiepreises eingeführt. Die Galerie zeigt eine endlos scrollbare Liste von Fotos, die von Google+ abgerufen werden. Die Liste der Fotos wird von einer AppEngine-App abgerufen, die wir zur Moderation der Fotos in der Galerie verwenden. Außerdem haben wir die Galerie-App als Open-Source-Projekt auf Google Code veröffentlicht.

Galerieseite

Das Backend der Galerie ist eine App Engine-Anwendung, die mit der Google+ API nach Beiträgen mit einem der Hashtags des Google Fotografiepreises sucht (z.B. #megpp und #travelgpp). Die App fügt diese Beiträge dann der Liste der nicht moderierten Fotos hinzu. Einmal pro Woche sieht sich unser Content-Team die Liste der nicht moderierten Fotos an und meldet Fotos, 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 Gallery-Frontend wurde mit der Google Closure Library erstellt. Das Galerie-Widget selbst ist eine Schließkomponente. Oben in der Quelldatei teilen wir Closure mit, dass diese Datei eine Komponente namens photographyPrize.Gallery enthält und die Teile der Closure-Bibliothek benötigt, die von der App verwendet werden:

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 Galerieseite enthält ein wenig JavaScript, das die Liste der Fotos mit JSONP aus der AppEngine-App abruft. JSONP ist ein einfacher JavaScript-Hack, der ein Script einschleust, das wie jsonpcallback("responseValue") aussieht. Für die JSONP-Daten verwenden wir die goog.net.Jsonp-Komponente in der Closure-Bibliothek.

Das Galerie-Script durchsucht die Liste der Fotos und generiert HTML-Elemente, die auf der Galerieseite angezeigt werden. Das unendliche Scrollen funktioniert, indem das Fenster-Scrollereignis verbunden wird und eine neue Gruppe von Fotos geladen wird, wenn das Fenster fast am Ende der Seite ist. Nachdem das neue Segment der Fotoliste geladen wurde, erstellt das Galerie-Script Elemente für die Fotos und fügt sie dem Galerieelement hinzu, um sie anzuzeigen.

Liste der Bilder anzeigen

Die Anzeigemethode für Bildlisten ist ziemlich einfach. Es durchsucht die Bildliste, generiert HTML-Elemente und „Mag ich“-Schaltflächen. Im nächsten Schritt fügen Sie das generierte Listensegment dem Hauptelement der Galerie hinzu. Im folgenden Code sind einige Konventionen des Closure Compilers zu sehen. Beachten Sie die Typdefinitionen im JSDoc-Kommentar und die Sichtbarkeitseinstellung „@private“. Private Methoden haben nach ihrem Namen einen Unterstrich (_).

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

Damit wir sehen können, wann der Besucher die Seite ganz nach unten gescrollt hat und neue Bilder geladen werden müssen, wird die Galerie an das Scroll-Ereignis des Fensterobjekts angehängt. Um Unterschiede in der Browserimplementierung zu überbrücken, 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 und goog.dom.getDocumentHeight() die Höhe des HTML-Dokuments zurück.

/**
 * 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. Die Abfrage dauert goog.Uri. Anschließend können Sie eine Anfrage mit einem Abfrageparameterobjekt und einer Erfolgs-Callback-Funktion an den Jsonp-Anbieter 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 zum Kompilieren und Minimieren des Codes verwendet. Der Closure-Compiler ist auch nützlich, um die korrekte Typisierung zu erzwingen. Dazu verwenden Sie in Ihren Kommentaren die @type foo JSDoc-Notation, um den Typ einer Property festzulegen. Außerdem werden Sie darüber informiert, wenn Sie keine Kommentare zu einer Methode haben.

Einheitentests

Außerdem benötigten wir Unit-Tests für das Galerie-Script. Daher ist es praktisch, dass die Closure-Bibliothek ein integriertes Framework für Unit-Tests hat. Es folgt den jsUnit-Konventionen und ist daher einfach zu verwenden.

Zur Unterstützung beim Schreiben der Unit-Tests habe ich ein kleines Ruby-Script geschrieben, das die JavaScript-Datei analysiert und für jede Methode und Eigenschaft in der Galeriekomponente einen fehlgeschlagenen Unit-Test generiert. Angenommen, es liegt dieses Script vor:

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

Der Testgenerator generiert für jede Property einen leeren Test:

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

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

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

Diese automatisch generierten Tests waren ein guter Einstieg in das Schreiben von Tests für den Code. Außerdem wurden standardmäßig alle Methoden und Eigenschaften abgedeckt. Die fehlgeschlagenen Tests haben einen schönen psychologischen Effekt: Ich musste die Tests einzeln durchgehen und richtige Tests schreiben. In Kombination mit einem Codeabdeckungsmessgerät ist es ein lustiges Spiel, die Tests und die Abdeckung auf „Grün“ zu setzen.

Zusammenfassung

Gallery+ ist ein Open-Source-Projekt, mit dem eine moderierte Liste von Google+-Fotos angezeigt wird, die mit einem #Hashtag übereinstimmen. Sie wurde mit Go und der Closure-Bibliothek erstellt. Das Back-End wird in der App Engine ausgeführt. Gallery+ wird auf der Website des Google Fotografiepreises verwendet, um die Galerie mit den Einreichungen zu präsentieren. In diesem Artikel haben wir uns die wichtigsten Teile des Frontend-Scripts angesehen. Mein Kollege Johan Euphrosine vom App Engine Developer Relations-Team schreibt einen zweiten Artikel über die Backend-App. 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