创建 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 代码的生产环境示例,请继续关注!