Windows ストア アプリの作り方

こんにちは。id:nagayamaです。
現在僕は、はてなブックマークのディレクターという仕事をしています。Webサービスを提供するディレクターたるもの、新しいデバイスやプラットフォームがどういった仕組みになっているのかを体系的に理解しておくのは大事な事です。
今回ははてなブックマークからWindows ストア アプリがリリースされたのをきっかけに、そのアプリの開発に携わったid:nobuokaの力をかりつつ、簡単なはてなブックマークのアプリを実際に作りながら、その仕組みを学びました。
その一連の流れをチュートリアルのような形でまとめたものを公開します。実際のソースコードはGitHubでも公開しています。ぜひ最後までお読みいただき、興味がでた方は手許にcloneして遊んでみてください。

Windows ストア アプリについて

Windows ストア アプリの概要は Windows ストア アプリの概要 に詳しくありますが、とくにWeb開発者にとって注目すべきなのは、

Web 開発テクノロジに関する知識をお持ちの場合は、HTML5、カスケード スタイル シート レベル 3 (CSS3)、JavaScript を使って 新しいユーザーインターフェースのWindows ストア アプリを開発できます。

という箇所でしょう。
今回は、実際にHTML5, CSS, JavaScriptで開発するスタイルで、はてなブックマーク人気エントリーを表示するアプリを作っていく過程を解説していきたいと思います。

開発環境

Windows ストア アプリを開発するにはWindows 8が必要です。
開発者向けの試用版が提供されていますので、こちらをダウンロードして使います。
Oracle社が提供しているVirtualBoxや、VMware社が提供する各種仮想化環境にインストールして開発する事ができますので、OSを一からインストールする事ができない環境でも試す事ができます。

OSのインストールが完了したら続いて、開発ツールのインストールを行います。
デベロッパーセンターのダウンロードページへアクセスします。
f:id:nagayama:20121030130216j:image:w512
「詳細情報および他の言語を入手する (英語)」というリンクをたどり、「Visual Studio Express 2012 for Windows 8」のDownload language を日本語にし「Install now」からインストールを開始します。
f:id:nagayama:20121030130217j:image:w512

インストールが完了したらもちろん起動です。
f:id:nagayama:20121030130218j:image:w512

起動時にプロダクトキーの入力を求められますが、今回は試用という事でこのウィンドウは閉じて先に進めます。
f:id:nagayama:20121018173305j:image:w512

プロジェクトの作成とデバッグ

マイクロソフトのアカウントを入力してログインすると、Start Pageが開きます。
f:id:nagayama:20121018173444j:image:w512

設定によって英語のインターフェースで起動する事がありますが、その時は右上のquick launchに「language」と打ち込み言語設定を変更します。
f:id:nagayama:20121030130219j:image:w512

日本語に設定して再起動すると…
f:id:nagayama:20121030130220j:image:w512

UIが日本語になりました。
f:id:nagayama:20121030130221j:image:w512

ではさっそくプロジェクトを作っていきましょう。
プロジェクトにはテンプレートという雛形があります。色々なテンプレートがありますが、今回はこの中から「グリッド アプリケーション」を選択します。
f:id:nagayama:20121030130222j:image:w512

テンプレートが開き、開発を始める準備が整いました。
f:id:nagayama:20121030130224j:image:w512

ちゃんと動作するかどうか、まずはF5キーを押してデバッグモードにはいります。
f:id:nagayama:20121018173447j:image:w256 f:id:nagayama:20121018173448j:image:w256
ちゃんと動いてますね。

この際にシミュレーターをつかってデバッグすると動作確認画面と開発環境を行き来しやすくなります。
f:id:nagayama:20121030130223j:image:w512

Windows ストア アプリのHTML

f:id:nagayama:20121030130224j:image:w512

では実際にコードを追いかけてみましょう。
まずdefault.htmlを見てみると、body要素の中に

<div id="contenthost" data-win-control="Application.PageControlNavigator" data-win-options="{home: '/pages/groupedItems/groupedItems.html'}"></div>

という記述があります。他のHTMLへのパスがかいてありますが、このHTMLを読み込んで表示しているようですね。
このモデルは単一ページナビゲーション モデルといいデベロッパーセンターに詳しい解説がありますが、通常のWebページではスクリプトをロードする度に初期化されてしまうので、単一のページを起点に画面遷移などが行える仕組みになっています。

groupedItems.htmlの中を見てみると色々な記述があります。
その中でも以下のような「data-win-control="WinJS.Binding.Template"」という属性がついたdiv要素がいくつかあります。

    <div class="headertemplate" data-win-control="WinJS.Binding.Template">

このWinJS.Binding.Templateは、いわゆるテンプレートエンジンに相当するもので、PerlですとTemplateTookit, PHPだとSmarty, RubyだとAmritaなどに近いもののようです。
このテンプレートエンジンの詳しい使い方は http://msdn.microsoft.com/ja-jp/library/windows/apps/hh758329.aspx で参照する事ができます。
簡単には、

    <h4 class="item-title" data-win-bind="textContent: title"></h4>

という風にdata-win-bindという属性を使ってバインドした変数を展開していきます。

このテンプレートの定義の下には、実際に画面に表示されるHTMLが記述されています。
今回は HatenaBookmark というプロジェクト名にしたためアプリのタイトルとして HatenaBookmark が表示されていますが、プロジェクト名にはコロンなどの記号が使えませんので、この h1 の中を「Hatena::Bookmark」に変更してみます。

    <h1 class="titlearea win-type-ellipsis">
        <span class="pagetitle">Hatena::Bookmark</span>
    </h1>

アプリのデータ構造

さて、実際のデータはどこにあるのでしょう。
まず、groupedItems.htmlと同じディレクトリにある、groupedItems.jsを見てみます。
どうやらこの中では、groupedItems.htmlの要素にイベントやデータを割りあてているようです。
具体的には下の箇所で、 ListViewにData.groups.dataSourceを渡しています。

        // This function updates the ListView with new layouts
        _initializeLayout: function (listView, viewState) {
            /// <param name="listView" value="WinJS.UI.ListView.prototype" />

            if (viewState === appViewState.snapped) {
                listView.itemDataSource = Data.groups.dataSource;
                listView.groupDataSource = null;
                listView.layout = new ui.ListLayout();
            } else {
                listView.itemDataSource = Data.items.dataSource;
                listView.groupDataSource = Data.groups.dataSource;
                listView.layout = new ui.GridLayout({ groupHeaderPosition: "top" });
            }
        },

では、このDataを定義しているのはどこでしょうか。groupedImtes.jsの中にはそれらしい所はみつかりません。
もう一度groupedItems.htmlに戻り、head要素の中をみてみると、groupedItems.jsの他に

    <script src="/js/data.js"></script>

という、いかにもといったスクリプトを読み込んでいます。

さっそくdata.jsをみてみると、

    // TODO: Replace the data with your real data.
    // You can add data from asynchronous sources whenever it becomes available.
    generateSampleData().forEach(function (item) {
        list.push(item);
    });

というTODOがありました。
ここでlistにitemを登録して、Dataを作っているようですので、generateSampleData()をみるとどういったデータが送られてくるのかがわかるはずです。

f:id:nagayama:20121030130225j:image:w512

ありました。
さきほどのアプリで表示された内容がここに格納されていますので、ここの内容を書き換えれば、きっと内容もかわるはず。

    var sampleGroups = [
        { key: "group1", title: "最近の人気エントリー", subtitle: "はてなで人気の話題", backgroundImage: darkGray, description: "最近はてなブックマークで人気のある話題です" },
    ];

    // Each of these sample items should have a reference to a particular
    // group.
    var sampleItems = [
        { group: sampleGroups[0], title: "人気記事: 1", subtitle: "これはすごい", description: "概要がここにはいります。", content: "本文がここにはいります", backgroundImage: lightGray },
        { group: sampleGroups[0], title: "人気記事: 2", subtitle: "これはきれい", description: "概要がここにはいります。", content: "本文がここにはいります", backgroundImage: darkGray },
        { group: sampleGroups[0], title: "人気記事: 3", subtitle: "これはべんり", description: "概要がここにはいります。", content: "本文がここにはいります", backgroundImage: mediumGray },
        { group: sampleGroups[0], title: "人気記事: 4", subtitle: "これはやばい", description: "概要がここにはいります。", content: "本文がここにはいります", backgroundImage: darkGray },
        { group: sampleGroups[0], title: "人気記事: 5", subtitle: "これはひどい", description: "概要がここにはいります。", content: "本文がここにはいります", backgroundImage: mediumGray }
    ];

今回は人気エントリーRSSを利用するので、上記のように書き換えてみます。
これで実行してみると、ご覧の通り内容が書きかわってぐっとそれっぽくなってきました。
f:id:nagayama:20121018173308j:image:w512

つづいては、このデータ構造をはてなブックマークRSSを取得して作りだしていきます。

Web上のコンテンツの取得

WinJS.xhrという関数を使ってRSSを取得します。
WinJS.xhrはXMLHttpRequestのラッパーになっていて、これを使うとWeb上のコンテンツを簡単にダウンロードしてアプリから利用する事ができます。
http://msdn.microsoft.com/ja-jp/library/windows/apps/hh868282.aspx

サーバとの通信は非同期になるので、コールバック関数の中でlistにpushするという処理を行うようにします。

    WinJS.xhr({ url: feedUrl }).done(
    function complete(result) {
        // この関数で処理する
    },
    function error(error) {
    });

はてなブックマークではJSON形式でのフィード提供をしていないので、Googleが提供しているAPIを利用させていただき、RSSをJSONに変換します。

    var feedUrl = "http://b.hatena.ne.jp/hotentry.rss";
    feedUrl = "http://ajax.googleapis.com/ajax/services/feed/load?v=1.0&output=json&num=999&q=" + encodeURIComponent(feedUrl);

現在は人気エントリーしか表示しませんが、他のカテゴリーも表示できるようにカテゴリーのグループをつくっておきます。

    var categories = [
        { key: "group1", title: "最近の人気エントリー", subtitle: "はてなで人気の話題", description: "最近はてなブックマークで人気のある話題です" },
    ];

WinJS.xhrを利用して、取得したJSONにgropu属性を追加してlistにpushしていきます。
このlistはWinJS.Binding.Listのインスタンスにpushするなどの操作をする事で、動的に表示しているリストの要素を変更する事ができます。

    WinJS.xhr({ url: feedUrl })
        .done(function complete(result) {
            var jsonData = JSON.parse(result.response);
            jsonData.responseData.feed.entries.forEach(function (item) {
                item.group = categories[0];
                list.push(item);
            });
        });

    /* サンプルデータは表示しないようにコメントアウトしておきます
    generateSampleData().forEach(function (item) {
        list.push(item);
    });
    */

実行すると、ついに人気エントリーのコンテンツが表示されるようになりました!
f:id:nagayama:20121018173309j:image:w512

テンプレートの編集

でも少し見た目が変ですね…。
この見た目をいじるためには、上でも述べたgroupedItems.htmlの中のテンプレートを変更します。

    <div class="itemtemplate" data-win-control="WinJS.Binding.Template">
        <div class="item">
            <img class="item-image" src="#" data-win-bind="src: backgroundImage; alt: title" />
            <div class="item-overlay">
                <h4 class="item-title" data-win-bind="textContent: title"></h4>
                <h6 class="item-subtitle win-type-ellipsis" data-win-bind="textContent: subtitle"></h6>
            </div>
        </div>
    </div>

背景色を変更するために画像が設定されていましたが、そこをcontentSnippetに変更して概要を表示するようにします。

    <img class="item-image" src="#" data-win-bind="src: backgroundImage; alt: title" /><div data-win-bind="textContent: contentSnippet"></div>

サブタイトルは無いので削除します。

    <h6 class="item-subtitle win-type-ellipsis" data-win-bind="textContent: subtitle"></h6>

これでだいぶ見た目がすっきりしました。
f:id:nagayama:20121018173310j:image:w512

ブックマーク数を表示する

しかし何かが足りません。そうブックマーク数です。
はてなブックマークではそのエントリーにつけられたブックマーク数を表示する画像APIを提供しています。
今回はそれを使って、ブックマーク数を表示してみましょう。

data.jsにもどって、

    WinJS.xhr({ url: feedUrl })
        .done(function complete(result) {
            var jsonData = JSON.parse(result.response);
            jsonData.responseData.feed.entries.forEach(function (item) {
                item.group = categories[0];
                item.counterImageURL = "http://b.st-hatena.com/entry/image/" + item.link; // 画像APIのURLを追加する
                list.push(item);
            });
        });

このデータを表示するために以下のHTMLをgroupedImtes.htmlのテンプレートに追加します。

    <div class="item-subtitle win-type-ellipsis"><img data-win-bind="src: counterImageURL" /></div>

これではてなブックマークらしくなりました。
f:id:nagayama:20121018173311j:image:w512


HTMLとJavaScriptをつかったアプリには、ローカルコンテキストと Webコンテキストというの2つのコンテキストがあります。
ローカルコンテキストではWebサイトでよく使われているような外部ドメインの JavaScript を読み込みができないという制限があったり、Webコンテキストではシステムへのアクセスが制限されます。
(詳しくは http://msdn.microsoft.com/ja-jp/library/windows/apps/hh465373.aspx を参照してください)
こういった制限があるため、各アイテムをクリックした時に不正なHTMLを挿入しようとしている旨の警告がでて強制終了してしまいます。
ここでは、toStaticHTMLを利用してHTMLを制限に収まる形にして挿入します。
あわせて、表示に必要のない要素はコメントアウトしました。
itemDetail.jsの中を以下のように変更します。

        var item = options && options.item ? Data.resolveItemReference(options.item) : Data.items.getAt(0);
        element.querySelector(".titlearea .pagetitle").textContent = item.group.title;
        element.querySelector("article .item-title").textContent = item.title;
        /*element.querySelector("article .item-subtitle").textContent = item.subtitle;
        element.querySelector("article .item-image").src = item.backgroundImage;
        element.querySelector("article .item-image").alt = item.subtitle;*/
        element.querySelector("article .item-content").innerHTML = toStaticHTML(item.content);
        element.querySelector(".content").focus();

また、グループの一覧を表示するgroupDetail.htmlも不必要な要素を表示しないようにして、概要の部分にcontentSnippetを表示したりします。

    <div class="headertemplate" data-win-control="WinJS.Binding.Template">
        <h2 class="group-subtitle" data-win-bind="textContent: subtitle"></h2>
        <!-- img class="group-image" src="#" data-win-bind="src: backgroundImage; alt: title" / -->
        <h4 class="group-description" data-win-bind="innerHTML: description"></h4>
    </div>
    <div class="itemtemplate" data-win-control="WinJS.Binding.Template">
        <div class="item">
            <!-- img class="item-image" src="#" data-win-bind="src: backgroundImage; alt: title" / -->
            <div class="item-info">
                <h4 class="item-title" data-win-bind="textContent: title"></h4>
                <!--h6 class="item-subtitle win-type-ellipsis" data-win-bind="textContent: subtitle"></h6 -->
                <div><img data-win-bind="src: counterImageURL" /></div>
                <div class="item-description" data-win-bind="textContent: contentSnippet"></div>
            </div>
        </div>
    </div>

さて、これで一通りブックマークのRSSから得られる情報を表示しました。
さらにCSSなどを使って見た目を調整したり、ユーザーさんのコメントを表示したり、たとえばReadabilityのAPIと組み合わせて、アプリ内で本文を確認できる仕組みをつけたり、色々な応用が考えられます。

DOM Explorer の使い方

さらにカスタマイズをしていくなかで、DOM Explorerの使い方を覚えておくと便利です。
デバッグを実行している間に、デバッグ → ウィンドウ → DOM Explorerで開くことができます。
f:id:nagayama:20121030130226j:image:w512

左上の「要素の選択」をクリックすると、シミュレーターの方に画面が切り替わり、その中のカーソルの下にあるエレメントに青い枠がでます。
これで調べたいエレメントをクリックすると、現在どのようなHTMLが描画されどのようなスタイルが適用されているかが分析できます。
f:id:nagayama:20121030130227j:image:w512

Stylesのタブの中はリアルタイムに値を変更できますので、ここで調節した値をCSSにして保存すればHTMLでアプリケーションを作っている感覚で見た目を変更する事ができます。

今後の道標

ここまでチュートリアル形式で Windows ストア アプリ開発の方法を見てきましたが、Windows ストア アプリ開発で使用できる機能は他にも様々に存在します。 さらに開発を進めていくうえで参考になるドキュメントを紹介します。

まず、JavaScript で開発する場合に限らず、Windows ストア アプリにどのような機能を実装すべきなのか、どのようなユーザー体験を提供すべきなのか、といったことは、下記ドキュメントをご覧ください。

また、JavaScript で開発する場合に参考にすべきドキュメントについては、下記ページにロードマップとしてまとまっていますので、こちらを参考にしてください。

ロードマップとしてまとまっているとはいえ、それでも量が膨大ですので、最初に読むと良さそうなドキュメント類をいくつか紹介します。

サンプルコードについて

MSDN には、Windows ストア アプリのサンプルコードが多数存在します。 Microsoft によって提供されているものもありますし、第三者により提供されているものもあります。 特に Microsoft によって提供されている各種機能のサンプルコードはわかりやすいものが多く充実しておりますので、ドキュメントを見てよくわからなければサンプルコードを検索するということを心がけるとよいと思います。

これらのサンプルコードは web 上で検索してダウンロードすることもできますが、Visual Studio の "New Project" の "Online" から検索してダウンロードしてくることもできます。

スプラッシュ画面とタイル画像やロゴ画像の変更方法について

スプラッシュ画面に表示される画像やロゴ画像は、プロジェクトのルートディレクトリにある package.appxmanifest で指定されます。 スプラッシュ画面に表示される画像の変更方法は下記ドキュメントをご覧ください。

ロゴ画像なども同様に package.appxmanifest を編集することで変更できます。 ロゴなどの画像のサイズは下記ドキュメントをご覧ください。

WinJS ライブラリとクラス定義、名前空間定義の補助メソッドについて

この記事の中では特に説明せずに使用してきましたが、JavaScript で Windows ストア アプリを開発するための機能が WinJS ライブラリとして提供されています。 WinJS ライブラリにはクラスを定義するための補助関数から各種 UI やアニメーションまで多くの機能が含まれています。

特に基本的な補助機能であるクラス定義や名前空間定義に関しては下記ドキュメントをご覧ください。

コントロールについて

ユーザーとの対話を行うアプリケーションでは、コンテンツをユーザーに見せたりユーザーとの相互作用を行ったりするための制御器――すなわちコントロールが必要となります。 HTML 要素そのものもユーザーとの相互作用を行うための機能を持つ (例えば type=checkbox のインプット要素を例にすると、ユーザーの操作によりチェック状態が変化し、また、そのチェック状態をユーザーに見えるように表示するという機能があります) ので、JavaScript を使う Windows ストア アプリでは HTML 要素そのものもコントロールだとみなすことができます (HTML コントロール)。 また、HTML 要素そのものだけでなく、WinJS ライブラリによって提供されるコントロール (WinJS コントロール) もあります。

この記事中でも、プロジェクトテンプレート内でもともと使用されているコントロールとして WinJS.UI.ListView コントロールなどを使用しました。 コントロールを使いこなすことで、Windows ストア アプリとして一貫した見た目、操作性を少ない労力で提供することができます。 コントロールについて、詳しくは下記ドキュメントをご覧ください。

さらに、独自のコントロールを定義することも可能です。 詳しくは下記ブログ記事をご覧ください。

コントラクトについて

Windows ストア アプリでは、コントラクトを用いて別のアプリや Windows のシステムとの間の対話的操作のサポートを宣言することができます。 例えばはてなブックマークアプリでは表示中のエントリページの URI を共有す
る共有ソースコントラクトを実装しているので、その URI を共有してメールアプリや twitter アプリで使用するということが容易になっています。 コントラクトについては下記ドキュメントとブログ記事をご覧ください。

特によく使われるであろう 3 つのコントラクトについて、以下にドキュメントを挙げます。

まとめ

さて、だいぶ駆け足でお送りしてきましたが、Windows ストア アプリの概要は掴めたでしょうか。
アプリという形にするためには、デザインガイドラインを含めいくつかの制限を乗り越える必要はありますが、やはりWeb開発者としては普段使い慣れ親しんだHTMLとJavaScriptという開発環境と、既存のWebアプリケーションの資産を有効に活用できるという事もあり、これまでのWindows デスクトップ アプリケーション開発に比べてぐっと身近に感じる事ができました。
今回のソースコードは以下で公開していますので、是非手許にcloneして試してみてください。
https://github.com/hatena/WindowsStoreAppTutorial