Google Photography Prize 圖片庫

Ilmari Heikkinen

Google Photography Prize 網站

我們最近在 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 程式碼的實際工作環境範例,敬請持續關注!

參考資料