Galerie des Google Photography Prize

Ilmari Heikkinen

Website für den Google Photography Prize

Seit Kurzem gibt es den Bereich Galerie auf der Website Google Photography Prize. Die Galerie zeigt eine unendlich scrollbare Liste der von Google+ abgerufenen Fotos an. Sie ruft die Liste der Fotos aus einer AppEngine-App ab, die wir zum Moderieren der Fotoliste in der Galerie verwenden. Wir haben auch die Gallery App als Open-Source-Projekt bei Google Code veröffentlicht.

Galerieseite

Das Back-End der Galerie ist eine AppEngine-App, die mithilfe der Google+ API nach Beiträgen mit einem der Hashtags für den Google Photography Prize (z.B. #megpp und #travelgpp) sucht. Die App fügt diese Beiträge dann der Liste der nicht moderierten Fotos hinzu. Unser Content-Team geht einmal pro Woche 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 gekennzeichneten Fotos zur Liste der Fotos hinzugefügt, die auf der Galerieseite angezeigt werden.

Moderations-Backend

Das Frontend der Galerie wird mithilfe 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 die von der App verwendeten Teile der Closure-Bibliothek benötigt:

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 JavaScript, das JSONP verwendet, um die Liste der Fotos aus der AppEngine App abzurufen. JSONP ist ein einfacher ursprungsübergreifender JavaScript-Hack, der ein Skript einschleust, das wie jsonpcallback("responseValue") aussieht. Um die JSONP-Inhalte zu verarbeiten, verwenden wir die goog.net.Jsonp-Komponente in der Closure-Bibliothek.

Das Galerieskript geht die Liste der Fotos durch und generiert HTML-Elemente, damit sie auf der Galerieseite erscheinen. Das unendliche Scrollen funktioniert, indem es sich an das Ereignis zum Scrollen des Fensters anschließt und einen neuen Satz Fotos lädt, wenn sich das Fenster nahe am Ende der Seite befindet. Nach dem Laden des neuen Fotolistensegments erstellt das Galerieskript Elemente für die Fotos und fügt sie dem Galerieelement hinzu, um sie anzuzeigen.

Liste der Bilder anzeigen

Die Anzeigemethode für Bilderlisten ist ziemlich einfach. Er geht die Bildliste durch, generiert HTML-Elemente und +1-Schaltflächen. Im nächsten Schritt fügen Sie das generierte Listensegment dem Hauptgalerieelement der Galerie hinzu. Der folgende Code enthält einige Closure-Compiler-Konventionen. Beachten Sie die Typdefinitionen im JSDoc-Kommentar und die Sichtbarkeit @private. Private Methoden sind mit einem Unterstrich (_) nach ihrem Namen gekennzeichnet.

/**
 * 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 auf der Seite nach unten gescrollt hat und wir neue Bilder laden müssen, schließt sich die Galerie an das Scroll-Ereignis des Fensterobjekts an. Um die Unterschiede bei Browserimplementierungen zu erläutern, 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() die Fenstergröße 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

Um die Bilder vom Server zu laden, verwenden wir die Komponente goog.net.Jsonp. Für die Abfrage ist ein goog.Uri erforderlich. Nach der Erstellung können Sie eine Abfrage 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, verwendet das Galerieskript den Compiler Closure zum Kompilieren und Kompilieren des Codes. Der Compiler Closure ist außerdem nützlich, um die korrekte Eingabe zu erzwingen (Sie verwenden in Ihren Kommentaren die JSDoc-Notation, um den Typ einer Eigenschaft festzulegen) und teilt Ihnen mit, wenn für eine Methode keine Kommentare vorhanden sind.@type foo

Unittests

Außerdem benötigten wir Einheitentests für das Galerie-Skript. Daher ist es praktisch, dass die Closure-Bibliothek ein integriertes Modultest-Framework enthält. Es folgt den jsUnit-Konventionen, was den Einstieg erleichtert.

Als Hilfe beim Schreiben der Einheitentests habe ich ein kleines Ruby-Skript geschrieben, das die JavaScript-Datei parst und einen fehlgeschlagenen Einheitentest für jede Methode und Eigenschaft in der Galeriekomponente generiert. Bei einem Skript wie diesem:

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

Der Testgenerator generiert für jedes 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 gaben mir einen einfachen Einstieg in das Schreiben von Tests für den Code, und alle Methoden und Eigenschaften wurden standardmäßig abgedeckt. Die fehlgeschlagenen Tests hatten einen schönen psychologischen Effekt, dass ich die Tests einzeln durchgehen und ordnungsgemäße Tests schreiben musste. In Kombination mit einem Messtool für die Codeabdeckung macht es auch Spaß, Tests und Abdeckung auf Grün zu setzen.

Zusammenfassung

Gallery+ ist ein Open-Source-Projekt, das eine moderierte Liste von Google+ Fotos anzeigt, die mit einem #Hashtag übereinstimmen. Es wurde mithilfe von Go und der Closure-Bibliothek erstellt. Das Back-End wird in App Engine ausgeführt. Gallery+ wird auf der Website des Google Photography Prize verwendet, um die Galerie anzuzeigen. In diesem Artikel ging es um das Frontend-Skript. 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 an einem Produktionsbeispiel für Go-Code interessiert sind, halten Sie sich auf dem Laufenden!

Verweise