Galerie de photos Google Photography Prize

Ilmari Heikkinen

Site Web du prix Google en photographie

Nous avons récemment lancé la section "Galerie" sur le site du Prix de la photographie Google. La galerie affiche une liste infinie de photos extraites de Google+. Elle obtient la liste de photos à partir d'une application AppEngine que nous utilisons pour modérer la liste de photos dans la galerie. Nous avons également publié 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 relatives au contenu. Une fois que vous avez cliqué 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.

Le backend de modération

Le frontend de la galerie est créé à l'aide de la bibliothèque Google Closure. Le widget Galerie est lui-même un composant de fermeture. En haut du fichier source, nous informons Closure que ce fichier fournit un composant nommé photographyPrize.Gallery et que nous avons besoin des parties de la bibliothèque Closure utilisées 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 peu de code JavaScript qui utilise JSONP pour récupérer la liste des photos de l'application AppEngine. JSONP est un simple hack JavaScript inter-origine qui injecte un script ressemblant à jsonpcallback("responseValue"). Pour gérer le JSONP, nous utilisons le composant goog.net.Jsonp de la bibliothèque Closure.

Le script de la galerie parcourt la liste des photos et génère des éléments HTML pour les afficher 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 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.

Affichage de 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. Les méthodes privées sont suivies d'un trait de soulignement (_).

/**
 * 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 quand le visiteur a fait défiler la page jusqu'en bas et que nous devons charger de nouvelles images, la galerie se connecte à l'événement de défilement de l'objet fenêtre. Pour masquer les différences d'implémentation entre les navigateurs, nous utilisons des 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. Une goog.Uri est nécessaire pour effectuer une requête. Une fois créé, vous pouvez envoyer une requête au fournisseur Jsonp 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 la galerie utilise le compilateur Closure pour compiler et réduire le 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 quand vous n'avez pas de commentaires pour une méthode.

Tests unitaires

Nous avions également besoin de tests unitaires pour le script de la galerie. Il est donc pratique que la bibliothèque Closure intègre un framework de test unitaire. Il suit les conventions jsUnit, ce qui le rend facile à prendre 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 non concluant pour chaque méthode et propriété du composant de galerie. Avec un script comme:

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

Le générateur de test 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, et toutes les méthodes et propriétés étaient couvertes par défaut. Les tests échoués ont créé un effet psychologique intéressant, car j'ai dû passer en revue les tests un par un et écrire des tests appropriés. Associé à un indicateur de couverture de code, c'est un jeu amusant pour obtenir un résultat "vert" pour les tests et la couverture.

Résumé

Gallery+ est un projet Open Source qui permet d'afficher une liste modérée de photos Google+ correspondant à un 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 de la photographie Google pour afficher la galerie des envois. Dans cet article, nous avons passé en revue les éléments essentiels du script côté client. Mon collègue Johan Euphrosine, de l'équipe Developer Relations d'App Engine, rédige un deuxième article sur l'application backend, écrite en Go, le nouveau langage côté serveur de Google. Si vous souhaitez voir un exemple de code Go en production, restez à l'écoute !

Références