Google Photography Prize 图库

Ilmari Heikkinen

Google 摄影大奖网站

我们最近在 Google 摄影大奖网站上推出了“图库”版块。该图库会显示从 Google+ 提取的照片的无限滚动列表。它会从 AppEngine 应用获取照片列表,我们使用该应用来审核图库中的照片列表。我们还在 Google Code 上以开源项目的形式发布了图库应用。

图库页面

该图库的后端是一个 App Engine 应用,它使用 Google+ API 搜索包含 Google 摄影大赛的某个标签(例如 #megpp 和 #travelgpp)的帖子。然后,该应用会将这些帖子添加到其未经审核的照片列表中。我们的内容团队每周会查看一次未经审核的照片列表,并举报违反内容指南的照片。点击“审核”按钮后,未被举报的照片会添加到图库页面上显示的照片列表中。

审核后端

图库前端是使用 Google Closure 库构建的。图库 widget 本身就是一个闭包组件。在源文件顶部,我们告知 Closure 此文件提供名为 photographyPrize.Gallery 的组件,并需要应用使用的 Closure 库的部分:

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

图库页面包含一些 JavaScript,这些 JavaScript 使用 JSONP 从 App Engine 应用检索照片列表。JSONP 是一种简单的跨源 JavaScript 黑客攻击,用于注入类似 jsonpcallback("responseValue") 的脚本。为了处理 JSONP 内容,我们将使用 Closure 库中的 goog.net.Jsonp 组件。

图库脚本会遍历照片列表,并为照片生成 HTML 元素,以便在图库页面上显示。无限滚动功能的运作方式是:钩接到窗口滚动事件,并在窗口滚动到页面底部时加载新一批照片。加载新的照片列表片段后,图库脚本会为照片创建元素,并将其添加到图库元素中以进行显示。

显示图片列表

图片列表显示方法非常基本。它会遍历图片列表,生成 HTML 元素和 +1 按钮。下一步是将生成的列表片段添加到图库的主要图库元素。您可以在以下代码中看到一些 Closure 编译器惯例,请注意 JSDoc 注释中的类型定义和 @private 可见性。私有方法的名称后面会带有下划线 (_)。

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

处理滚动事件

为了了解访问者何时滚动到页面底部,以及我们何时需要加载新图片,该图库会连接到窗口对象的滚动事件。为了弥补浏览器实现方面的差异,我们使用了 Closure 库中的一些实用函数:goog.dom.getDocumentScroll() 会返回包含当前文档滚动位置的 {x, y} 对象,goog.dom.getViewportSize() 会返回窗口大小,goog.dom.getDocumentHeight() 会返回 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() {
  // ...
};

加载图片

如需从服务器加载图片,我们将使用 goog.net.Jsonp 组件。它需要 goog.Uri 才能进行查询。创建完成后,您可以向 Jsonp 提供程序发送查询,并附上查询参数对象和成功回调函数。

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

如上所述,图库脚本使用 Closure 编译器来编译和缩减代码。Closure 编译器还可用于强制执行正确的类型(您可以在注释中使用 @type foo JSDoc 标记来设置属性的类型),并且当您没有为某个方法添加注释时,它也会通知您。

单元测试

我们还需要为图库脚本编写单元测试,因此 Closure 库内置单元测试框架非常方便。它遵循 jsUnit 惯例,因此易于上手。

为了帮助我编写单元测试,我编写了一个小型 Ruby 脚本,用于解析 JavaScript 文件,并为图库组件中的每个方法和属性生成失败的单元测试。假设您有一个脚本,如下所示:

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

测试生成器会为每个属性生成一个空测试:

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

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

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

这些自动生成的测试让我能够轻松开始为代码编写测试,并且默认涵盖了所有方法和属性。失败的测试产生了良好的心理效应,迫使我逐个检查测试并编写适当的测试。结合使用代码覆盖率计,您可以将测试和覆盖率全部变为绿色,这是一个有趣的游戏。

摘要

Gallery+ 是一个开源项目,用于显示与 #标签匹配的经过审核的 Google+ 照片列表。它是使用 Go 和 Closure 库构建的。后端在 App Engine 上运行。Gallery+ 用于在 Google 摄影大奖网站上显示提交作品的图库。 在本文中,我们详细介绍了前端脚本的要点。我同事 Johan Euphrosine 来自 App Engine 开发者关系团队,正在撰写第二篇文章,介绍后端应用。后端使用 Go(Google 的新服务器端语言)编写。因此,如果您有兴趣了解 Go 代码的生产环境示例,请持续关注!

参考