まだプログラマーですが何か?

プログラマーネタとアスリートネタ中心。たまに作成したウェブサービス関連の話も http://twitter.com/dotnsf

タグ:html

Node-RED Advent Calendar 2017 に参加しました(ちなみにアドベントカレンダーに参加するのは初めて)! 12月9日のネタは「Node-RED で(普通の)ウェブアプリケーションを作る」です。

Node-RED を早くから活用している方の多くは IoT 連携であったり、組み込み機械との連携のデータフローとして使っているケースが多いと思っています。でもそんなケースばかりではなく、自分自身は Node-RED でデータベースを読み書きする REST API だけを作ったり、その REST API を呼び出すフロントエンドまでも含めて Node-RED で作ることがあります。今日はその作り方をスクリーンショットを交えて紹介します。


【普通のウェブアプリケーション】
最初に、今回作成する「(普通の)ウェブアプリケーション」を定義します。ここでの「ウェブアプリケーション」とは「アプリケーションサーバーとデータベースサーバーから構成」されていて、「アプリケーションサーバーのユーザー画面を通じて、データの保存、読み込み、検索、・・といった処理ができる」ものとします:
2017111702


これを Node-RED で作ります。具体的にはアプリケーションサーバー部分を Node-RED として、外部のデータベースサーバーに対する読み書きを行う API を Node-RED 上に定義します。そしてそれらの API を使う UI となる HTML も Node-RED で作ることで、ウェブアプリケーションとしての機能を実現します。なお今回はデータベースサーバーに IBM Cloudant を使い、全ての機能を IBM Cloud 上に作成することにします:
2017111901


【環境準備】
というわけで、まず Node-RED を用意します。Node-RED 環境が既に手元にある方は読み飛ばしていただいてもいいのですが、この後 IBM Cloudant を利用するので IBM Cloud のアカウントを取得し、IBM Cloudant のサービスインスタンスを用意するようにしてください(まあそこまでやるなら Node-RED も IBM Cloud で作っちゃうのがいいと思います)。

Node-RED はローカルで作ってしまってもいいと思いますが、このあとデータベース連携を行うので、(IBM Bluemix 改め)IBM Cloud の Node-RED 環境を使うことにします。無料のライトプランで契約している場合は Internet of Things Platform Starter ボイラープレートからランタイムを作成してください:
2017111701


ランタイムが稼働するまでしばらく待ちます。このボイラープレートから作成すると Node-RED の(Node.js の)ランタイムに加えて、Cloudant データベースのサービスインスタンスも同時に使えるようになります:
2017111701


実際に Node-RED を使おうとすると、初回のみアクセス用の ID とパスワードの設定を求められます。推測しにくいものを指定して、設定してください:
2017111702


改めて作成したアプリケーションの URL を指定し、Node-RED の画面になったら "Go to your Node-RED flow editor" をクリックしてフローエディタ画面に移動します(この際に認証が求められるので、直前に設定した ID とパスワードを指定します):
2017111703


Node-RED フローエディタ画面が表示されました。IBM Cloud 環境では画面左のパレットリスト内にデータベースや IBM Watson など初めから多くのサービスノードが有効になっているのが便利です(この後実際に使います):
2017111704


なお、Node-RED 環境をローカルで(IBM Cloud を使わずに)用意した場合は別途データベースを用意する必要があります。以下では IBM Cloudant を使う前提で紹介するので、IBM Cloud から Cloudant サービスをインスタンス化してください:
2017112101


【API を作成】
ではここから「普通のウェブアプリケーション」を作っていきます。改めて今回作成するウェブアプリケーションの構成を確認しておきます:
2017111901


今回のアプリケーションでは Node-RED 上に用意する HTML 内のフォームで入力した値を Cloudant に格納し、また格納済みの値を Cloudant から取り出して一覧表示できるようにします。 ということは、今回作成する必要があるのは以下の3つです:
 (1) ユーザー画面の HTML ページ
 (2) (1) 画面で入力した値を Cloudant に保存する API
 (3) (2) で保存した値を Cloudant から一括で取り出す API

この (2) と (3) を最初に作ることにします。

最初に (2) の格納用 API を作ることにします。機能としては((1) で用意する)ユーザー画面で指定した値を受け取ってデータベース(IBM Cloudant)に格納する API です。なので、このような仕様の REST API を作ります:
メソッドパスデータ挙動
POST/myrecordCloudant に保存するデータ(JSON)送信したデータを Cloudant データベースに保存


ではこの API を Node-RED で作ります。Node-RED のキャンバスをクリアします(初期状態で何か配置されているものは全て消します(マウスで選んで DELETE キー))。そして、以下のように3つのノードを配置&接続します:
2017112102


一番左は HTTP Request ノードです(パレット上では "http" と表示されていて、右側だけにジョイントが設定されています)。このノードをダブルクリックして、以下のような属性を指定し、POST /myrecord として動くようにします。最後に「完了」をクリックします:
2017112103


水色のノードは IBM Cloudant out ノードです("cloudant" と表示され、左側にだけジョイントが設置されているものです)。HTTP Request ノードの payload の値を受け取って、そのまま IBM Cloudant に格納する、という目的のノードです。IBM Cloud のボイラープレートから Node-RED 環境を作成した場合はこの部分は自動的にバインド先のサービス名が代入されるはずなので特に変更は不要です。格納先のデータベース名は属性として指定する必要があるので、ダブルクリックしてデータベース名を指定します(以下の例では mydb という名前のデータベースを指定しています)。また msg.payload 内の値だけを格納してほしいので、"Only store msg.payload object?" にチェックを入れておきます:
2017112104


なお、IBM Cloud のボイラープレートを使わずに、IBM Cloudant のインスタンスを別途作った場合は、ここで IBM Cloudant に接続するための username や password も指定する必要があります。


右下にあるのは HTTP Response ノードです。HTTP Request とペアで設定する必要があります。HTTP Request ノードからは IBM Cloudant ノードと HTTP Response ノードに分岐しています。この結果この POST API では HTTP Request ノードに送信されたデータがそのまま HTTP Response として返されると同時に、同じ値が IBM Cloudant の指定データベース(mydb)内に格納される、という挙動になります。ここまでの設定で下のような見た目になります。これだけで (2) のデータ保存用 REST API が出来てしまいました。簡単ですね:
2017112105



では続けて (3) の、データベースから保存されている全レコードを取り出す REST API も追加します。(2) で格納したデータを全て取り出せるように、以下のような API を定義します:
メソッドパスデータ挙動
GET/myrecords(なし)Cloudant データベースから全データを取り出して JSON 配列で返す


先程と同様に、キャンバスに以下のように3つのノードを配置して接続します:
2017112106



一番左は HTTP Request ノードです。このノードをダブルクリックして、以下のような属性を指定し、GET /myrecords として動くようにします:
2017112107


真ん中にあるのは IBM Cloudant in ノードです("cloudant" と表示され、左右両方にジョイントが設置されているものです)。HTTP Request に対して、IBM Cloudant の指定データベースから全レコードを取り出して返すようにするため、ダブルクリックして以下のように属性を指定します(以下の例では mydb という名前のデータベースから全データを取り出す指定をしています):
2017112108


右側にあるのが HTTP Response ノードです。直前の IBM Cloudant in ノードで取得した値を HTTP レスポンスの結果として返します。これで (3) の REST API も完成です:
2017112101


【HTML を作成】
では最後にここまでに作った API を呼び出して使う HTML ページ(利用者向けページ)を作ります。最終的にはそれなりに凝ったものを作りますが、とりあえずは (2) の API の挙動を確認するためのページを作ってみます。キャンバスに以下のように3つのノードを配置して接続します:
2017112102


一番左は HTTP Request ノードです。このノードをダブルクリックして、以下のような属性を指定し、GET /home として動くように(ブラウザから /home にアクセスした時に以下の HTML が表示されるように)します:
2017112103


真ん中は template ノードです。ノードをダブルクリックして以下の HTML 構文を入力します:
2017112104


(テンプレートの入力内容)
<form method="post" action="/myrecord">
Country:<input type="text" name="country" value=""/><br/>
Capital:<input type="text" name="capital" value=""/><br/>
<input type="submit" value="Submit"/>
</form>

↑ country (国名)と capital (首都)を指定して送信ボタンをクリックすると POST /myrecord を呼び出して保存する、という HTML です。


ここまでの作業で3本のフローができました。この状態で右上の「デプロイ」をクリックしてデプロイします:
2017112101


【動作確認】
まずデータを作成する前に IBM Cloud のダッシュボードから、IBM Cloudant のサービスインスタンスを選び、"LAUNCH" ボタンから Cloudant のダッシュボード画面を表示します:
2017112101


IBM Cloudant のダッシュボード画面の左ペインの上から2番目のデータベースアイコンを選択し、現在の Cloudant データベースの一覧を確認します(nodered というデータベースが1つだけ存在していて、この時点では mydb データベースも、その中身のドキュメントも存在していません):
2017112102


ではデータを格納する前に、格納先である mydb データベースを作っておきます。画面右上の "Create Database" をクリックします:
2017112106


データベース作成のダイアログが表示されるので、作成するデータベースの名前(今回の場合は "mydb")を指定して "Create" ボタンをクリックします:
2017112107


すると mydb データベースが作成され、同時に画面は mydb データベースを表示するページに切り替わります(現時点では1つもドキュメントは入っていないので何も表示されません)。元のデータベース一覧に戻るにはデータベース一覧アイコンを再度クリックするか、画面左上の mydb の左にある "<" 印をクリックします:
2017112108


データベース一覧画面に戻りました。今度は nodered データベースに加えて、mydb データベースが追加されていること(そしてドキュメント数がゼロになっていること)が確認できます:
2017112109



改めて先程作成した HTML ページにウェブブラウザでアクセスします。上記では /home というパスに GET リクエストした際に表示される HTML を定義したので、ウェブブラウザで https://(Node-RED のサーバー名)/home にアクセスして、以下のような画面が表示されることを確認します:
2017112103


この Country に国名、Capital にその国の首都を入力して Submit ボタンをクリックします。たとえばこんな感じで:
2017112104


Submit ボタンをクリックすると(実際には POST /myrecord が実行されて)アドレスのパスが /myrecord に変わります。そして画面には POST /myrecord のフローで最後に定義した HTTP Response ノードから結果(送信データを同じもの)が返されます:
2017112105


同時にこのデータは Cloudant out ノードによって Cloudant の mydb データベースに格納されています。Cloudant のデータベース一覧ページをリロードすると、先程まで文書数がゼロだった mydb データベースに1件データが格納されていることが確認できます:
2017112104


具体的なデータの内容を確認するために mydb データベースを選択して中身を見てみましょう。All Documents を Table 状態で確認すると、一件のドキュメントが格納されていて、その capital は Tokyo、country は Japan となっていることが確認できます。期待通りにデータ保存 API が動いていることが確認できました:
2017112102


次にデータ一覧取得 API の挙動を確認します。その前に同様の手順を繰り返して、もう何件かデータを格納しておきます:
2017112103


とりあえず、以下のような5件のデータが入っている状態にしました。このデータを (3) の API で取り出します:
2017112105


ウェブブラウザのアドレス欄に https://(Node-RED のサーバー名)/myrecords と入力してアクセスします。すると (3) で作成した API が実行され、その結果が返ります。以下のように mydb データベース内の5件のドキュメントレコードが JSON 配列になって返されることが確認できます:
2017112106


API が正しく動くことを確認できたので、改めて HTML の見た目を改良します。(1) の(GET /home のフローの) template ノードの属性にて、HTML の内容を以下のように変更して、デプロイします:
<html>
<head>
<title>Countries & Capitals</title>
<script src='//code.jquery.com/jquery-2.2.4.min.js'></script>
<script>
$(function(){
  $('#myform').submit( function(){
    $.ajax({
      type: 'POST',
      url: '/myrecord',
      data: { country: $('#country').val(), capital: $('#capital').val() },
      success: function( data ){
        console.log( data );
        setTimeout( 'list()', 1000 );
      },
      error: function(){
        console.log( 'error' );
      }
    });
    return false;
  });
  list();
});
function list(){
  $('#list').html( '' );
  $.ajax({
    type: 'GET',
    url: '/myrecords',
    success: function( data ){
      if( data && data.length > 0 ){
        for( var i = 0; i < data.length; i ++ ){
          var doc = data[i];
          var tr = '<tr><td>' + doc.country + '</td><td>' + doc.capital + '</td></tr>';
          $('#list').append( tr );
        }
      }
    },
    error: function(){
      console.log( 'error' );
    }
  });
}
</script>
</head>
<body>
  <table border='1'>
  <thead>
  <tr><th>Country</th><th>Capital</th><tr>
  </thead>
  <tbody id='list'>
  </tbody>
</table>
<hr/>

<form method='POST' id='myform'>
  Country: <input type='text' id='country' name='country' value=''/><br/>
  Capital: <input type='text' id='capital' name="capital" value=''/><br/>
  <input type='submit' value='click'/>
</form>
</body>
</html>

具体的には jQuery で AJAX を使ってシングルページの中でデータの追加と一覧表示ができるようにしています。/home にアクセスしてロードすると、まず現在のデータベース内のレコード一覧が表示され、新しいデータを POST すると一覧が同一ページ内で更新されます:
2017112101


※完成形の Node-RED フロー定義をこちらに用意しました。クリップボード経由で読み込むことで再現できると思います:
https://raw.githubusercontent.com/dotnsf/samples/master/nodered_webapp.json




243 ゲームをご存知でしょうか?
2015092901


2048 ゲームは(そこそこ)有名だと思いますが、そのクローンゲームです。2048 ゲームは以下のルールで遊ぶものです:
(1) 矢印キーを使って、画面内のタイルを4方向にスライドさせる
(2) 同じ数字のタイルをぶつけると、数字が倍の1つのタイルができる
(3) ぶつけて出来上がったタイルの数字が得点になる(2048 タイルを作ることが1つの目標)


243 は 2048 と異なり、3つの同じ数字を重ねることで1つにまとまります。2048 よりは難易度高くなっています。具体的なルールは以下になります:
(1) 矢印キーを使って、画面内のタイルを4方向にスライドさせる
(2) 同じ数字のタイルを3つぶつけると、数字が3倍の1つのタイルができる
(3) ぶつけて出来上がったタイルの数字が得点になる(243 タイルを作ることが1つの目標)



で、このゲームの HTML5 が Github 上で公開されていました。これを使って IBM Bluemix 上で 243 ゲーム環境を作ってみます(まあ、特にトリッキーなこともないんですが・・)。

まず Bluemix 上に 243 ゲームが動くランタイムを作成します。サーバーサイドで動くモジュールがないので、Github の staticfile ビルドパックを使っても動くと思いますが、Bluemix ダッシュボードから PHP ランタイムを選んでも(事実上 PHP を使わないので、かなり贅沢な環境ですが)動きます。今回は PHP ランタイムを使う方向でいきましょう:
2015092902


並行して 243 ゲームの zip モジュールを GitHub からダウンロードします:
2015092903


ダウンロードした zip モジュールを展開します。index.html が存在するディレクトリがドキュメントルートになります:
2015092904


ここまでできたら、cf コマンドラインツールを使って、このドキュメントルートを PHP ランタイムにプッシュするだけ、です:
# cd (展開した index.html があるディレクトリ) ドキュメントルートへ移動
# cf login -a https://api.ng.bluemix.net/    cf で Bluemix にログイン(米国DCの場合)
# cf push (PHP ランタイムの名前)        cf で Bluemix に 243 ゲームをプッシュ

プッシュが成功したら、PHP ランタイムの URL にブラウザからアクセスすると 243 ゲームが始まります:
2015092905


ぎゃー、惜しかったけど無理。難しい!!

僕は1994年に新卒入社しました。Windows95 では搭載PCに初めからモデムが内蔵されていることが多くなり、インターネットのダイアルアップ接続も標準機能になったので楽でしたが、その Win95 がリリースされる前年の、まだ「ダイアルアップ接続するにもモデムを買って、RS232Cで外付けして、フリーソフトをダウンロードして、インストールして、TCP/IP の設定をして・・・」という手続きが必要だった時代です。

・・・と、ここまで書いて「ダイアルアップ接続って何?」と聞かれないか不安になってきました。分からない人はお父さんに聞いてみましょう。 

そんな時代、最初に使ったインターネットブラウザは NCSA Mosaic でした。「HTMLで記述されたウェブページの閲覧ができて、画像まで表示できる!」という、今では普通に行っている作業が 1993 年にできるようになりました。

その NCSA Mosaic の開発に携わったマーク・アンドリーセンが独立して再開発したウェブブラウザが Netscape Navigator(Communicator) でした。Mosaic と比べて圧倒的に速い動作速度に加えて、Java アプレットやJavaScript エンジン、クッキー、フレームといった今でも利用されている機能をいち早く搭載したブラウザでした(1994年)。
svg


このリリースがちょうど自分の新卒入社の時期とも重なり、また運良くそういった関係の業務に新人時代から携わることも出来て、自分は Netscape Navigator でインターネットを使い始めた、という印象が強く残っています。後にリリースされるバージョン 3.0 では電子メールアプリと HTML の WYSIWYG エディタまで内蔵し、Mosaic やまだ機能が未熟だった Internet Explorer との比較で当時のシェアは 70% と言われており、事実上の標準ブラウザでした。


Netscape Navigator を開発していた Netscape Communications 社は後に当時の AOL(現在のタイム・ワーナー)に買収され、Mozilla としてオープンソース化され、その遺伝子の一部は FireFox へと引き継がれていきますが、Netscape Navigator としての進化は完全に終わっていて、歴史の一部になっています。事実、現在では「Netscape Navigator を使ったことのない人」の方が多数派ではないかと思っています。


そんな Netscape Navigator ですが、まだダウンロードすることはできるようです:
http://www.kotalog.net/archives/1601

最終版となった 9.0.0.6 を使ってみました。いつの間にか CSS もサポートされていて、拙作「マンホールマップ」も、ある程度は見れる感じで嬉しい。まあ比較的新しめのライブラリとか、あまり使ってないせいもあるだろうな:
netscape
(画面は Linux 版)

一方、もう一つの拙作で jQuery Mobile 使いまくりの「ねっぴ」の方は画面崩れまくり。jQuery Mobile にはさすがに未対応と見た:
netscape1


この右上の "N" マークが懐かしいなあ・・・

 

ウェブ制作において、画像やサイトの一部を別モーダルウィンドウでプレビューのように表示する、通称「ライトボックス」と呼ばれる機能があります。 具体的にはこんな感じのものです:

マンホールマップ  ←クリックするとライトボックス内でマンホールマップを表示します

↑CSS が適当なので少し見にくいかも。あとスマホだとうまく表示されないかもしれません・・・


元のページに戻ってくることを前提に、違うページの情報を一瞬表示させる、という目的で便利に使える機能です。Facebook などでも写真を表示する時に使われてますね。こういった機能を実現するための JavaScript ライブラリも多く公開されています(ちなみに上記機能は Colorbox というライブラリを使って実現しています)。

Colorbox で iframe を使った簡単な実装例を紹介します。上記ページから js と css をダウンロードし、jQuery と合わせて <head> 内に以下のような記述をしておきます:
<link  href="css/colorbox.css" rel="stylesheet">
<script type="text/javascript" src="js/jquery.js"></script>
<script type="text/javascript" src="js/jquery.colorbox-min.js"></script>
<script type="text/javascript">
$(function(){
	$('.box').colorbox( { iframe:true, width:"90%", height:"90%" } );
});
</script>

この例では box というクラスが付与されたリンクに対してライトボックスを実行するような記述になっています。 
その上で以下のような <a> リンクを用意、これだけです:
<a class="box" href="xxx">XXXX</a>


ところが、このライトボックスが使えないページ(ライトボックスの中身として表示できないページ)、というのがたまに存在しています。例えばグーグルのトップページ(http://www.google.co.jp/)です:

Google  ←クリックするとダイアログは表れますが、中に何も表示されません


実は同じようなページは他にもあります。僕が知る限りではアマゾン価格コムのページもライトボックスでは表示されません。

この違いは何でしょうか? 実はページを HTTP リクエストで取得する際のレスポンスヘッダ X-Frame-Optionsによって、そのコンテンツが iframe の内部に表示することを許可するかどうかを制御できます。上記の挙動の違いはこのレスポンスヘッダ X-Frame-Options の値によるものでした。
2014040600


デフォルト値は「なし」です。その場合(つまり特に何もしない場合)、そのページは iframe 内に表示可能です。一方この値が DENY に設定されていると、そのページを iframe 内に表示することはできません。また SAMEORIGIN(生成元と同じフレーム内に限り表示可)や、ALLOW-FROM xxx(生成元が xxx の場合に限り表示可)といった設定も可能です。詳しくはこちらを参照してください。
2014040601


こういう制限をかける側としてはそれなりの意図があって制限しているんでしょうけど、やはり表示できるものなら表示したい、という作成側の意図もあるわけです。また AJAX のクロスドメイン制限にも言えることですが、制限自体は JavaScript での挙動や iframe 内での挙動に制限をかけているだけであって、情報の取得そのものを制限しているわけではないので、回避することもできます。 で、今日はその(X-Frame-Options 制限の)回避に挑戦してみます。

考え方としては iframe での表示に制限をかけられているサイトに対して、Java や PHP などのサーバーサイドプログラミングによって情報を取得(これらには制限がないので可能)し、その結果を iframe 内に入れてしまおう、というものです。まあクロスドメイン対策と同じ考え方です。
2014040602


ということで、上記の紫色の部分を別途作成する(加えて、それをどこかのサーバーに設置しておく)必要があるわけです。この部分は例えば Java のサーブレットであれば、おおまかにはこんな感じになるでしょうかね:
public class SpoofXFrameOptionsServlet extends HttpServlet {
  public void doGet( HttpServletRequest req, HttpServletResponse res ) throws IOException{
    req.setCharacterEncoding( "UTF-8" );

    String html = "";
    String url = "", _url = req.getParameter( "url" );
    if( _url != null && _url.length() > 0 ) url = _url;
    if( url != null && url.length() > 0 ){
      try{
        GetMethod method = new GetMethod( url );
        HttpClient client = new HttpClient();
        int sc = client.executeMethod( method );
        html = method.getResponseBodyAsString();
      }catch( Exception e ){
        e.printStackTrace();
      }
    }

    res.setContentType( "text/html; charset=UTF-8" );
    res.setCharacterEncoding( "UTF-8" );
    res.getWriter().println( html );
}

このサーブレットが http://xxxx/spoofxframeoptions という名前でアクセスできるようになっていたとすると、
http://xxxx/spoofxframeoptions?url=(iframe 内に入れたいページのURL)

のように指定することで、目的のページのコンテンツを取得できることになります。 なので、この上記の URL を iframe 内で指定すればいい、ということになります。
<iframe class="box" href="http://xxxx/spoofxframeoptions?url=(iframe 内に入れたいページのURL)"></iframe>

で、これを実際にやってみたのがこちらのリンクです:
Google  ←ライトボックス内でGoogleトップページが表示できるようになりました

厳密には上記のサーブレットのサンプルはまだ不完全で、実際には文字コードを考慮したり、HTML 内の <base href="..."> 要素を追加する必要があったり、ページによっては Referer を指定しないと取得できなかったり、、、といった細かな修正が必要になるのですが、とりあえず X-Frame-Options の制約を回避する手段としては使えそうですね。


 

このページのトップヘ