Galerie de photos Google Photography Prize

Ilmari Heikkinen

Site Web Google Photography Prize

Nous avons récemment lancé la section "Galerie" sur le site du site Google Photography Prize. La galerie affiche une liste déroulante infinie de photos extraites de Google+. Elle obtient la liste des photos d'une application AppEngine que nous utilisons pour modérer la liste des photos de la galerie. Nous avons également lancé l'application Galerie en tant que projet Open Source sur Google Code.

Page de la galerie

Le backend de la galerie est une application AppEngine qui utilise l'API Google+ pour rechercher les posts contenant l'un des hashtags du Google Photography Prize (par exemple, #megpp et #travelgpp). L'application ajoute ensuite ces posts à sa liste de photos non modérées. Une fois par semaine, notre équipe chargée du contenu examine la liste des photos non modérées et signale celles qui ne respectent pas nos consignes. Après avoir appuyé sur le bouton Modérer, les photos non signalées sont ajoutées à la liste des photos affichées sur la page de la galerie.

Backend de modération

L'interface de la galerie est créée à l'aide de la bibliothèque Google Closure. Le widget Galerie est lui-même un composant Closure. En haut du fichier source, nous indiquons à Closure que ce fichier fournit un composant nommé photographyPrize.Gallery et qu'il requiert les parties de la bibliothèque Closure utilisée par l'application:

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

La page de la galerie contient un extrait de code JavaScript qui utilise JSONP pour récupérer la liste des photos de l'application AppEngine. JSONP est un simple piratage JavaScript multi-origine qui injecte un script ressemblant à jsonpcallback("responseValue"). Pour gérer le JSONP, nous utilisons le composant goog.net.Jsonp dans la bibliothèque Closure.

Le script de la galerie parcourt la liste des photos et génère des éléments HTML pour qu'elles apparaissent sur la page de la galerie. Le défilement infini fonctionne en se connectant à l'événement de défilement de la fenêtre et en chargeant un nouveau lot de photos lorsque le défilement de la fenêtre est proche du bas de la page. Après avoir chargé le nouveau segment de la liste de photos, le script de la galerie crée des éléments pour les photos et les ajoute à l'élément de la galerie pour les afficher.

Afficher la liste des images

La méthode d'affichage de la liste d'images est assez basique. Il parcourt la liste d'images, génère des éléments HTML et des boutons +1. L'étape suivante consiste à ajouter le segment de liste généré à l'élément principal de la galerie. Vous pouvez voir certaines conventions du compilateur Closure dans le code ci-dessous. Notez les définitions de type dans le commentaire JSDoc et la visibilité @private. Un trait de soulignement (_) est ajouté après le nom des méthodes privées.

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

Gérer les événements de défilement

Pour savoir à quel moment le visiteur a fait défiler la page jusqu'en bas et que nous devons charger de nouvelles images, la galerie est liée à l'événement de défilement de l'objet de fenêtre. Pour examiner les différences d'implémentation dans les navigateurs, nous utilisons quelques fonctions utilitaires pratiques de la bibliothèque Closure: goog.dom.getDocumentScroll() renvoie un objet {x, y} avec la position de défilement actuelle du document, goog.dom.getViewportSize() renvoie la taille de la fenêtre et goog.dom.getDocumentHeight() la hauteur du document 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() {
  // ...
};

Chargement des images

Pour charger les images à partir du serveur, nous utilisons le composant goog.net.Jsonp. L'interrogation nécessite un goog.Uri. Une fois la requête créée, vous pouvez envoyer une requête au fournisseur JSON avec un objet de paramètre de requête et une fonction de rappel de réussite.

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

Comme indiqué ci-dessus, le script de galerie utilise le compilateur Closure pour compiler et réduire la taille du code. Le compilateur Closure est également utile pour appliquer une saisie correcte (vous utilisez la notation JSDoc @type foo dans vos commentaires pour définir le type d'une propriété). Il vous indique également si une méthode ne comporte pas de commentaires.

Tests unitaires

Nous avions également besoin de tests unitaires pour le script de galerie. Il est donc pratique que la bibliothèque Closure intègre un framework de tests unitaires. Il respecte les conventions jsUnit, ce qui facilite la prise en main.

Pour m'aider à écrire les tests unitaires, j'ai écrit un petit script Ruby qui analyse le fichier JavaScript et génère un test unitaire qui échoue pour chaque méthode et propriété du composant de galerie. Prenons un script comme:

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

Le générateur de tests génère un test vide pour chacune des propriétés:

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

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

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

Ces tests générés automatiquement m'ont permis de commencer facilement à écrire des tests pour le code. Toutes les méthodes et propriétés étaient couvertes par défaut. Les échecs créent un bon effet psychologique : je devais passer les tests un par un et écrire les bonnes réponses. Associé à un outil de mesure de couverture de code, ce jeu amusant consiste à rendre les tests et la couverture tout verts.

Résumé

Galerie+ est un projet Open Source qui permet d'afficher une liste modérée de photos Google+ correspondant à un hashtag #hashtag. Il a été créé à l'aide de Go et de la bibliothèque Closure. Le backend s'exécute sur App Engine. Gallery+ est utilisé sur le site Web du prix Google Photography pour afficher la galerie des photos envoyées. Dans cet article, nous avons passé en revue les parties juteuses du script de frontend. Mon collègue Johan Euphrosine de l'équipe chargée des relations avec les développeurs App Engine rédige un deuxième article consacré à l'application backend. Le backend est écrit en Go, le nouveau langage côté serveur de Google. Si vous souhaitez voir un exemple de production de code Go, tenez-vous informé !

Références