Creación de la Galería del Premio de Fotografía de Google
Recientemente, lanzamos la sección Galería en el sitio del Premio de Fotografía de Google. La galería muestra una lista de fotos de desplazamiento infinito recuperadas de Google+. Obtiene la lista de fotos de una app de AppEngine que usamos para moderar la lista de fotos de la galería. También lanzamos la app de galería como un proyecto de código abierto en Google Code.
El backend de la galería es una app de AppEngine que usa la API de Google+ para buscar publicaciones con uno de los hashtags de Google Photography Prize (p.ej., #megpp y #travelgpp). Luego, la app agrega esas publicaciones a su lista de fotos no moderadas. Una vez a la semana, nuestro equipo de contenido revisa la lista de fotos no moderadas y marca las que incumplen nuestros lineamientos de contenido. Después de presionar el botón Moderar, las fotos que no se marcaron se agregan a la lista de fotos que se muestran en la página de la galería.
Frontend de la galería
El frontend de la galería se compila con la biblioteca Google Closure. El widget de Gallery es un componente de Closure. En la parte superior del archivo fuente, le indicamos a Closure que este archivo proporciona un componente llamado photographyPrize.Gallery
y requiere las partes de la biblioteca de Closure que usa la app:
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 página de la galería tiene un poco de JavaScript que usa JSONP para recuperar la lista de fotos de la app de AppEngine. JSONP es un hack de JavaScript de origen cruzado simple que inserta una secuencia de comandos que se parece a jsonpcallback("responseValue")
. Para controlar el contenido JSONP, usamos el componente goog.net.Jsonp
en la biblioteca Closure.
La secuencia de comandos de la galería revisa la lista de fotos y genera elementos HTML para que se muestren en la página de la galería. El desplazamiento infinito funciona conectándose al evento de desplazamiento de la ventana y cargando un nuevo lote de fotos cuando el desplazamiento de la ventana está cerca de la parte inferior de la página. Después de cargar el nuevo segmento de lista de fotos, la secuencia de comandos de la galería crea elementos para las fotos y los agrega al elemento de la galería para mostrarlos.
Cómo mostrar la lista de imágenes
El método de visualización de la lista de imágenes es bastante básico. Revisa la lista de imágenes, genera elementos HTML y botones de +1. El siguiente paso es agregar el segmento de lista generado al elemento principal de la galería. Puedes ver algunas convenciones del compilador de Closure en el siguiente código. Observa las definiciones de tipos en el comentario de JSDoc y la visibilidad @private. Los métodos privados tienen una línea baja (_) después de su nombre.
/**
* 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;
};
Controla eventos de desplazamiento
Para ver cuándo el visitante desglosó la página hasta la parte inferior y necesitamos cargar imágenes nuevas, la galería se conecta al evento de desplazamiento del objeto de ventana. Para ocultar las diferencias en las implementaciones del navegador, usamos algunas funciones de utilidad prácticas de la biblioteca de Closure: goog.dom.getDocumentScroll()
muestra un objeto {x, y}
con la posición de desplazamiento actual del documento, goog.dom.getViewportSize()
muestra el tamaño de la ventana y goog.dom.getDocumentHeight()
muestra la altura del documento 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() {
// ...
};
Imágenes de carga
Para cargar las imágenes del servidor, usamos el componente goog.net.Jsonp
. Se necesita un goog.Uri
para realizar la consulta. Una vez creado, puedes enviar una consulta al proveedor de Jsonp con un objeto de parámetro de consulta y una función de devolución de llamada de éxito.
/**
* 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);
};
Como se mencionó anteriormente, la secuencia de comandos de la galería usa el compilador Closure para compilar y reducir el código. El compilador de Closure también es útil para aplicar la escritura correcta (usas la notación JSDoc @type foo
en tus comentarios para establecer el tipo de una propiedad) y también te indica cuando no tienes comentarios para un método.
Pruebas de unidades
También necesitábamos pruebas de unidades para la secuencia de comandos de la galería, por lo que es útil que la biblioteca de Closure tenga un framework de pruebas de unidades integrado. Sigue las convenciones de jsUnit, por lo que es fácil comenzar a usarlo.
Para ayudarme a escribir las pruebas de unidades, escribí una pequeña secuencia de comandos de Ruby que analiza el archivo JavaScript y genera una prueba de unidades fallida para cada método y propiedad del componente de la galería. Dada una secuencia de comandos como la siguiente:
Foo = function() {}
Foo.prototype.bar = function() {}
Foo.prototype.baz = "hello";
El generador de pruebas genera una prueba vacía para cada una de las propiedades:
function testFoo() {
fail();
Foo();
}
function testFooPrototypeBar = function() {
fail();
instanceFoo.bar();
}
function testFooPrototypeBaz = function() {
fail();
instanceFoo.baz;
}
Estas pruebas generadas automáticamente me permitieron comenzar a escribir pruebas para el código de forma sencilla, y todos los métodos y propiedades se abordaron de forma predeterminada. Las pruebas fallidas crean un buen efecto psicológico en el que tuve que revisar las pruebas una por una y escribir pruebas adecuadas. Junto con un medidor de cobertura de código, es un juego divertido hacer que las pruebas y la cobertura sean verdes.
Resumen
Gallery+ es un proyecto de código abierto para mostrar una lista moderada de fotos de Google+ que coincidan con un #hashtag. Se compiló con Go y la biblioteca de Closure. El backend se ejecuta en App Engine. Gallery+ se usa en el sitio web del Premio de Fotografía de Google para mostrar la galería de envíos. En este artículo, analizamos los aspectos más interesantes de la secuencia de comandos del frontend. Mi colega Johan Euphrosine, del equipo de Relaciones con Desarrolladores de App Engine, está escribiendo un segundo artículo sobre la app de backend. El backend está escrito en Go, el nuevo lenguaje del servidor de Google. Si te interesa ver un ejemplo de producción de código Go, no te pierdas las novedades.
Referencias
- Premio de Fotografía de Google
- Página del proyecto de Gallery+
- Biblioteca de cierre
- Compilador de Closure