建立 Google 攝影獎相片廊
我們最近在 Google 攝影獎網站上推出了「相片庫」專區。圖片庫會顯示從 Google+ 擷取的相片無限捲動清單。圖片庫會從 AppEngine 應用程式取得相片清單,我們會使用該應用程式審核圖片庫中的相片清單。我們也將相片庫應用程式以開放原始碼專案的形式發布在 Google Code 上。
相片庫的後端是 AppEngine 應用程式,會使用 Google+ API 搜尋含有 Google 攝影獎其中一個主題標籤的貼文 (例如 #megpp 和 #travelgpp)。應用程式會將這些貼文加入未經審核的相片清單。內容團隊會每週檢查未經審核的相片清單,並標記違反內容規範的相片。按下「Moderate」按鈕後,系統會將未標記的相片加入相片庫頁面顯示的相片清單。
圖庫前端
相片庫前端是使用 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,可使用 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;
}
這些自動產生的測試讓我可以輕鬆開始編寫程式碼測試,而且所有方法和屬性都會預設納入測試範圍。失敗的測試會產生良好的心理效應,讓我必須逐一完成測試,並撰寫適當的測試。搭配程式碼涵蓋率計量器,您就能輕鬆讓測試和涵蓋率全都顯示綠色。
摘要
Gallery+ 是一項開放原始碼專案,可顯示與 #標籤相符的 Google+ 相片審核清單。這個應用程式是使用 Go 和 Closure 程式庫建構而成。後端是在 App Engine 上執行。在 Google 攝影獎網站上,Gallery+ 會用於顯示提交相片的圖庫。在本文中,我們將介紹前端指令碼的實用內容。我的同事 Johan Euphrosine 是 App Engine 開發人員關係維繫團隊成員,他正在撰寫第二篇文章,討論後端應用程式。後端是以 Go 這門 Google 新推出的伺服器端語言編寫而成。因此,如果您想查看 Go 程式碼的實際應用範例,請密切留意後續消息!