Google Photography Prize ギャラリーを作成する
先日、Google Photography Prize のサイトにギャラリー セクションを開設しました。ギャラリーには、Google+ から取得した写真の無限スクロール リストが表示されます。ギャラリー内の写真リストの管理に使用する写真のリストを AppEngine アプリから取得します。また、Google Code でギャラリー アプリをオープンソース プロジェクトとしてリリースしました。
ギャラリーのバックエンドは、Google+ API を使用して Google Photography Prize のハッシュタグ(#megpp や #travelgpp など)のいずれかが含まれる投稿を検索する AppEngine アプリです。投稿された投稿は、管理されていない写真のリストに追加されます。Google のコンテンツ チームは週に 1 回、管理されていない写真の一覧を確認し、コンテンツ ガイドラインに違反する写真を報告します。[中] ボタンをクリックすると、フラグが付いていない写真がギャラリー ページに表示される写真のリストに追加されます。
ギャラリー フロントエンド
ギャラリーのフロントエンドは、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 を使用して App Engine アプリから写真のリストを取得する 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 コンパイラは、正しい型を設定するのにも便利です(コメント内で @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 コードの本番環境のサンプルをご覧になりたい場合は、ぜひご期待ください。