Google Photography Prize 圖片庫

Ilmari Heikkinen

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 程式碼的實際應用範例,請密切留意後續消息!

參考資料