创建 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 代码的生产环境示例,请持续关注!
参考
- Google 摄影奖
- Gallery+ 项目页面
- 闭包库
- Closure 编译器