Google Photography Prize 图库

Ilmari Heikkinen

Google Photography Prize 网站

我们最近在 Google 摄影奖网站上推出了“图库”部分。图库会显示从 Google+ 中提取的照片的无限滚动列表。它会从 AppEngine 应用获取照片列表,该应用用于审核图库中的照片列表。我们还在 Google 代码上将图库应用作为开源项目发布了。

图库页面

图库的后端是一个 AppEngine 应用,该应用使用 Google+ API 来搜索带有 Google 摄影奖 # 标签(例如 #megpp 和 #travelgpp)的帖子。然后,该应用会将这些帖子添加到其未审核的照片列表中。我们的内容团队每周会检查一次未审核的照片列表,并举报违反内容准则的照片。点击“适中”按钮后,未标记的照片就会添加到图库页面显示的照片列表中。

审核后端

Gallery 前端是使用 Google Closure 库构建的。“图库”微件本身就是一个 Closure 组件。在源文件的顶部,我们告知 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 从 AppEngine 应用中检索照片列表。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;
}

这些自动生成的测试帮助我轻松开始编写代码测试,并且默认涵盖所有方法和属性。失败的测试会产生很好的心理影响,我必须逐个进行测试并编写合适的测试。它搭配代码覆盖率测定工具,让测试和覆盖率变为绿色,这是一款趣味横生的游戏。

摘要

“图库+”是一个开源项目,可显示一系列与 #标签匹配的 Google+照片。它是使用 Go 和 Closure 库构建的。后端在 App Engine 上运行。Google 摄影大奖网站上使用“图库+”来显示提交内容图库。 在本文中,我们介绍了许多精彩的前端脚本。来自 App Engine 开发技术推广团队的同事 Johan Euphrosine 正在撰写第二篇文章,探讨后端应用。后端使用 Go(Google 的新服务器端语言)编写。因此,如果您有兴趣查看 Go 代码的生产环境示例,请继续关注!

参考编号