建立 Google 攝影獎圖庫
我們最近在 Google 攝影獎網站上推出了圖庫專區,圖片庫會顯示無限捲動的清單,當中包含從 Google+ 擷取的相片。這個清單會從 AppEngine 應用程式取得相片清單,並依照這個清單管理圖片庫中的相片清單。此外,我們也在 Google 程式碼中以開放原始碼專案的形式發布了 Gallery 應用程式。
圖片庫後端是 AppEngine 應用程式,可透過 Google+ API 搜尋內含 Google Photography Prize 主題標記 (例如 #megpp 和 #travelgpp) 的貼文。應用程式接著會將這些貼文新增至無人管理的相片清單中。我們的內容團隊每週會檢查一次未審核的相片清單,並檢舉違反內容規範的相片。按一下 [審核] 按鈕後,未標記的相片就會新增至圖片庫頁面顯示的相片清單。
程式庫前端
圖片庫前端是使用 Google 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 Hack,可插入類似 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 Photography Prize 網站會使用 Gallery+,即可顯示提交作品圖庫。 在本文中,我們探討了詳細的前端指令碼。App Engine 開發人員關係團隊的同事 Johan Euphrosine 撰寫第二篇文章,說明後端應用程式。後端是以 Google 的全新伺服器端語言 Go 編寫。因此,如果您想查看 Go 程式碼的實際工作環境範例,敬請持續關注!
參考資料
- Google 攝影獎
- Gallery+ 專案頁面
- 封閉程式庫
- 閉包編譯器