AngularJSとRequireJSを合わせて使ってみた


モジュール単位でファイルに分けたいかなーと思ったので、
AngularJS本家のチュートリアルのphonecatAppを題材にして、
AngularJS + RequireJS | StarterSquadを参考にちょっとアレンジ。


結果のソースはこちら
単体テストの実行と結合したソースをproduction.htmlで見れるところまで確認。

アプリの定義


phonecatApp.controllersにはPhoneListCtrlとPhoneDetailCtrlが入ってる。
phonecatApp.filtersにはcheckmark、
phonecatApp.servicesにはPhoneがそれぞれ入ってる。

app/assets/js/app.js

define([
  'angular',
  'angularRoute',
  'angularResource',
  './services/Phone',
  './filters/checkmark',
  './controllers/PhoneListCtrl',
  './controllers/PhoneDetailCtrl'
], function(angular) {

  'use strict';

  var phonecatApp = angular.module('phonecatApp', [
    'ngRoute',
    'phonecatApp.controllers',
    'phonecatApp.filters',
    'phonecatApp.services'
  ]);
  phonecatApp.config([
    '$routeProvider',
    function($routeProvider) {
      $routeProvider.
        when('/phones', {
        templateUrl: 'assets/js/templates/phone-list.html',
        controller: 'PhoneListCtrl'
      }).
        when('/phones/:phoneId', {
        templateUrl: 'assets/js/templates/phone-detail.html',
        controller: 'PhoneDetailCtrl'
      }).
        otherwise({
        redirectTo: '/phones'
      });
    }
  ]);
  return phonecatApp;
});

モジュール定義(例:コントローラ)


依存関係で読み込んでいるmodule(app/assets/js/controllers/module.js)がポイントになる。

app/assets/js/controllers/PhoneListCtrl.js

define([
  './module',
  '../services/Phone'
], function(controllers) {

  'use strict';

  return controllers.controller('PhoneListCtrl', [
    '$scope',
    'Phone',
    function($scope, Phone) {
      $scope.phones = Phone.query();
      $scope.orderProp = 'age';
    }
  ]);
});


moduleでphonecatApp.controllersを定義してコントローラを登録させる。
servicesとかも似たような感じで各まとまりでmodule作ってそこに登録させる。

app/assets/js/controllers/module.js

define([
  'angular',
], function(ng) {
  'use strict';
  return ng.module('phonecatApp.controllers', []);
});

アプリの起動


ng-appディレクティブだと間に合わないので、angular.bootstrapを使って手動でphonecatAppを実行する。

app/assets/js/bootstrap.js

require([
  'angular',
  'app'
], function(ng) {
  'use strict';

  ng.bootstrap(document, ['phonecatApp']);
});

感想


define、requireとangular.module両方で依存定義する必要があるので、ちょっと微妙。

mongooseで埋め込みを使う

MongoDBのリレーションでやるか埋め込みでやるか迷った末に埋め込みでやることにしたので、
mongooseでのやり方をざっと試してみた。

使ったバージョン

node.js v0.8.18
mongoose 3.5.5

試験ソース

コールバックだらけになるので、jsdeferred使って流れ整理した。

var Deferred = require('./deps/jsdeferred').Deferred,
// mongoose
mongoose = require('mongoose'),
// 子スキーマ
ChildSchema = new mongoose.Schema({
    name : String
}),
// 親スキーマ
ParentSchema = new mongoose.Schema({
    name : String,
    childlen : [ChildSchema]
}),
// 親モデル
Parent = mongoose.model('Parent', ParentSchema);

// 接続
mongoose.connect('mongodb://localhost/foo');

!function() {
    // 最初にdb.parentsを全部消しておく
    var d = new Deferred();
    Parent.remove(function(err) {
        if (err) {
            d.fail(err);
            return;
        }
        d.call();
    });
    return d;
}()
.next(function() {
    var d = new Deferred(),
    // parentを新規作成して保存
    parent = new Parent({
        name : 'test'
    });
    parent.save(function(err) {
        if (err) {
            d.fail(err);
            return;
        }
        d.call();
    });
    return d;
})
.next(function() {
    var d = new Deferred();
    // 親を取得
    Parent.findOne({ name : 'test'}, function(err, parent) {
        if (err) {
            d.fail(err);
            return;
        }
        // 親の名前をチェック
        console.log(parent.name);
        console.log('子供が0人');
        // この時点ではまだ子供がいない
        console.log(parent.childlen);
        // 子供を一人追加
        parent.childlen.push({
            name : 'baby1'
        });
        parent.save(function(err) {
            if (err) {
                d.fail(err);
            }
            d.call();
        });
    });
    return d;
})
.next(function() {
    var d = new Deferred();
    // 親を取得
    Parent.findOne({ name : 'test'}, function(err, parent) {
        if (err) {
            d.fail(err);
            return;
        }
        console.log('子供が1人');
        console.log(parent.childlen);
        // まとめて複数人追加
        parent.childlen.push({ 
            name : 'baby2'
        });
        parent.childlen.push({ 
            name : 'baby3'
        });
        parent.childlen.push({ 
            name : 'baby4'
        });
        parent.save(function(err) {
            if (err) {
                d.fail(err);
            }
            d.call();
        });
    });
    return d;
})
.next(function() {
    var d = new Deferred();
    // 親を取得
    Parent.findOne({ name : 'test'}, function(err, parent) {
        if (err) {
            d.fail(err);
            return;
        }
        console.log('子供が4人');
        console.log(parent.childlen);

        // 名前を更新する
        parent.childlen[0].name = 'John';
        parent.childlen[1].name = 'Paul';
        parent.childlen[2].name = 'George';
        parent.childlen[3].name = 'Ringo';
        parent.save(function(err) {
            if (err) {
                d.fail(err);
            }
            d.call();
        });
    });
    return d;
})
.next(function() {
    var d = new Deferred();
    // 親を取得
    Parent.findOne({ name : 'test'}, function(err, parent) {
        if (err) {
            d.fail(err);
            return;
        }
        console.log('更新後');
        console.log(parent.childlen);
        // 1つ削除
        parent.childlen.remove(parent.childlen[1]);
        parent.save(function(err) {
            if (err) {
                d.fail(err);
            }
            d.call();
        });
    });
    return d;
})
.next(function() {
    var d = new Deferred();
    // 親を取得
    Parent.findOne({ name : 'test'}, function(err, parent) {
        if (err) {
            d.fail(err);
            return;
        }
        console.log('1件削除後');
        console.log(parent.childlen);
        // 全て削除する
        /** 
         * forEachだとうまく消せない
         */
        // parent.childlen.forEach(function(c) {
        //     c.remove();
        // });
        var i, len = parent.childlen.length;
        for (i = len - 1; i >= 0; i--) {
            parent.childlen[i].remove();
        }
        parent.save(function(err) {
            if (err) {
                d.fail(err);
            }
            d.call();
        });
    });
    return d;
})
.next(function() {
    var d = new Deferred();
    // 親を取得
    Parent.findOne({ name : 'test'}, function(err, parent) {
        if (err) {
            d.fail(err);
            return;
        }
        console.log('0人');
        console.log(parent.childlen);
        d.call();
    });
    return d;
})
.next(function() {
    mongoose.disconnect();
})
.error(function(err) {
    console.log(err);    
    mongoose.disconnect();
});

実行結果

$ node index
test
子供が0人
[]
子供が1人
[{ name: 'baby1', _id: 51110eba0d42bc5711000003 }]
子供が4人
[{ name: 'baby1', _id: 51110eba0d42bc5711000003 }
{ name: 'baby2', _id: 51110eba0d42bc5711000004 }
{ name: 'baby3', _id: 51110eba0d42bc5711000005 }
{ name: 'baby4', _id: 51110eba0d42bc5711000006 }]
更新後
[{ _id: 51110eba0d42bc5711000003, name: 'John' }
{ _id: 51110eba0d42bc5711000004, name: 'Paul' }
{ _id: 51110eba0d42bc5711000005, name: 'George' }
{ _id: 51110eba0d42bc5711000006, name: 'Ringo' }]
1件削除後
[{ _id: 51110eba0d42bc5711000003, name: 'John' }
{ _id: 51110eba0d42bc5711000005, name: 'George' }
{ _id: 51110eba0d42bc5711000006, name: 'Ringo' }]
0人
[]

感想

削除の時にforEachで回すとインデックスの関係か削除うまくいかないみたい。

expressでフォームからファイルアップロードする


node.jsでexpress使ってフォームのファイルアップロードする場合、
bodyParser使っていればrequestのfilesから取得できる様子。

実験コード

HTMLはこんな感じ。

<!DOCTYPE HTML>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Study</title>
    </head>
    <body>
        <div>
            <h3>File Upload</h3>
            <!-- ファイルアップロードフォーム -->
            <form id="file-form" method="post" action="/postdata" enctype="multipart/form-data">
                <input type="file" name="image_file" />
                <button type="submit" class="btn">Submit</button>
            </form>
            <h3>Uploaded File</h3>
            <!-- 前にアップロードしたファイル -->
            <img src="./img/upfile.png" />
        </div>
    </body>
</html>

サーバ側のソースこんな感じ。

var express = require('express'),
fs = require('fs'),
imageDirPath = __dirname + '/webroot/img/',
app = express();

app.use(express.static(__dirname + '/webroot'));
app.use(express.bodyParser());

app.post('/postdata', function(req, res) {
    // fileオブジェクト取得
    var file = req.files.image_file;
    // リネームしてimgタグで見れるようにする
    fs.rename(file.path, imageDirPath + 'upfile.png' , function(err) {
        if (err) {
            console.log(err);
        }
        // リダイレクトで戻す
        res.set('Location', '/');
        res.send(301);
    });
});
app.listen(8080);

補足

bodyParserにパラメータ渡すと、一時ファイルで拡張子を維持するとか、一時ファイルの保存ディレクトリを指定できる。

app.use(express.bodyParser({ keepExtensions: true, uploadDir: '/my/files' }));

node-mysqlでTEXT型使ってたら文字化けした


node-mysqlの0.9.6を使っていたら、TEXT型のカラムのデータが時々文字化けする現象に出くわした。
node-mysqlJavaScriptだけで全部できてたので、ソースを追って調べてみた。

調査


mysql/lib/query.jsに

row[field.name] += buffer.toString('utf-8');

となっていて、BufferをUTF-8の文字列にしてから連結している部分を発見。ここが怪しい。

対応


Bufferをいったん保持しておいて、最終的に連結して文字列化するように変更してみた。
以下、差分。

5a6,19
> function joinBuffers(buffers) {
>     var i, len = buffers.length, size = 0;
>     for (i = 0; i < len; i++) {
>         size += buffers[i].length;
>     }
>     var result = new Buffer(size);
>     var buffer, from = 0;
>     while (buffer = buffers.shift()) {
>         buffer.copy(result, from, 0);
>         from += buffer.length;
>     } 
>     return result;
> }
> 
62c76
<           row[field.name] = '';
---
>           row[field.name] = [];
66,68c80
<           row[field.name] += buffer.toString('utf-8');
<         } else {
<           row[field.name] = null;
---
>           row[field.name].push(buffer);
72a85,90
>         }
> 
>         if (row[field.name].length > 0) {
>             row[field.name] = joinBuffers(row[field.name]).toString('utf-8');
>         } else {
>             row[field.name] = null;

結果


解決したっぽい。文字化けしなくなった。
githubでforkして修正をpull request投げてみようかと思ったけど、
なんか開発進んでだいぶ様変わりしてるみたいなので、そっとブラウザを閉じた。

WebSocketでバイナリを送受信してみた(2)

クライアントからバイナリを送る確認ができたので、
バイナリに更にデータ乗っけて送ったりもらったりしてみる。

クライアント側

input:fileでファイルを参照したら、先頭にhelloworld文字列を追加した
バイナリデータとして送信する。

サーバ側からメッセージを受け取って、バイナリデータだったら
先頭の文字列と画像データに分けてそれぞれ表示する。

// BlobBuilder
BlobBuilder = window.BlobBuilder || window.WebKitBlobBuilder || window.MozBlobBuilder;
// URL
URL = window.URL || window.webkitURL;

// document
var doc = document;

// WebSocketを開く
var socket = new WebSocket('ws://' + location.host);
// バイナリデータはArrayBufferで取得する
socket.binaryType = 'arraybuffer';

// messageイベント
socket.addEventListener('message', function(evt) {
    var data = evt.data;
    if (data.constructor === String) {
        // Stringの場合、pタグで追加
        var p = doc.createElement('p');
        p.textContent = data;
        doc.getElementById('messages').appendChild(p);
    } else if (data.constructor === ArrayBuffer) {
        // ArrayBufferの場合、付加された文字列と画像データに分ける

        // 先頭から10バイトを8ビット符号なし整数値の配列に
        var arr = new Uint8Array(data.slice(0, 10));
        var key = [];
        for (var i = 0; i < 10; i++) {
            // キャラクタコードで文字列に変換して格納
            key.push(String.fromCharCode(arr[i]));
        }
        // 付加した文字列をpタグで追加
        var p = doc.createElement('p');
        p.textContent = key.join('');
        doc.getElementById('messages').appendChild(p);
        // 画像データ分をimgタグで追加
        var builder = new BlobBuilder();
        var img = new Image();
        builder.append(data.slice(10));
        img.src = URL.createObjectURL(builder.getBlob());
        doc.getElementById('images').appendChild(img);
    } else {
        alert('nanigashi');
    }
}, false);

// 文字列データをサーバに送信
doc.getElementById('sendText')
.addEventListener('click', function(evt) {
    socket.send('test');
}, false);

// fileを参照したらサーバに送信
doc.getElementById('image')
.addEventListener('change', function(evt) {
    var file = evt.target.files[0];
    var builder = new BlobBuilder();
    builder.append('helloworld');
    builder.append(file);
    // socket.send(file);
    socket.send(builder.getBlob());
}, false);

サーバ側

クライアントからバイナリデータを受け取ったら文字列と画像データに分解して、
文字列を返したあとに新たな文字列を先頭にくっつけた画像データを送信する。

// http server
var connect = require('connect');
var httpServer = connect()
.use(connect.static(__dirname + '/webroot'))
.listen(1234);

// WebSocket Server
var WebSocketServer = require('websocket').server;
var wsServer = new WebSocketServer({
    httpServer : httpServer,
    autoAcceptConnections : true
});

// クライアント接続イベント
wsServer.on('connect', function(client) {
    // クライアントからのメッセージ受信イベント
    client.on('message', function(message) {
        if (message.type === 'utf8') {
            // 文字列だったら文字列としてそのまま送信
            client.sendUTF(message.utf8Data);
        } else if (message.type === 'binary') {
            // バイナリだったら付加されている文字列を取り出し
            var src = message.binaryData;
            var text = new Buffer(10);
            src.copy(text, 0, 0, 10);
            // 受け取った文字列を返す
            client.sendUTF('receive:' + text.toString('utf8'));
            
            // 新たな文字列を付加して書き換えたバイナリデータとして送信
            var str = 'helloagain';
            var key = new Buffer(str);
            var buf = new Buffer(src.length);
            key.copy(buf, 0);
            src.copy(buf, 10, 10);
            client.sendBytes(buf);
        } else {
            console.log('nanigashi');
        }
    });
});

結果

ArrayBufferを使っていろいろやることで、バイナリデータを分割したりして
いろいろできた。ちょうどFirefoxも12からArrayBufferのsliceができるようになってた。

WebSocketでバイナリを送受信してみた

Firefox11になってとっくにWebSocketのベンダープレフィックスとれてたし、
バイナリデータの送受信の辺りがどうなってるか確認してみた。

確認したブラウザはFirefox11.0とGoogle Chrome18.0.1025.162。

サーバ側


node.jsで実装する。
httpサーバ用にconnectを使って、WebSocketサーバはWebSocket-Nodeを使う。

それぞれnpm install connect、npm install websocketでモジュール入れる。

サーバ側のコードは次の通り。

// http server
var connect = require('connect');
var httpServer = connect()
.use(connect.static(__dirname + '/webroot'))
.listen(1234);

// WebSocket Server
var WebSocketServer = require('websocket').server;
var wsServer = new WebSocketServer({
    httpServer : httpServer,
    autoAcceptConnections : true
});

// クライアント接続イベント
wsServer.on('connect', function(client) {
    // クライアントからのメッセージ受信イベント
    client.on('message', function(message) {
        if (message.type === 'utf8') {
            // 文字列だったら文字列としてそのまま送信
            client.sendUTF(message.utf8Data);
        } else if (message.type === 'binary') {
            // バイナリだったらバイナリとしてそのまま送信
            client.sendBytes(message.binaryData);
        } else {
            console.log('nanigashi');
        }   
    }); 
});

クライアント側からメッセージ届いたら、タイプに応じてそのまま返すだけ。

クライアント側


ボタンを押したら固定文字列を送るのと、ファイル参照したらそれを送るようにする。
送ったデータがサーバから送り返されたら、タイプに応じてそれぞれのdivの中に書きだす。
HTML次のとおり。

<!DOCTYPE HTML>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>WebSocket Test</title>
    </head>
    <body>
        <input id="sendText" type="button" value="文字列を送る" />
        <input id="image" name="image" type="file" />
        <!-- 文字列受信ログ -->
        <div id="messages"></div>
        <!-- 画像受信ログ -->
        <div id="images"></div>
        <script type="text/javascript" src="index.js"></script>
    </body>
</html>

クライアント側のJavaScript

// URL
URL = window.URL || window.webkitURL;

// document
var doc = document;
        
// WebSocketを開く
var socket = new WebSocket('ws://' + location.host);
        
// messageイベント
socket.addEventListener('message', function(evt) {
    var data = evt.data;
    if (data.constructor === String) {
        // Stringの場合、pタグで追加
        var p = doc.createElement('p');
        p.textContent = data;
        doc.getElementById('messages').appendChild(p);
    } else if (data.constructor === Blob) {
        // Blobの場合、imgタグで追加
        var img = new Image();
        img.src = URL.createObjectURL(data);
        doc.getElementById('images').appendChild(img);
    } else {
        alert('nanigashi');
    }
}, false);

// 文字列データをサーバに送信
doc.getElementById('sendText')
.addEventListener('click', function(evt) {
    socket.send('test');
}, false);

// fileを参照したらサーバに送信
doc.getElementById('image')
.addEventListener('change', function(evt) {
    var file = evt.target.files[0];
    socket.send(file);
}, false);

結果


FirefoxGoogle Chrome共に送ったバイナリデータを送り返してもらって表示できた。

node-iconvでEUCJP-WIN使おうとしてころんだ。


node.jsはutf-8が前提なので、sjisだのeucだのは変換してあげないとダメですよ。
とのことなので、扱いたかったデータがeucだったので、node-iconvを入れて変換してみた。

node-iconvのインストールと変換処理


npmで入れる。

$ npm install iconv


こんな感じで使う。

/**
 * 省略
 */
// Iconv
var Iconv = require('iconv').Iconv;

// EUC to UTF-8なコンバータを生成
var conv = new Iconv('EUC-JP', 'UTF-8//TRANSLIT//IGNORE');          

// バッファをコンバートしてUTF-8なデータにする
var data = (conv.convert(buf)).toString('utf8');
/**
 * 省略
 */

機種依存文字(だったっけ?)に対応したくなった


このまでも概ね問題ないんだけど、ローマ数字みたいなのがダメになっちゃうので、
EUC-JPの代わりにEUCJP-WINを指定して、//TRANSLIT//IGNOREを外す。


で、実行したら、エラーになった。
エラーメッセージ。

Error: EINVAL, Conversion not supported.

サポートしてないよってことっぽいので調べてみる。


調査コード。

var sys = require('sys');
var Iconv = require('iconv').Iconv;
var charsets = [
    'EUC-JP-MS',
    'EUC-JP',
    'EUCJP-MS',
    'EUCJP-OPEN',
    'EUCJP-WIN',
    'EUCJP'
];
var charset;
while (charset = charsets.shift()) {
    try {
        new Iconv(charset, 'utf-8');
    } catch(e) {
        sys.log('Fail : ' + charset);
        sys.log(e);
    }
}


結果。

$ node a.js
22 Apr 22:43:16 - Fail : EUC-JP-MS
22 Apr 22:43:16 - Error: EINVAL, Conversion not supported.
22 Apr 22:43:16 - Fail : EUCJP-MS
22 Apr 22:43:16 - Error: EINVAL, Conversion not supported.
22 Apr 22:43:16 - Fail : EUCJP-OPEN
22 Apr 22:43:16 - Error: EINVAL, Conversion not supported.
22 Apr 22:43:16 - Fail : EUCJP-WIN
22 Apr 22:43:16 - Error: EINVAL, Conversion not supported.

EUCJPとEUC-JPしかサポートしてなのね。

パッチを当てる。


対応させるために、こちらで公開されているlibiconv-1.13-ja-1.patch.gzを当てる。



node-iconvのlibiconvディレクトリがあるところまで移動。

$ cd .node_libraries/.npm/iconv/1.1.0/package/deps/


パッチをダウンロード。

$ wget http://www2d.biglobe.ne.jp/~msyk/software/libiconv/libiconv-1.13-ja-1.patch.gz


libiconvのディレクトリに入ってパッチあてる。改めてビルドする。

$ cd libiconv-1.13.1/
$ gzip -dc ../libiconv-1.13-ja-1.patch.gz | patch -p1
$ ./configure --disable-shared --enable-static --enable-relocatable --enable-extra-encodings
$ make


.node_libraries/.npm/iconv/1.1.0/packageまでちょっと戻って、node-iconvのあたりも改めてビルドする。

$ cd ../../
$ g++ -Ideps/libiconv-1.13.1/include -I/usr/local/include/node -O2 -fPIC -Wall -ansi   -c -o iconv.o iconv.cc
$ g++ -shared -o iconv.node iconv.o deps/libiconv-1.13.1/lib/.libs/libiconv.a

動作確認。


さっきの確認スクリプトをもう一度実行する。

$ node a.js

こんどはOK。


変換処理の方も実行してみて確認。

/**
 * 省略
 */
// EUC to UTF-8なコンバータを生成
var conv = new Iconv(EUCJP-WIN', 'UTF-8');
/**
 * 省略
 */


変換できた。