読者です 読者をやめる 読者になる 読者になる

@kotyのブログ

.NETとかJavaとかPythonとか勉強会のこととかを、田舎者SEがつづります。記事のライセンスは"CC BY"でお願いします。

長野県の路線バス検索ページ(のフロントエンド)をAngularJS/TypeScriptで作った

概要

実物はこちらです。 http://b-sw.in/albus/

このページは、大きく

  • フロントエンド・・・HTML+JavaScript
  • バックエンド・・・RoR(らしい)

の2部構成でできています。このうちバックエンドについては、@mz_ken 氏が作っているandroidアプリ「あるバス」のAPIを使わせていただいています。多謝。 本格的にバス検索を利用したい長野県民androidユーザーは、ぜひとも当該アプリを使ってください。

今回AngularJSとTypeScriptの勉強を兼ねて、HTMLアプリでフロントエンド作りました。

開発環境は以下です。ほかにjQueryやらTwitter Bootstrapやらを使っていますが本題から外れますので言及しません。

.NETのコードは一切出てこず静的ファイルのみで構成されているため、Visual Studioを使わなくても、例えばLinux上のEmacsでも開発はできます。TypeScirptの開発環境はnpmからも入れられますからね。

TypeScriptの環境作成

windows上での開発なので、TypeScript for Visual Studio 2012 をインストールします。

JavaScriptMVCフレームワーク

AngularJSは、JavaScriptのUIフレームワークです。この種のフレームワークMVCフレームワークというらしいです。MVCフレームワークの機能は大まかに数種類のものがあり*1、それぞれの機能をサポートしているフレームワークとしていないフレームワークがあります。このうちUI Bindingsは.NETのWPFも備えている機能です。私はもともとWPFをかじっていたため、JavaScriptWPFバインディングみたいなことができないかなぁという動機で、JavaScriptMVCフレームワークを使ってみることにしました。

AngularJS

ここ1, 2年でJavaScriptMVCフレームワークはいろいろ出現しています。この中でAngularJSが今後一番有力というか使われるようになると個人的には思っています。ほかに私が使ってみたことがあるMVCフレームワークにKnockoutJSがありますが、双方向バインディングをするためにはfunctionオブジェクトを介す必要がある点がイマイチです。AngularJSはそれがなく通常のJavaScriptオブジェクトをバインドできます*2。またgoogle製という点も信頼できます。何となくだけど。。。

AngularJSについて知りたい場合は、本家のサイトのチュートリアルをひととおりやってみると良いです。git のブランチを切り替えてトピックを進める形になっており学習を進めやすいです。

とここまで調べた段階で、冒頭に紹介した@mz_ken 氏のandroidアプリの存在を知り、これの縮退Web版を作ってみることにしました。

AngularJSの基本的な話から書こうと思ったんですが、すでにTypeScriptと組み合わせた形で記事にしている方がいるので、そちらを参照して下さい。

JSONP

webサービスのAPIをコールする部分は下記です。

$http.jsonp("http://www9264ui.sakura.ne.jp/busstops/result_bts_lines?format=json&format=js&callback=JSON_CALLBACK")
.success(data => {
    for (var i = 0 ; i < data.busstops.length ; i++) {
        $scope.busstops.push(data.busstops[i].busstopname);
    }
    this.busstopList = data.busstops;
    this.putMarkers($scope);
});

AngularJSのAPIを使わないと、コールバックの中での変更がDOMに反映されません。また、callbackの関数名はJSON_CALLBACKにしておく必要があります。

AngularUI

バス停を地図に表示するためにGoogle Maps APIを使っています。ところがそのままではAngularJSから使いづらいので、AngularUIというAngularJSのmodule群を使いました。AngularUIの中にmapsというズバリ今回使いたかった機能があるため、結局自分でmoduleやカスタムタグを作らずに済みました。ほかにもjQueryUIを利用するためのモジュールがあるみたいですね。

まずはscriptタグを記述します。

<script src="Scripts/angular-ui/angular-ui.min.js"></script>

地図に関する記述は以下です。マーカー用のdivを書かなければいけないということが分からずけっこうハマりました。

<!-- map body -->
<div id="map_canvas"
     data-ui-map="myMap" data-ui-options="mapOptions"
     data-ui-event="{'map-click': 'mapClick($event)'}"></div>
<!--for marker-->
<div data-ng-repeat="marker in busstopMarkers" data-ui-map-marker="busstopMarkers[$index]"
    data-ui-event="{'map-click': 'openMarkerInfo(marker)'}">
</div>
<!--infowindow-->
<div data-ui-map-info-window="busstopInfoWindow">
    <h1>{{currentMarker.title}}</h1>
    <button data-ng-click="setFromBusStop($event)">乗車</button>
    <button data-ng-click="setToBusStop($event)">降車</button>
</div>

対応するfunctionやモデルは$scopeのプロパティとして定義しておきます。*3

$scope.setFromBusStop = $event => {
    $scope.fromBusStop = $scope.currentMarker.title;
};
$scope.setToBusStop = $event => {
    $scope.toBusStop = $scope.currentMarker.title;
};
$scope.openMarkerInfo = function (marker) {
    $scope.currentMarker = marker;
    //$scope.currentMarkerLat = marker.getPosition().lat();
    //$scope.currentMarkerLng = marker.getPosition().lng();
    //$scope.currentBusstopName = marker.title;
    $scope.busstopInfoWindow.open($scope.myMap, marker);
};
$scope.mapOptions = {
    zoom: 15,
    mapTypeId: google.maps.MapTypeId.ROADMAP
};

何も考えないと、コントローラクラスのconstructorにだらだらと記述してしまい行数増えてしまいます。$scopeを持ちまわってメソッドアウトする方が良いかもしれません。

オートコンプリート

便利かどうかは分かりませんが、やってみたかったのでバス停名のテキストボックスに自動補完機能をつけました。jQueryUIのオートコンプリートをそのまま使ったのでは、DOMだけが変わりmodelに反映されません。ググってこちらのコード辺まんまを使わせてもらってます。カスタムディレクティブですね。

angular.module('albus', ['ui']).directive('autoComplete', function () {
    return function (scope, iElement, iAttrs) {
        scope.$watch(iAttrs.uiItems, function (values) {
            iElement.autocomplete({
                source: values,
                select: function () {
                    setTimeout(function () {
                        iElement.trigger('input');
                    }, 0);
                }
            });

        }, true);
    };
});

uiItemsという属性の値を監視し変更があった場合にautocompleteを適用しなおす、という記述のようです。autocompleteのselectイベント内でinputイベントを遅延発生させているのはAngularJSのmodelからviewへの反映のタイミングの関係なんでしょう。ちょっとトリッキーですね。

作ったディレクティブを、下記のようにauto-omplete属性として利用します。

<input class="inputbusstop" type="text" placeholder="到着バス停"
                     data-ng-model="toBusStop"
                     data-auto-complete
                     data-ui-items="busstops" />

まとめ

結果として、app.ts(app.js)にはviewに関する記述(DOM要素のidを指定する等)はありません。ちょっとだけあるけど、それは地図の表示非表示およびcss切替えというviewで完結している処理です。より複雑な画面だとまた話は変わってくるでしょうが、この程度の画面であればそこそこ容易にviewとロジックを分離することができました。

画面をリフレッシュせずにDOMの一部だけを書き換える、いわゆるシングルページアプリケーションが今後増えてくると思います。Web系ではこの傾向は顕著でしょう。エンタープライズ系であっても、Web系ほどではないもののこの流れはあるでしょう。それにあたりJavaScriptMVCフレームワークの検討は避けて通れません。AngularJSはその有力な候補となるでしょう。

また、ある程度の規模のJavaScriptアプリケーションを作るには静的な型システムが欠かせません。コンパイルするとJavaScriptになる言語はほかにCoffeeScriptやHaxeなどありますが、TypeScriptは学習コストやコンパイル後のJSの可読性に長所があります。JavaScriptのフリーダムさも包含してはいますが。

AngularJSもTypeScriptも情報が少なく調べるのがちょっと大変です。もっと使われるようになって情報が増えてほしいものです。

*1:出典:http://www.publickey1.jp/blog/12/javascript_mvc.html

*2:かわりに、modelを更新する場所とタイミングに気を使う必要はありますが。

*3:TypeScriptの強調表示ってできないんかな?