Google フォトグラフィー プライズ ギャラリー

Ilmari Heikkinen

Google フォト アワードのウェブサイト

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

ギャラリー ページ

ギャラリーのバックエンドは AppEngine アプリで、Google+ API を使用して Google フォト アワードのハッシュタグ(#megpp、#travelgpp など)が含まれている投稿を検索します。その後、それらの投稿は、管理対象外の画像のリストに追加されます。Google のコンテンツ チームは、モデレーションされていない写真のリストを 1 週間に 1 回確認し、コンテンツ ガイドラインに違反している写真を報告します。[管理] ボタンをクリックすると、報告されていない写真がギャラリー ページに表示される写真のリストに追加されます。

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

ギャラリーのフロントエンドは、Google 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 コンパイラのコンベンションがいくつか含まれています。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 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 フォトグラフィー アワードのウェブサイトで、送信された作品のギャラリーを表示するために使用されます。この記事では、フロントエンド スクリプトの重要な部分について説明しました。App Engine Developer Relations チームの同僚である Johan Euphrosine が、バックエンド アプリについて説明する 2 つ目の記事を執筆しています。バックエンドは、Google の新しいサーバーサイド言語である Go で記述されています。Go コードの本番環境の例を確認したい場合は、今後の記事をお待ちください。

参照