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

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

タグ:node

「エクセルファイルを扱えるライブラリ」といえば、Java であれば Apache POI などがありますが、Node.js ではどうだろう?? と思って調べてみました。その名もズバリの xlsx という npm ライブラリを見つけたので使ってみました:

npm - xlsx
https://www.npmjs.com/package/xlsx

2018080100


ライブラリ名は xlsx ですが、対応フォーマットは xls や XML に加えて ODS まで含まれていて、かなり柔軟に使えそうです。


【扱うサンプル】
こんな感じのエクセルファイルを用意して使うことにします:
2018080101


データとしては "A1:C14" の範囲にまとまっていて、その右に2軸の折れ線グラフが1つあります。この表の B14 セルは B2:B13 の合計(SUM)、C14 セルは C2:C13 の平均値(AVERAGE)がマクロで定義されています。まあ「よくあるシート」だと思っていますが、このエクセルファイルを xlsx で扱ってみます。ちなみに同じファイルがこちらからダウンロードできます:


【読み込み例】
まず npm で xlsx ライブラリをインストールします:
$ npm install xlsx

そして xlsx ライブラリを使ってエクセルファイルを読み込む Node.js コードを作成します。この例ではファイル名を指定して読み込み、"Sheet1" シートを取得して console.log() で出力しています:
var XLSX = require( 'xlsx' );

// ファイル読み込み
var book = XLSX.readFile( './SalesSample.xls' );

// シート
var sheet1 = book.Sheets["Sheet1"];
console.log( sheet1 );


実行結果はこんな感じになります。ダンプされたような感じです:
{ '!margins':
   { left: 0.7,
     right: 0.7,
     top: 0.75,
     bottom: 0.75,
     header: 0.3,
     footer: 0.3 },
  B1: { v: '売上', t: 's', w: '売上' },
  C1: { v: '前年比', t: 's', w: '前年比' },
  A2: { v: 1, t: 'n', w: '1' },
  B2: { v: 7370, t: 'n', w: '7370' },
  C2: { v: 0.87, t: 'n', w: '0.87' },
  A3: { v: 2, t: 'n', w: '2' },
   :
   :
  A12: { v: 11, t: 'n', w: '11' },
  B12: { v: 24380, t: 'n', w: '24380' },
  C12: { v: 0.812, t: 'n', w: '0.812' },
  A13: { v: 12, t: 'n', w: '12' },
  B13: { v: 28283, t: 'n', w: '28283' },
  C13: { v: 0.814, t: 'n', w: '0.814' },
  B14: { v: 156518, t: 'n', f: 'SUM(B2:B13)', w: '156518' },
  C14:
   { v: 0.8603333333333333,
     t: 'n',
     f: 'AVERAGE(C2:C13)',
     w: '0.860333333' },
  '!protect': false,
  '!ref': 'A1:C14',
  '!objects': [ , , , , { cmo: [Object], ft: [Object] } ] }

この結果の見方を少し説明します。例えば sheet1["!ref"] の値は 'A1:C14' となっていて、このシートの中で有効なセルとして認識されている範囲は A1:C14 とされています。つまりグラフ部分は完全に無視されていて、このライブラリでは現在は扱えない情報ということになります。xlsx はあくまで表部分の読み書きを対象としています。

次に sheet1["C2"] の値は { v: 0.87, t: 'n', w: '0.87' } となっています。この意味は以下のようになります:
 v: 0.87(数値としての値は 0.87)
 t: 'n'(数値のセルとして認識されている)
 w: '0.87'(表示されているテキストは '0.87')

なお、t の値は以下のいずれかになります:
 b: Boolean
 n: 数値
 d: 日付時刻
 s: 文字列
 z: スタブ
 e: エラー

したがって sheet1["C2"] の { v: 0.87, t: 'n', w: '0.87' } の意味は「値が 0.87 の数値セルで、画面上では '0.87' と表示されている」ということになります。

また sheet1["B14"] の値は { v: 156518, t: 'n', f: 'SUM(B2:B13)', w: '156518' } となっています。この中の f: 'SUM(B2:B13)' は値が数式で定義されていて、その式が SUM(B2:B13) であることを意味しています(sheet1["C14"] も同様です)。表としてはこのレベルで各セルの値を取得できている、ということがわかります。

この読み込み例のサンプル(app1.js)はこちらからダウンロードできます。こちらのファイルは(後述の書き込み機能の結果を確認できるように)上記のコードに少し機能を追加していて、コマンドラインパラメータで読み込むエクセルファイルを指定可能にしています(無指定の場合は SalesSample.xls を読み込みます):
$ node app1.js (xxx.xls)


【書き込み例】
xlsx ライブラリを使ってファイルの書き込みを行うサンプルです。この例では SalesSample.xls を読み込んだあとに "C13" セルの値を上書きして、SalesSample2.xlsx というファイル名で保存しています:
var XLSX = require( 'xlsx' );


// ファイル読み込み
var book = XLSX.readFile( './SalesSample.xls' );

// シート
var sheet1 = book.Sheets["Sheet1"];
console.log( sheet1 );

// セル更新
sheet1["C13"] = { v: 1.01, t: 'n', w: '1.01' };

// シート更新
book.Sheets["Sheet1"] = sheet1;

// ファイル書き込み
XLSX.writeFile( book, './SalesSample2.xlsx', { type: 'xlsx' } );


(注1 最後の XLSX.writeFile() 実行時の最後のオプション { type: 'xlsx' } を指定しないとマクロ関数が無効な状態で保存されてしまいます)

この書き込みのサンプル(app2.js)はこちらからダウンロードできます。実行はそのまま node コマンドで実行します:
$ node app2.js

実行すると SalesSample2.xlsx というファイルが出来ているはずです。試しにこのファイルをエクセルで開いてみるとこのようになります:
2018080201



"C13" セルの値は { v: 1.01, t: 'n', w: '1.01' } に上書きしましたが、たしかに 0.814 から 1.01 に更新されています。同時に "C14" セルの値も(AVERAGE関数が再計算されて)変わっています。一方でグラフが完全に消えてしまいました。まあ読み込みを実行した時点でグラフの情報は消えていたので、その内容で保存するとこのようになってしまうのだと思います。xlsx ライブラリの制限事項になると思いますが、実際に使う際にはご注意ください。

(注2 厳密には SalesSample2.xlsx ファイルが生成された時点では C14 セルの値は変わっていませんが、このファイルをエクセルで開くと AVERAGE 関数が再計算されて開くので、そこで値が正しく変わったように見えます)


以上、詳細は本家のドキュメントを参照いただきたいのですが、少なくともグラフを操作せずにシートの中身を取り出す用途であれば充分につかえて、対応フォーマットも多そうだな、、という印象を持っています。自然言語処理機械学習の学習データとしてエクセル資産を活用する、なんて話になった時に活躍できそうなライブラリですね。



以前に express-ipfilter ライブラリを使って、Node.js アプリの IP アドレスフィルタリングを行うサンプルを紹介しました:
http://dotnsf.blog.jp/archives/1066182158.html

↑ここで紹介したサンプルは一応動くものですが、アプリケーションを IBM Cloud の Cloud Foundry アプリとしてデプロイすると( IP アドレスフィルタリングが)正しく動かないことがわかりました。原因は Cloud Foundry 内のルーティングで x-forwarded-for ヘッダの情報が変わってしまい、正しい IP アドレスを取得できなくなってしまうようでした。

IBM Cloud の Cloud Foundry 環境でもこの IP アドレスフィルタリングを有効にするには、フィルタリングを行う前に Express() の use メソッドを使って、
app.use( 'trust proxy', true );

を呼び出してからフィルタリングを行う必要があります。

(解説)
http://expressjs.com/ja/api.html



 

Node.js を使っていて 404 エラーや 500 エラーなどが発生した場合に表示されるエラーページをカスタマイズできないか、と思って挑戦してみました。

今回挑戦した環境では Node.js にフレームワークの Express と、テンプレートエンジンに EJS を使いました。エラーページを EJS で作ることを想定しています。

まず、JavaScript のコードはこんな感じです:

app.js
// app.js
var express = require( 'express' );
var app = express();

//. EJS テンプレートエンジン
app.set( 'views', __dirname + '/templates' );
app.set( 'view engine', 'ejs' );

//. / へのアクセスは正常にできる
app.get( '/', function( req, res ){
  res.render( 'index', {} );
});

//. /err へのアクセスは 500 エラーとする
app.get( '/err', function( req, res ){
  res.render( 'index', { value: novalue } ); //. novalue 変数が未定義なので 500 エラーが発生する
});

//. 有効なルーティングを上記に記述
//. /, /err 以外のパスは 404 エラー

//. 404 エラーが発生した場合、
app.use( function( req, res, next ){
  res.status( 404 ); //. 404 エラー
  res.render( 'err404', { path: req.path } ); //. 404 エラーが発生したパスをパラメータとして渡す
});

//. 500 エラーが発生した場合、
app.use( function( err, req, res, next ){
  res.status( 500 ); //. 500 エラー
  res.render( 'err500', { error: err } ); //. 500 エラーの内容をパラメータとして渡す
});

var port = 3000;
app.listen( port );
console.log( 'server started on ' + port );

上記を補足すると、Express でルーティングを2つ定義しています。1つめがドキュメントルート(/)へのアクセスで、この場合は正常な処理として(後述の)index.ejs を表示します。

2つめは /err へのアクセスで、こちらの場合はわざと 500 エラーを発生させています(index.ejs でページを表示させるような記述にしていますが、実際にはこの中で使われている novalue という変数が未定義なので、そこで 500 エラーが発生するようにしています)。

ルーティングとしてはこの2つ(/ と /err)だけを定義しているので、これら以外のパス(例えば /home)にアクセスがあった場合は 404 エラーが発生します。404 エラーが発生した場合の処理としては err404.ejs というテンプレートを使って、アクセスしたパス(/home)が存在していない、という旨のエラーページを表示します。

最後に 500 エラーが発生した場合の処理を定義しています。ここでは err500.ejs というテンプレートを使って、エラーの内容をあわせて表示します。上記で /err ページにアクセスすると 500 エラーが発生するので、この err500.ejs のページが表示されることになります。


次に上記で使うことにした3種類のテンプレート(index.ejs, err404.ejs, err500.ejs)を用意します。それぞれ以下のような内容にしました:

index.ejs
<html>
<head>
<title>index</title>
</head>
<body>
<h1>index</h1>
</body>
</html>
↑単に index と表示されるだけのシンプルなページ


err404.ejs
<html>
<head>
<title>err404</title>
</head>
<body>
<h1>err404</h1>
<%= path %> is not defined nor found.
</body>
</html>
↑ "(エラーがあったパス)is not defined nor found." というエラーメッセージが表示されるページ


err500.ejs
<html>
<head>
<title>err500</title>
</head>
<body>
<h1>err500</h1>
Error: <%= error %>
</body>
</html>
↑ "Error: (エラー内容)" というエラーメッセージが表示されるページ


app.js ではこれらのテンプレートファイルが templates/ フォルダに存在していることを前提に参照するようにしています。したがって templates/ フォルダを作って、その中にこれら3つの .ejs ファイルを格納しておきます。これで準備OK。


実際に動かして試してみました。まずは正常系、/ へのアクセスは普通に問題なくできます:
2018070401


次に /home にアクセスして、わざと 404 エラーを発生させてみた時の画面です。err404.ejs で定義されたテンプレートを使って期待通りにエラーページが表示されています:
2018070402


最後に /err にアクセスして、わざと 500 エラーを発生させてみた時の画面です。こちらも err500.ejs で定義されたテンプレートを使って期待通りにエラー内容が表示されています:
2018070403


上記コードのサンプルをこちらに用意しました:
https://github.com/dotnsf/nodejserror


これで Node.js でもカスタムエラーページを作れることがわかりました。

Node.js + Express で作成する Web アプリケーションでセッション認証に対応したアプリケーションを作ってみます。今回はその実装のために Express Session パッケージJWT(Json Web Token)パッケージを使った例を紹介します。


【設計】
最初に、以下で紹介するソースコード(の最新版)をこちらで公開しています。よろしければこちらを参照/クローンして使ってください:
https://github.com/dotnsf/jwtsample


まず大まかな仕組みは以下のようにします:
・Node.js と Express で必要な REST API を用意する。今回は POST /login(ログイン)、GET /logout(ログアウト)、GET /items(一覧取得)、そして POST /item(新規作成)の4つの REST API を用意します。
・POST /item は認証済みのユーザーからでないと実行できないようにします。他の3つは認証前でも実行できるものとします。

(注 本来ログアウトは GET 以外のメソッドで実装するべきですが、今回はサンプルをシンプルにしたので URL のみで実行できるよう GET で実装しています。実際のアプリを作る際には POST で実装してください)


その上で、次のような流れを実現できるようにします:
(1) ユーザー ID とパスワードで認証(ログイン)する
(2) 正しい ID とパスワードが指定された場合はそのユーザーオブジェクトで JWT を作成し、セッションに格納する
(3) 新規作成の API 実行時には新たに ID とパスワードを要求するのではなく、セッションに正しいオブジェクトが存在している場合のみ実行を許可する。またこの際、セッション内のオブジェクトからユーザー名をデコードし、タイムスタンプと合わせて作成データに含める(この API をいつどのログインユーザーが実行したのか、の情報を作成データに含める)
(4) ログアウトの API 実行時にセッションをリセットする。そのため再度ログインしないと新規作成の API は実行できなくなる


上記流れの中で3箇所セッションを操作しています。逆にセッションの操作だけでログインの確認や実行権限の有無を判断できるようにしています。

で、この流れを実装すべく、以下の(単なるページロードも含めて) 6つの API を用意することにします:
#メソッドパス目的
1GET/(index.html)インデックスページのロード
2GET/login.htmlログインページのロード
3GET/logoutログアウトし、インデックページをロード
4POST/loginID とパスワードでログイン
5GET/items全アイテムのリストを取得
6POST/item新たにアイテムを作成し、リストに追加


【実装】
上記設計を実装する上で2つの静的ページ index.html と login.html が必要です。前者は一覧を表示した上で新規作成も行うページで、後者はログインのページです。まずこれらを先に作っておきます。

まずは 1. index.html 。こちらは 5. で作成する GET /items の API を使って全アイテムの一覧を取得して表示する機能と、6. の POST /item を使って新規にアイテムを作成する機能を作ります。今回は1つの表の中で一覧表示と、その最下行から新規作成できるような表を jQuery を併用して作ってみました:
<html>
<head>
<meta charset="utf8"/>
<script type="text/javascript" src="//code.jquery.com/jquery-2.0.3.min.js"></script>
<link href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet"/>
<script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
<title>LIST</title>
<script>
$(function(){
  getItems();
});
function getItems(){
  $('#items_table_tbody').html( '' );
  $.ajax({
    type: 'GET',
    url: '/items',
    success: function( items ){
      console.log( items );
      items.forEach( function( item ){
        var tr = '<tr><td>' + item.name + '</td><td>' + item.price + '</td><td>' + item.user.id + '</td></tr>';
        $('#items_table_tbody').append( tr );
      });
      var tr = "<tr>"
        + "<td><input type='text' id='name' placeholder='name'/></td>"
        + "<td><input type='text' id='price' placeholder='price'/></td>"
        + "<td><input type='button' class='btn btn-primary' value='Add' onClick='addItem();'/></td>"
        + "</tr>"
      $('#items_table_tbody').append( tr );
    },
    error: function( err ){
      console.log( err );
    }
  });
}
function addItem(){
  var name = $('#name').val();
  var price = parseInt( $('#price').val() );
  $.ajax({
    type: 'POST',
    url: '/item',
    data: { name: name, price: price },
    success: function( data ){
      console.log( data );
      getItems();
    },
    error: function( jqXHR, textStatus, errorThrown ){
      console.log( textStatus + ":" + errorThrown );
    }
  });
}
</script>
</head>
<body>

<div class="navbar navbar-default">
  <div class="container">
    <div class="navbar-header">
      <a href="/" class="navbar-brand">Items</a>
      <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
      </button>
    </div>
    <div class="collapse navbar-collapse target">
      <ul class="nav navbar-nav navbar-right" id="navbar">
      </ul>
    </div>
  </div>
</div>

<div class="container" style="padding:20px 0; font-size:8px;">
  <table class="table table-hover table-bordered" id="documents_table">
    <thead class="table-inverse">
      <tr>
      <th>name</th>
      <th>price</th>
      <th>user</th>
      </tr>
    </thead>
    <tbody id="items_table_tbody">
    </tbody>
  </table>
</div>

</body>
</html>




次に 2. login.html 、こちらは id と password を入力して /login に POST するだけのシンプルなページです:
<html>
<head>
<meta charset="utf8"/>
<script type="text/javascript" src="//code.jquery.com/jquery-2.0.3.min.js"></script>
<link href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet"/>
<script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
<title>Login</title>
</head>
<body>
<div class="row title">
  <div class="container">
    <div class="col-md-4 col-md-offset-4">
    <form id="loginform" class="form-signin" method="POST" action="/login">
    <h2>Login</h2>
    <input type="text" class="form-control clear" id="id" name="id" placeholder="user id" required="" autofocus=""/><br/>
    <input type="password" class="form-control clear" id="password" name="password" placeholder="password" required=""/><br/>
    <button class="btn btn-lg btn-primary btn-block" type="submit">Login</button>
    </form>
  </div>
</div>
</body>
</html>


そして、3. ~ 6. は Node.js で実装します:
// app.js

var express = require( 'express' );
var bodyParser = require( 'body-parser' );
var jwt = require( 'jsonwebtoken' );
var session = require( 'express-session' );
var app = express();

var superSecret = 'weloveibmcloud';  //. 適当に変更してください

app.set( 'superSecret', superSecret );
app.use( express.static( __dirname + '/public' ) );  //. 静的コンテンツは /public 以下
app.use( bodyParser.urlencoded() );
app.use( bodyParser.json() );

//. Express-Session の設定
app.use( session({
  secret: superSecret,
  resave: false,
  saveUninitialized: false,
  cookie: {
    httpOnly: true,
    secure: false,           //. https で使う場合は true
    maxage: 1000 * 60 * 60   //. 60min
  }
}) );

//. ログイン処理(4.)
app.post( '/login', function( req, res ){
  res.contentType( 'application/json' );
  var id = req.body.id;
  var password = req.body.password;

  /*
   * 本来はユーザーディレクトリやマスター DB などを参照して id & password が正しいかどうかを判断する。
   * 今回は簡易的に id と password の中身が一致している場合はその id でログイン成功したものとする
   */
  if( id && id == password ){
    //. ログイン成功
    var user = { id: id, password: password };  //. トークンの素になるオブジェクト
    var token = jwt.sign( user, superSecret, { expiresIn: '25h' } );
    req.session.token = token; //. セッションに記錄

    res.redirect( '/' );
  }else{
    //. ログイン失敗
    res.redirect( '/login.html' );
  }
});

//. ログアウト(3.)
app.get( '/logout', function( req, res ){
  req.session.token = null; //. セッションをリセット
  res.redirect( '/' );
});

var items = [];  //. データ一覧(本来はデータベースなどで管理するもの、今回はメモリ処理だけで実装)

//. データ一覧取得(5.)
app.get( '/items', function( req, res ){
  res.contentType( 'application/json; charset=utf-8' );
  res.write( JSON.stringify( items, 2, null ) );
  res.end();
});

//. データ新規作成(6.)
app.post( '/item', function( req, res ){
  res.contentType( 'application/json; charset=utf-8' );
  var token = ( req.session && req.session.token ) ? req.session.token : null;
  if( !token ){
    res.status( 401 );
    res.write( JSON.stringify( { status: false, result: 'No token provided.' }, 2, null ) );
    res.end();
  }else{
    //. トークンをデコードして、ログイン時のユーザーオブジェクトを取りだす
    jwt.verify( token, app.get( 'superSecret' ), function( err, user ){
      if( err ){
        res.status( 401 );
        res.write( JSON.stringify( { status: false, result: 'Invalid token.' }, 2, null ) );
        res.end();
      }else{
        var item = req.body;                        //. ポストされたデータ
        item.user = user;                           //. セッションのユーザー情報をデータに含める
        item.timestamp = ( new Date() ).getTime();  //. タイムスタンプをデータに含める

        items.push( item );

        res.write( JSON.stringify( { status: true, item: item }, 2, null ) );
        res.end();
      }
    });
  }
});


var port = 3000;
app.listen( port );
console.log( 'server started on ' + port );

なお、上記の 3. ログイン処理と 4. ログアウト処理は(今回アプリケーション側を複雑にしたくなかったので)セッションの値を処理した後に redirect メソッドで直接ページを切り替えるようにしています。本来は他の処理と同様に結果を JSON で返す形にするのがいいと思っています。

個人的に肝になると思われる箇所を赤字にしています。まず今回はトークンの仕組みとして Json Web Token を利用しており、またそのトークンをセッションで管理するため、これら2つのパッケージをロードしています:
    :
var jwt = require( 'jsonwebtoken' );
var session = require( 'express-session' );
    :

4. ログイン処理(POST /login )時は本来ならば既存のユーザーディレクトリなどを参照して認証を行います。今回のサンプルではユーザーディレクトリを使わずに「入力した ID とパスワードが一致していれば正しい認証が行われた」とみなしています。この辺りは実装時に少し気をつけてください。

そして正しい認証が行われた後にその情報を使ってオブジェクトを作り、そのオブジェクトを JWT でトークン化して、セッションに記録するように処理しています:
    :
    //. ログイン成功
    var user = { id: id, password: password };  //. トークンの素になるオブジェクト
    var token = jwt.sign( user, superSecret, { expiresIn: '25h' } );
    req.session.token = token; //. セッションに記錄
    :

一方、3. ログアウト処理ではセッションの内容をクリアするだけでログイン情報が消えることになります:
    :
  req.session.token = null; //. セッションをリセット
    :

データは本来はデータベースなどに保存するべきものですが、今回は簡易的にメモリ内の items 変数で管理しています(そのためアプリケーションサーバーを再起動すると中身がリセットされます)。そして 5. データの一覧取得ではその items 変数をそのまま JSON 配列で返すようにしています。なお、このメソッドはトークンの有無に関係なく実行可能です。

そして 6. データの新規作成時にはまずセッション内のトークンの有無を確認します。トークンがあればログイン済み、なければログイン前と判断します:
    :
var token = ( req.session && req.session.token ) ? req.session.token : null;
    :

トークンが存在していた場合、そのトークンをデコード(verify)することで、トークン化前のユーザーオブジェクトを取り出すことができます:
    :
    //. トークンをデコードして、ログイン時のユーザーオブジェクトを取りだす
    jwt.verify( token, app.get( 'superSecret' ), function( err, user ){
    :

そして取り出したユーザーオブジェクトを使って、送信データ(req.body)に加えて、ユーザー情報とタイムスタンプを追加して1つのデータオブジェクトを生成しています。これを items 配列に加える形で一覧に追加しています:
          :
        var item = req.body;                        //. ポストされたデータ
        item.user = user;                           //. セッションのユーザー情報をデータに含める
        item.timestamp = ( new Date() ).getTime();  //. タイムスタンプをデータに含める
          :

これによって、5. のデータ一覧で取り出すデータにはデータを作成したユーザーの情報やそのタイムスタンプも合わせて含まれるように実現しています。

こうして作成した app.js と index.html、login.html を使ってアプリケーションを動かします。なお、実際に動かす場合は2つの HTML ファイル(index.html と login.html)を public/ フォルダ内に格納しておきます(app.js で静的コンテンツファイルは public/ フォルダ内にある、と定義しています)。


【動作確認】
Node.js で app.js を実行します:
$ node app

今回の app.js では 3000 番ポートでアプリケーションが稼働します。環境によって 3000 番ポートが使えない場合は app.js 内の該当箇所を編集してください。

ウェブブラウザで該当サーバーの 3000 番ポートを指定(例: http://localhost:3000/)してアクセスします。挙動を確認しやすくするため、開発コンソールも開いておきます。最初のロード時には index.html が表示されます。この中で GET /items が実行されており、その取得結果が開発コンソールに表示されています(最初は中身が存在しないので、空配列になっているはずです):
2018062801


まだログインしていないのでデータの作成はできないのですが、作成できないことを確認してみます。name と price に適当な商品名とその価格を入力し、Add ボタンをクリックします:
2018062802


すると開発コンソールに "error:Unauthorized" というメッセージが表示されます。「まだログインしてない」というエラーです。この表示がでればとりあえず未認証ではデータを作成できない、ということが確認できます:
2018062803


ではログインしてみます。ブラウザの URL パスを /login.html に指定して(例: http://localhost:3000/login.html)アクセスするとログイン画面が表示されます:
2018062804


上述のように今回のサンプルでは id とパスワードに同じものが指定された場合はログイン成功とみなすことにしているので、最初はわざと異なる値(例えば id: user1、password: user)を入力して "Login" ボタンをクリックしてみます。正しく動くと(ログインできないので)元の画面に戻ってきてしまいます:
2018062805


改めて id とパスワード欄両方に "user1" と入力して "Login" ボタンをクリックします。この場合は認証の条件を満たしているのでログインに成功し、元の一覧ページに移動します:
2018062806


外見上の違いがなくわかりにくいのですが、今回はログイン済みの状態で一覧ページを見ています(ちゃんと作る場合はログイン有無で外見上の違いもあるといいと思います)。改めて name と price を入力して "Add" ボタンをクリックします:
2018062807


さっきはログイン前だったので "Unauthorized" エラーになりましたが、今回はログイン済みなのでデータの新規作成に成功します。そして GET /items が実行された結果が開発コンソールに表示され、先程まで空配列だったところにデータが1つはいった配列が表示されていることがわかります。また画面にも入力したデータがテーブル表示されるようになり、そこには(入力していない)ユーザー名も表示されています。先程のログインの記録がセッションに残っており、その情報を使って API が実行されていることがわかります:
2018062808


もう1つデータを追加すると画面では2行に表示され、また開発コンソールにも2つのデータを持った配列が表示されます:
2018062809


ここでいったんログアウトしてみます。URL パスに /logout (例: http://localhost:3000/logout)を指定してアクセスしログアウト処理を実行します。画面はそのまま元の一覧ページに戻りますが、今度はログインされていない状態で表示されています。そのため3つ目のデータを作成しようと "Add" ボタンをクリックしても最初と同様に "error:Unauthorized" エラーが表示されてしまいます:
2018062810



と、まあこんな感じです。JWT でログイン情報をトークン化してセッションで保持/消去を管理することで、このようなセッション認証対応アプリを作ることができるようになります。

見栄えとかリンクとか、本来はもう少し凝った UI を考えるべきなのでしょうが、今回の話の中では本質的でないので省略して手抜き簡易化しています。ごめんなさい。



たまに子供や初心者を相手にプログラミングを教えてみせたりする際に Scratch を使っています:
Screenshot from 2018-05-04 15-28-23


MIT メディアラボが開発した GUI によるプログラミング開発言語および実行環境です。プログラムをエディタでガリガリと記述するような、いわゆる「コーディング」に相当する作業があまり必要なく、GUI を中心とした見た目に対する処理や変更をイベント単位に足していきながら少しずつ動くものを作っていくものです。そのためユーザーが完成形をイメージしながら作っていくことができ、コーディングそのものに不慣れな人がプログラミングを理解するのに向いていると思っています。

この Scratch は良くも悪くも GUI 中心の作業になるため、他の環境との連携が難しい(というか、そもそもどういう外部インターフェースが用意されているのかわかっていなかった)という印象を持っていました。Scratch の閉じた世界の中では初心者にも簡単でわかりやすい、という面があるのと同時に、Scratch 以外の環境とは繋がりにくいと思っていました。

で、なんかインターフェースが公開されてないかなあ・・・ と思って探していたら、この Scratch の国内第一人者でもある石原さんが書かれた一連のブログを見つけました:
Scratch(スクラッチ)を外部のプログラムなどとつなぐ「遠隔センサー接続」を解説する(その1)


↑このブログで書かれている内容は Scratch 1.4 の「遠隔センサーを有効に設定」して 42001 番ポートを開放した上で、TCP/IP のメッセージングによる連携を Ruby で行う、というものです。基本的にはほぼ同じ内容を Node.js で実装してみようと思い、調べた結果をまとめました。


なお、石原さんは「まちくえすと」の開発などで活躍されていてご存知の方もいらっしゃると思いますが、拙作マンホールマップiOS アプリ版の開発に尽力いただいたご縁もあり、そのお付き合いの中で Scratch のことも教えていただきました。僕にとっては「Scratch の先生」でもあります。石原さん、このブログとても助かりました。ありがとうございます!



まず、以下の作業を行う前提として、Scratch はローカルインストール可能なバージョン 1.4 を使う必要があります。公式サイトからオンライン版であるバージョン 2.0 以降を利用することも可能ですが、以下で紹介する Node.js 連携は Scratch 1.4 をローカルインストールして使う想定で記載しています。各種 OS 向けの Scratch 1.4 はこちらからダウンロードして導入してください(以下、Ubuntu 16.04 環境に Scratch 1.4 を導入した前提で記述します):
https://scratch.mit.edu/scratch_1.4/



まず最初に、Scrach を外部連携するためのポート開放を行います。Scratch 1.4 を起動し、「調べる」カテゴリ内「●●センサーの値」と書かれたブロックを右クリックして、「遠隔センサー接続を有効にする」を選択します:
Screenshot from 2018-05-04 15-29-09


すると「遠隔センサー接続が有効になりました」という確認ダイアログが表示されます:
Screenshot from 2018-05-04 15-29-39



この作業後、Scratch は 42001 番ポートで接続できるようになっています。つまり僕の以前のブログで紹介したような TCP/IP プログラミングによって Scratch との TCP/IP 接続が可能になっているのでした。試しに以下のような Node.js プログラム(scratch01.js)を書いて実行してみます(以前のブログで書いたものよりも更に簡略化し、最小限必要な内容だけにしています):
var net = require( 'net' );

//. 接続先 IP アドレスとポート番号
var host = '127.0.0.1';
var port = 42001;

//. 接続
var client = new net.Socket();
client.connect( port, host, function(){
  console.log( '接続: ' + host + ':' + port );
});

//. サーバーからメッセージを受信したら、その内容を表示する
client.on( 'data', function( data ){
  console.log( '' + data );
});

↑このプログラムを実行すると、127.0.0.1(自分自身)の 42001 番ポートに TCP/IP 接続し、接続先から何かメッセージが送られてくるとそのメッセージ内容を文字列化してそのまま表示する、というものです。これをまず node コマンドを使って実行しておきます:
$ node scratch01.js

これで 42001 番ポートに接続し、メッセージの受信待ち状態になりました。


そして Scratch 側からはこの(42001 番ポートの)接続先に対してメッセージを送信してみます。この処理を行うには「制御」カテゴリ内「●●を送る」ブロックを使います:
Screenshot from 2018-05-04 15-40-55


同ブロックを右クリックして「新規」を選択し、メッセージの名前を任意に指定します。以下の例ではメッセージの名前に Hello と指定してます:
Screenshot from 2018-05-04 15-42-14


するとブロックの表示内容が「Hello を送る」と切り替わります。この状態で同ブロックを何度かマウスでクリックします(クリックするたびにこのブロックが処理を実行します):
Screenshot from 2018-05-04 15-42-41


すると先に実行していた Node.js プログラムのコンソール画面に(最初の制御文字に続いて) 
broadcast "Hello"
という文字行がクリックするたびに表示されます:
Screenshot from 2018-05-04 15-46-59


この結果から Scratch の「●●を送る」ブロックを1回実行すると
broadcast "(指定したメッセージ)"

という文字列が送信される、ということが分かりました。これで Scratch から Node.js のプログラムにメッセージを送信し、Node.js プログラム側でその内容を受信することができました。


この応用で逆に Node.js から Scratch にメッセージを送ることもできます。例えば scratch02.js を以下の内容で作成します(接続後に 'broadcast "a"' というメッセージを Scratch に送信する、という内容です):
//. scratch02.js
var net = require( 'net' );
var host = '127.0.0.1';
var port = 42001;

var client = new net.Socket();
client.connect( port, host, function(){
  console.log( '接続: ' + host + ':' + port );

  var msg = 'broadcast "a"';
  var len = msg.length;
  var buf = pack( len );
  client.write( buf + msg );  //. msg の内容を Scratch へ送信
});

//. 4バイトのバイト列(Buffer)に変換
function pack( n ){
  var m1 = n % 256;
  n = Math.floor( n / 256 );
  var m2 = n % 256;
  n = Math.floor( n / 256 );
  var m3 = n % 256;
  var m4 = Math.floor( n / 256 );

  return new Buffer( [ m4, m3, m2, m1 ] );
}



そして Scratch 側では 'broadcast "a"' というメッセージを受け取ったら何らかの処理がスタートする、というプログラムを書いておきます(下の例ではスプライト1の猫のキャラクターが画面内を歩き始める処理がスタートする、という内容にしています):
Screenshot from 2018-05-05 15-54-06


この状態で scratch02.js を実行すると、TCP/IP 通信によって Scratch に接続し、broadcast "a" というメッセージが送られ、Scratch のスプライト1のキャラクターが壁に跳ね返りながら動き回る処理がスタートします(特に止める方法を用意していないので、止めるには画面右上の赤丸部分をクリックしてください):
Screenshot from 2018-05-05 16-02-12


これで Scratch から Nodejs にメッセージを送ったり、逆に Node.js から Scratch にメッセージを送る、ということが実現できました。

 

このページのトップヘ