Google Photography Prize ギャラリー

Ilmari Heikkinen

Google Photography Prize のウェブサイト

最近、Google Photography Prize のサイトにギャラリー セクションをリリースしました。ギャラリーには、Google+ から取得した写真の無限スクロール リストが表示されます。この写真のリストは、ギャラリー内の写真のリストを管理するために使用する AppEngine アプリから取得されます。また、ギャラリー アプリを Google Code でオープンソース プロジェクトとしてリリースしました。

ギャラリー ページ

ギャラリーのバックエンドは、Google+ API を使用して Google Photography Prize のハッシュタグ(#megpp や #travelgpp など)が付いた投稿を検索する AppEngine アプリです。アプリは、それらの投稿を未管理の写真のリストに追加します。週に 1 回、コンテンツ チームが未審査の写真のリストを確認し、コンテンツ ガイドラインに違反している写真にフラグを付けます。[Moderate] ボタンをクリックすると、フラグが設定されていない写真がギャラリー ページに表示される写真のリストに追加されます。

モデレーション バックエンド

ギャラリーのフロントエンドは、Google Closure ライブラリを使用して構築されています。ギャラリー ウィジェット自体は Closure コンポーネントです。ソースファイルの先頭で、このファイルが photographyPrize.Gallery という名前のコンポーネントを提供することを Closure に伝え、アプリで使用される 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');

ギャラリー ページには、JSONP を使用して AppEngine アプリから写真のリストを取得する JavaScript が少し含まれています。JSONP は、jsonpcallback("responseValue") のようなスクリプトを挿入する、シンプルなクロスオリジンの JavaScript ハックです。JSONP 関連の処理には、Closure ライブラリの goog.net.Jsonp コンポーネントを使用しています。

ギャラリー スクリプトは、写真のリストを調べて、ギャラリー ページに表示する HTML 要素を生成します。無限スクロールは、ウィンドウ スクロール イベントにフックし、ウィンドウ スクロールがページの下部に近づいたときに新しい写真のバッチを読み込むことで機能します。新しい写真リスト セグメントを読み込んだ後、ギャラリー スクリプトは写真の要素を作成し、それらをギャラリー要素に追加して表示します。

画像の一覧を表示する

画像リストの表示方法は、かなり基本的なものです。画像リストを処理し、HTML 要素と +1 ボタンを生成します。次のステップでは、生成されたリスト セグメントをギャラリーのメイン ギャラリー要素に追加します。以下のコードでは、Closure Compiler の規則がいくつか確認できます。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 Compiler を使用してコードをコンパイルし、縮小します。Closure Compiler は、正しい型指定を強制する際にも役立ちます(コメントで @type foo JSDoc 表記を使用してプロパティの型を設定します)。また、メソッドにコメントがない場合にも通知されます。

単体テスト

また、ギャラリー スクリプトの単体テストも必要でした。Closure ライブラリには単体テスト フレームワークが組み込まれているため、便利です。jsUnit の規則に準拠しているため、簡単に使い始めることができます。

単体テストの作成を支援するために、JavaScript ファイルを解析して、ギャラリー コンポーネントの各メソッドとプロパティに対して失敗する単体テストを生成する小さな Ruby スクリプトを作成しました。次のようなスクリプトがあるとします。

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;
}

これらの自動生成されたテストにより、コードのテストを簡単に作成できるようになりました。また、すべてのメソッドとプロパティがデフォルトでカバーされていました。テストが失敗すると、テストを 1 つずつ確認して適切なテストを作成しなければならないという心理的な効果が生まれます。コード カバレッジ メーターと組み合わせると、テストとカバレッジをすべて緑色にする楽しいゲームになります。

概要

Gallery+ は、#ハッシュタグに一致する Google+ 写真のモデレートされたリストを表示するオープンソース プロジェクトです。Go と Closure ライブラリを使用して構築されています。バックエンドは App Engine で実行されます。Gallery+ は、Google Photography Prize のウェブサイトで、応募作品のギャラリーを表示するために使用されています。 この記事では、フロントエンド スクリプトの重要な部分について説明しました。App Engine デベロッパー リレーションズ チームの同僚である Johan Euphrosine が、バックエンド アプリに関する 2 つ目の記事を執筆しています。バックエンドは、Google の新しいサーバーサイド言語である Go で記述されています。Go コードの本番環境の例に興味がある場合は、今後の情報にご注目ください。

参照