JavaScriptでWebアプリを開発できる「Node.js」活用入門 4ページ
Bing Mapを用いたサンプルアプリケーション「newb map」
今回はWindows Azure上で動かすNode.jsアプリケーションのサンプルとして、Bing Mapsを使った「newb map」というWebアプリケーションを用意した。このアプリケーションは、Bing MapsのAPIを使って地図を表示するもので、地図上をクリックするとその場所に「ピン」がセットされる。ピンは複数を配置でき、ピン間の距離やその合計が表示されるようになっている(図14)。
また、ピンを配置した状態で「Publish」ボタンをクリックすることで、そのピン配置を保存することができる(図15)。
ピンを保存すると一意なURLが表示され、そのURLにアクセスすることで配置したピンが付いた地図を表示できる、という仕組みだ(図16)
newb mapアプリケーションではフレームワークとしてexpressを使用している。また、Webブラウザ側で動作させるクライアントサイドコードではJavaScriptライブラリであるjQueryを使用している。jQueryについては本記事内での解説は行わないので、こちらについては適宜関連ドキュメントなどを参照していただきたい。
また、データの保存にはWindows AzureストレージのTableストレージを使用している。Node.jsからWindows Azureストレージにアクセスするには「azure」モジュールを利用すれば良い。azureモジュールはnpmリポジトリに登録されており、npmコマンドでのインストールが可能だ。
> npm install azure
なお、azureモジュールではWindows Azureのストレージアカウントを環境変数から取得するようになっている。そのため、このプログラムを実行させる際には「AZURE_STORAGE_ACCOUNT」環境変数にアカウント名、「AZURE_STORAGE_ACCESS_KEY」環境変数にアクセスキーをあらかじめセットしておく必要がある。
var client = azure.createTableService()
以下では、このアプリケーションでポイントとなる部分のみを抜粋して紹介する。ソースコード全文については筆者のPersonalForgeリポジトリで公開しているので、興味のある方は適宜ダウンロードしていただきたい。
アプリケーションの全体構造
newb mapでは、表2のURLに対するリクエストについてレスポンスを返すようになっている。
URL | リクエスト | 説明 | サーバーに対し送信するデータ | サーバーから送信されるデータ |
---|---|---|---|---|
<サーバー名>/ | GET | トップページを表示する | なし | 対応するHTML |
<サーバー名>/published/<公開ID> | GET | 指定した公開IDに対応するピン付き地図を表示する | なし | 対応するHTML |
<サーバー名>/publish/ | POST | ピン情報をサーバーに送信して保存する | ピンの緯度・経度情報のリスト | 公開ID |
<サーバー名>/listpins/ | POST | 送信した公開IDに対応するピン情報を取得する | 公開ID | ピンの緯度・経度情報のリスト |
「<サーバー名>/」はアプリケーションのトップページを表示するURLだ。トップページでは地図の表示や地名による地図検索、ピンの配置、保存といった操作が行える。作成したピンの保存は、そのピンの情報をAjaxで「<サーバー名>/publish/」に対してPOSTで送信することで行う。
保存したピン情報にはID(「公開ID」)が与えられ、「<サーバー名>/published/<公開ID>」というURLにアクセスすることで、対応するピンが付加された地図が表示される。このWebページ内におけるピン情報取得にもAjaxが使用されており、公開IDを「<サーバー名>/listpins/」に対してPOSTで送信することでピン情報を取得している。
newb mapのサーバーサイドスクリプト
まず、各URLに対するリクエストを処理するため、アプリケーションのメインファイルであるapp.js内ではリスト8のようにルーティング設定を行っている。
リスト8 パスに対するルーティング(app.js内)
app.get('/', routes.index); app.get('/published/*', routes.published); app.post('/publish/', routes.publish); app.post('/loadpins/', routes.loadpins);
パスに対応する処理についてだが、まず「/」を処理する「routes.index」および「/published/<公開ID>/」を処理する「routes.published」についてはリスト9のようになっている。前者はtitle変数に「newb map」を与えてindex.jadeテンプレートをレンダリングするもので、後者はそれに加えて公開IDをURLから取り出し、pub_id変数としてテンプレートに与える処理を追加している。
リスト9 「/」および「/published/<公開ID>/」パスに対する処理(routes/index.js)
exports.index = function(req, res){ res.render('index', { title: 'newb map' }) }; exports.published = function(req, res){ var pub_id = req.url.replace("/published/", ""); pub_id = pub_id.replace("/", ""); res.render('published', { title: 'newb map: #' + pub_id, pub_id: pub_id }) };
また、ピン情報を保存する「/publish/」パスに対する処理がリスト10だ。POSTリクエストで送信されたピンの緯度・経度リストをストレージに保存し、公開IDを返す、というものになる。
ここでは、まず公開IDとして使用するランダムな16桁の文字列を生成し、続いてPOSTで渡されたデータをinsertEntity関数でTableストレージに保存、最後にIDをレスポンスとして返す、という処理を行っている。保存先のテーブル名は「newbmap」で、パーティションキーは公開ID、行キーにはピンのインデックス、「latitude」キーにピンの緯度、「longitude」キーにピンの経度を格納している。
リスト10 「/published/」パスに対する処理(routes/index.js)
exports.publish = function(req, res){ // generate unique id var S4 = function() { return (((1+Math.random())*0x10000)|0).toString(16).substring(1); }; var pub_id = S4() + S4() + S4() + S4(); // TODO: check is pub_id already exists. // save data to windows azure storage var azure = require('azure'); var client = azure.createTableService(); var tablename = "newbmap"; for (var i = 0; i < req.body.locations.length; i++) { var item = { PartitionKey: pub_id, RowKey: String(i), latitude: req.body.locations[i].latitude, longitude: req.body.locations[i].longitude } client.insertEntity(tablename, item, function(){}); } res.send(pub_id); };
ストレージに保存されたピン情報を取得する「/loadpins/」パスに対する処理はリスト11だ。こちらはPOSTリクエストで送信された公開IDに該当するピンの緯度・経度リストをストレージから取り出し、json形式で返す、というものになる。
公開IDに該当するピン情報の取り出しは、Tableストレージに対し「パーティションキーが指定した公開IDに一致する」という条件(「PartitionKey eq <公開ID>」というクエリ文字列)を与えてクエリを実行することで行っている。
リスト11 「/loadpins/」パスに対する処理(routes/index.js)
exports.loadpins = function(req, res){ // load data from windows azure storage var pub_id = req.body.pub_id; var azure = require('azure'); var client = azure.createTableService(); var tablename = "newbmap"; var query = azure.TableQuery.select().from(tablename); query = query.where('PartitionKey eq ?', pub_id); client.queryEntities(query, function(err, items){ var locations = new Array(); if (!err) { for (var i = 0; i < items.length; i++) { location = { latitude: items[i]["latitude"], longitude: items[i]["longitude"] }; locations.push(location); } res.json(locations); } else { console.log("error!"); } }); }
newb mapのJadeテンプレート
続いて、「<サーバー名>/」および「<サーバー名>/published/<公開ID>」に対応するHTMLを生成するためのテンプレートを見てみよう。まず、リスト12が「<サーバー名>/」に対応するHTMLを生成するJadeテンプレートだ。
ここでは地図本体を表示するためのdiv要素(#map_box)や、地名検索を行うフォーム(form#search_form)、そしてピンの保存を行うフォーム(form#publish_form)などが用意されている。また、実行されるスクリプトコードは「/javascripts/index.js」から読み出すようにしている。
リスト12 「<サーバー名>/」に対応するHTMLを生成するJadeテンプレート(index.jade)
h1 a(href="/") newb Map - Node.js,express,windows azure, bing Map #container #search form#search_form input#query_text(type='text') input(type='submit', value='Search') script(src='http://ecn.dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=7.0&mkt=ja-jp') #map_box #map_body script(src='/javascripts/index.js') #total_distance span total distance: span#total_result 0 span km form#publish_form input#publish_button(type='submit', value='Publish') span#publish_result #pins_list table thead th Index th latitude th longtitude th distance (km) tbody
また、「<サーバー名>/published/<公開ID>」に対応するHTMLコードを生成するJadeテンプレートはリスト13だ。index.jadeとほぼ同じに見えるが、ピンを保存するフォームが削除されている点と、スクリプトとして「javascript/published.js」をロードするようになっている点、そしてページ内で「var pub_id = “#{pub_id}”;」のように公開IDを格納する変数を宣言している点が異なっている。
リスト13 「<サーバー名>/published/<公開ID>」に対応するHTMLを生成するJadeテンプレート(published.jade)
h1 a(href="/") newb Map - Node.js,express,windows azure, bing Map #container #search form#search_form input#query_text(type='text') input(type='submit', value='Search') script(src='http://ecn.dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=7.0&mkt=ja-jp') #map_box #map_body script(src='/javascripts/published.js') script var pub_id = "#{pub_id}"; #total_distance span total distance: span#total_result 0 span km #pins_list table thead th Index th latitude th longtitude th distance (km) tbody