Monthly archives "1月 2016"

angular-fullstackをTypeScriptで書き換えてみよう

AngularJSをTypeScriptで書くと分かりやすいコードが書ける。
それを実証するために、angular-fullstakをTypeScriptで書き換えてみる。

参考文献
AngularJS+TypeScriptを試してみた。

なお、ここで想定している環境は下記の通り。
・Lubuntu(Ubuntu) 14.04
・angular-fullstakでアプリ構築済み
 構築していない場合は下記URLを参考に構築すること
 YEOMANを使ってMEAN(MongoDB+Express+AngularJS+Node.js)を作成しよう
・TypeScript開発環境構築済み
 構築していない場合は下記URLを参考に構築すること
 AtomでTypeScriptの開発環境を構築しよう

1. 型定義ファイルをインストールする

下記コマンドを実行

tsd install angular-ui-router --save
tsd install angular-resource --save
tsd install angular-cookies --save
tsd install lodash --save
tsd install socket.io-client --save

※ 他にも必要なファイルがある場合は、適宜インストールすること

2. TypeScriptのコンパイラーターゲットをES6にする

tsconfig.jsonで、targetをes6に変更する。(すでにangular-fullstackがBabelを使ってES6で書かれているため)

{
  "compilerOptions": {
    "target": "es6",
  }
}

次に、angular-fullstakの元々のコードとTypeScriptに書き換えたコードを列挙していく。

3. controllerを書き換える

例として client/app/main/main.controller.js を書き換える。
元のJavaScriptがすでにES6によるclass化がされているので、変換後のJavaScriptも元のJavaScriptとほとんど変わらない。

元のJavaScriptは下記の通り。

'use strict';

(function() {

class MainController {

  constructor($http, $scope, socket) {
    this.$http = $http;
    this.awesomeThings = [];

    $http.get('/api/things').then(response => {
      this.awesomeThings = response.data;
      socket.syncUpdates('thing', this.awesomeThings);
    });

    $scope.$on('$destroy', function() {
      socket.unsyncUpdates('thing');
    });
  }

  addThing() {
    if (this.newThing) {
      this.$http.post('/api/things', { name: this.newThing });
      this.newThing = '';
    }
  }

  deleteThing(thing) {
    this.$http.delete('/api/things/' + thing._id);
  }
}

angular.module('sampleApp')
  .controller('MainController', MainController);

})();

TypeScriptは下記の通り。
ここでconstructorの引数socketの型がanyであることに注目。
anyというは「何でもあり」を意味し、型を定義していないのと同義なので、TypeScriptにするメリットが得られない。
しかし、socketはangular-fullstackが作成しているので型定義ファイルが無い。
そこでとりあえずはanyとしておき、後でsocketの型を定義することにする。
main.controller.ts

/// <reference path="../../../typings/tsd.d.ts" />

'use strict';

class MainController {
  private awesomeThings: {};
  private newThing: string;

  constructor(private $http: angular.IHttpService, $scope: angular.IScope, socket: any) {
    $http.get('/api/things').then(response => {
      this.awesomeThings = response.data;
      socket.syncUpdates('thing', this.awesomeThings);
    });

    $scope.$on('$destroy', function() {
      socket.unsyncUpdates('thing');
    });
  }

  public addThing(): void {
    if (this.newThing) {
      this.$http.post('/api/things', { name: this.newThing });
      this.newThing = '';
    }
  }

  public deleteThing(thing): void {
    this.$http.delete('/api/things/' + thing._id);
  }
}

angular.module('sampleApp')
  .controller('MainController', MainController);

変換後のJavaScriptは下記の通り。

/// <reference path="../../../typings/tsd.d.ts" />
'use strict';
class MainController {
    constructor($http, $scope, socket) {
        this.$http = $http;
        $http.get('/api/things').then(response => {
            this.awesomeThings = response.data;
            socket.syncUpdates('thing', this.awesomeThings);
        });
        $scope.$on('$destroy', function () {
            socket.unsyncUpdates('thing');
        });
    }
    addThing() {
        if (this.newThing) {
            this.$http.post('/api/things', { name: this.newThing });
            this.newThing = '';
        }
    }
    deleteThing(thing) {
        this.$http.delete('/api/things/' + thing._id);
    }
}
angular.module('sampleApp')
    .controller('MainController', MainController);

4. serviceを書き換える

先ほどanyとしたsocketをclass化して型を定義する。
client/components/socket/socket.service.js

元のJavaScirptは下記の通り。

/* global io */
'use strict';

angular.module('sampleApp')
  .factory('socket', function(socketFactory) {
    // socket.io now auto-configures its connection when we ommit a connection url
    var ioSocket = io('', {
      // Send auth token on connection, you will need to DI the Auth service above
      // 'query': 'token=' + Auth.getToken()
      path: '/socket.io-client'
    });

    var socket = socketFactory({ ioSocket });

    return {
      socket,

      /**
       * Register listeners to sync an array with updates on a model
       *
       * Takes the array we want to sync, the model name that socket updates are sent from,
       * and an optional callback function after new items are updated.
       *
       * @param {String} modelName
       * @param {Array} array
       * @param {Function} cb
       */
      syncUpdates(modelName, array, cb) {
        cb = cb || angular.noop;

        /**
         * Syncs item creation/updates on 'model:save'
         */
        socket.on(modelName + ':save', function (item) {
          var oldItem = _.find(array, {_id: item._id});
          var index = array.indexOf(oldItem);
          var event = 'created';

          // replace oldItem if it exists
          // otherwise just add item to the collection
          if (oldItem) {
            array.splice(index, 1, item);
            event = 'updated';
          } else {
            array.push(item);
          }

          cb(event, item, array);
        });

        /**
         * Syncs removed items on 'model:remove'
         */
        socket.on(modelName + ':remove', function (item) {
          var event = 'deleted';
          _.remove(array, {_id: item._id});
          cb(event, item, array);
        });
      },

      /**
       * Removes listeners for a models updates on the socket
       *
       * @param modelName
       */
      unsyncUpdates(modelName) {
        socket.removeAllListeners(modelName + ':save');
        socket.removeAllListeners(modelName + ':remove');
      }
    };
  });

Socket.syncUpdatesの引数cbにデフォルト引数が付いていることに注目。
呼び出し側でcbを省略したときはangular.noopが適用される。
実際main.controller.tsではcb未設定で呼び出されている。
試しにcbのデフォルト引数を削除するとmain.controller.tsでSocketのエラーになる。
※ 元のコードではunsyncUpdates()でsocket.removeAllListeners()を使っているが、socket.removeListener()の間違いと思われる。(こういう間違いが見つけやすいのがTypeScriptの良いところだろう)
※ Socket.constructor()の引数socketFactoryがanyとなっているのは、angular-socket-ioの.d.tsファイルが無いため、型定義できないためである。
socket.service.ts

/// <reference path="../../../typings/tsd.d.ts" />

/* global io */
'use strict';

class Socket {
  private socket: SocketIOClient.Socket;

  constructor(socketFactory: any) {
    // socket.io now auto-configures its connection when we ommit a connection url
    var ioSocket = io('', {
      // Send auth token on connection, you will need to DI the Auth service above
      // 'query': 'token=' + Auth.getToken()
      path: '/socket.io-client'
    });

    this.socket = socketFactory({ ioSocket });
  }

  /**
   * Register listeners to sync an array with updates on a model
   *
   * Takes the array we want to sync, the model name that socket updates are sent from,
   * and an optional callback function after new items are updated.
   *
   * @param {String} modelName
   * @param {Array} array
   * @param {Function} cb
   */
  public syncUpdates(modelName: string, array: any, cb: Function = angular.noop): void {
    /**
     * Syncs item creation/updates on 'model:save'
     */
    this.socket.on(modelName + ':save', function(item: any): void {
      var oldItem = _.find(array, {_id: item._id});
      var index = array.indexOf(oldItem);
      var event = 'created';

      // replace oldItem if it exists
      // otherwise just add item to the collection
      if (oldItem) {
        array.splice(index, 1, item);
        event = 'updated';
      } else {
        array.push(item);
      }

      cb(event, item, array);
    });

    /**
     * Syncs removed items on 'model:remove'
     */
    this.socket.on(modelName + ':remove', function(item: any): void {
      var event = 'deleted';
      _.remove(array, {_id: item._id});
      cb(event, item, array);
    });
  };

  /**
   * Removes listeners for a models updates on the socket
   *
   * @param modelName
   */
  public unsyncUpdates(modelName: string): void {
    this.socket.removeListener(modelName + ':save');
    this.socket.removeListener(modelName + ':remove');
  };
}

angular.module('sampleApp')
  .service('socket', Socket);

変換後のJavaScriptは下記の通り。

/// <reference path="../../../typings/tsd.d.ts" />
/* global io */
'use strict';
class Socket {
    constructor(socketFactory) {
        // socket.io now auto-configures its connection when we ommit a connection url
        var ioSocket = io('', {
            // Send auth token on connection, you will need to DI the Auth service above
            // 'query': 'token=' + Auth.getToken()
            path: '/socket.io-client'
        });
        this.socket = socketFactory({ ioSocket: ioSocket });
    }
    /**
     * Register listeners to sync an array with updates on a model
     *
     * Takes the array we want to sync, the model name that socket updates are sent from,
     * and an optional callback function after new items are updated.
     *
     * @param {String} modelName
     * @param {Array} array
     * @param {Function} cb
     */
    syncUpdates(modelName, array, cb = angular.noop) {
        /**
         * Syncs item creation/updates on 'model:save'
         */
        this.socket.on(modelName + ':save', function (item) {
            var oldItem = _.find(array, { _id: item._id });
            var index = array.indexOf(oldItem);
            var event = 'created';
            // replace oldItem if it exists
            // otherwise just add item to the collection
            if (oldItem) {
                array.splice(index, 1, item);
                event = 'updated';
            }
            else {
                array.push(item);
            }
            cb(event, item, array);
        });
        /**
         * Syncs removed items on 'model:remove'
         */
        this.socket.on(modelName + ':remove', function (item) {
            var event = 'deleted';
            _.remove(array, { _id: item._id });
            cb(event, item, array);
        });
    }
    /**
     * Removes listeners for a models updates on the socket
     *
     * @param modelName
     */
    unsyncUpdates(modelName) {
        this.socket.removeListener(modelName + ':save');
        this.socket.removeListener(modelName + ':remove');
    }
}
angular.module('sampleApp')
    .service('socket', Socket);

先ほどのmain.controller.tsでsocketの型を定義する。

/// <reference path="../../../typings/tsd.d.ts" />
/// <reference path="../../components/socket/socket.service.ts" />

'use strict';

class MainController {
  private awesomeThings: {};
  private newThing: string;

  constructor(private $http: angular.IHttpService, $scope: angular.IScope, socket: Socket) {

5. directiveを書き換える

client/components/footer/footer.directive.jsを書き換えてみる。

元のJavaScriptは下記の通り。

'use strict';

angular.module('sampleApp')
  .directive('footer', function () {
    return {
      templateUrl: 'components/footer/footer.html',
      restrict: 'E',
      link: function(scope, element) {
        element.addClass('footer');
      }
    };
  });

TypeScriptは下記の通り。
footer.directive.ts

/// <reference path="../../../typings/tsd.d.ts" />
'use strict';

class Footer implements angular.IDirective {
  templateUrl = 'components/footer/footer.html';
  restrict = 'E';
  link(scope: angular.IScope, element: angular.IAugmentedJQuery): void {
    element.addClass('footer');
  }
}

angular.module('sampleApp')
  .directive('footer', () => new Footer());

変換後のJavaScriptは下記の通り。

/// <reference path="../../../typings/tsd.d.ts" />
'use strict';
class Footer {
    constructor() {
        this.templateUrl = 'components/footer/footer.html';
        this.restrict = 'E';
    }
    link(scope, element) {
        element.addClass('footer');
    }
}
angular.module('sampleApp')
    .directive('footer', () => new Footer());

6. angular-resourceを書き換える

最後におまけとしてangular-resourceを使っているclient/components/auth/user.service.jsを書き換えてみる。
angular-resourceは元のJavaScriptから随分違うコードに書き換える必要があるので注意が必要だ。
参考文献: How to use AngularJS ng.resource.IResource with TypeScript.

元のJavaScriptは下記の通り。

'use strict';

(function() {

function UserResource($resource) {
  return $resource('/api/users/:id/:controller', {
    id: '@_id'
  }, {
    changePassword: {
      method: 'PUT',
      params: {
        controller:'password'
      }
    },
    get: {
      method: 'GET',
      params: {
        id:'me'
      }
    }
  });
}

angular.module('sampleApp.auth')
  .factory('User', UserResource);

})();

TypeScriptは下記の通り。
user.service.ts

/// <reference path="../../../typings/tsd.d.ts" />

'use strict';

interface Password {
  oldPassword: string;
  newPassword: string;
}

interface User extends angular.resource.IResource<User> {
  _id: string;
  name: string;
  email: string;
  password: string;
  role: string;
}

interface UserResource extends angular.resource.IResourceClass<User> {
  changePassword(id: any, password: Password, successCallback: any, errorCallback: any): User;
  get(): User;
}

angular.module('sampleApp.auth')
  .factory('User', ($resource: angular.resource.IResourceService) => {
    var changePasswordAnction: angular.resource.IActionDescriptor = {
      method: 'PUT',
      params: {
        controller: 'password'
      }
    };

    var getAction: angular.resource.IActionDescriptor = {
      method: 'GET',
      params: {
        id: 'me'
      }
    };

    return <UserResource>$resource('/api/users/:id/:controller', {
      id: '@_id'
    }, {
      changePassword: changePasswordAnction,
      get: getAction
    });
  }
);

変換後のJavaScriptは下記の通り。

/// <reference path="../../../typings/tsd.d.ts" />
'use strict';
angular.module('sampleApp.atuh')
    .factory('User', ($resource) => {
    var changePasswordAnction = {
        method: 'PUT',
        params: {
            controller: 'password'
        }
    };
    var getAction = {
        method: 'GET',
        params: {
            id: 'me'
        }
    };
    return $resource('/api/users/:id/:controller', {
        id: '@_id'
    }, {
        changePassword: changePasswordAnction,
        get: getAction
    });
});

0

AtomでTypeScriptの開発環境を構築しよう

最近、TypeScriptにハマってます。
TypeScriptでAngularJSを書くと大変読みやすいコードが書けます。
TypeScriptの開発環境はAtomで構築するのが一番だろう。
というわけで、
UbuntuにAtomをインストールして、TypeScriptの開発環境の構築方法を説明する。

なお、ここで想定している環境は下記の通り。
Lubuntu(Ubuntu) 14.04
node.js インストール済み

1. Atomのインストール

参考文献: Ubuntu 14.04 に Atom をインストールして、起動チェックしてから、パッケージ導入するまで

下記のコマンド実行

sudo add-apt-repository ppa:webupd8team/atom
sudo apt-get update
sudo apt-get install atom

インストールが完了したら、スタートメニュー > プログラミング > Atom からAtomを起動できる

2. TypeScriptとtsdのインストール

参考文献: TypeScript の開発環境構築と周辺ツールの紹介

ちなみに、tsdとは型定義ファイルマネージャでTypeScirptの.d.tsファイルを管理するツールである。

下記のコマンド実行

npm install -g typescript
npm install -g tsd

3. AtomにTypeScript開発環境を構築する

下記URL参照
AtomでTypeScriptの環境を構築する

なお、筆者は上記のtsconfig.jsonのうち、下記の設定にカスタマイズしている。

{
  "formatCodeOptions": {
    "indentSize": 2, //自動インデントを2に設定
    "tabSize": 2, //タブサイズを2に設定
  }
}

0

YEOMANを使ってMEAN(MongoDB+Express+AngularJS+Node.js)を作成しよう

YEOMANを使ってMEANを作成する手順と開発チュートリアルを説明する。
MEANとはMongoDB+Express+AngularJS+Node.jsを組み合わせたWebアプリケーションである。
YEOMANとはフレームワークを簡単に作成できるツールで、node.jsのパッケージとして提供されている。
MEANのフレームワークにはangular-fullstakを使う。
※Yeomanで用意されているジェネレーターは沢山有るがMEANを作成できるジェネレーターで一番人気がangular-fullstak
 
ほとんどの手順は下記URLに従う。
YEOMANを使ってMEAN(MongoDB + Express.js + Angular.js + Node.js)のWebアプリケーションを作る
しかし、上記URLはYEOMANのバージョンが古いためか、チュートリアルの内容が現状のバージョンに沿わないため、下記であらためて説明する。
 
なお、ここで想定している開発環境は下記の通り。
Lubuntu(Ubuntu) 14.04
Node.js v4.2.3
yo 1.6.0
MongoDB 3.2.0
※ 上記以外のバージョンではチュートリアルの内容が沿わない場合がある
 

0. 事前準備

まずはangular-fullstakに必要なアプリをインストールする。
 

0.1. Node.jsのインストール

下記URL参照
Ubuntu 14.04 に Node.jsをインストールする
 

0.2. MongoDBのインストール

下記URL参照
Install MongoDB Community Edition on Ubuntu
 

0.3. Rubyとgemのインストール

下記URL参照
UbuntuにRubyGemsをインストールする
※なお、フレームワークにSassを使わない場合はRubyとgemをインストールする必要はない。
 

0.4. Sassのインストール

端末から下記コマンドを実行する

gem install sass

 

0.5. Yeomanのインストール

npm install -g yo grunt-cli bower

※Yeomanとはyo, grunt, bowerを組み合わせたもの
 

0.6. angular-fullstakのインストール

npm install -g generator-angular-fullstack
npm update -g

※yoが1.4.1以上でないとangular-fullstackが動かないのでアップデートすること
 

0.7. node-inspectorのインストール

デバッガを使う場合は下記コマンドを実行する。

npm install -g node-inspector

以上で事前準備は完了した。
次にMEANアプリの開発チュートリアルを説明する。
 

1. MEANアプリの雛形を作成する

yoを使ってMEANアプリを作成する。
アプリを作る場所はユーザーディレクトリの配下にする。
※ちなみにVMWareなどのバーチャルPCでの共有フォルダにはアプリは作れない。アプリ作成時にシンボリックリンクを貼るが、共有フォルダにはシンボリックリンクが貼れないため。
なお、アプリ名はsampleとする。
 

1.1. sampleディレクトリの作成

mkdir sample
cd sample

※ディレクトリ名がそのままアプリ名になる。
 

1.2. MEANアプリを作成する

yo angular-fullstack

なお、アプリの作成はウィザード方式になっており、各設定をダイアログで設定する。
各項目と選択すべき項目を列挙する。(▼が選択すべき項目 ●はチェックボックスの選択状態 ○はチェックボックスの非選択状態)

[?] What would you like to write scripts with? 
▼Babel
 TypeScript

 

[?] What would you like to write makeup with?
▼HTML
 Jade

 

[?] What would you like to write stylesheets with?
 CSS
▼Sass
 Stylus
 Less

※今回のチュートリアルではスタイルシートは弄らないのでSass以外を選んでも問題ない
 

[?] What Angular router would you like to use?
 ngRoute
▼uiRouter

 

[?] Would you like to include Bootstrap? Yes

 

[?] Would you like to include UI Bootstrap? Yes

 

[?] What would you like to use for data modeling?
●Mongoose (MongoDB)
○Sequelize (MySQL, SQLite, MariaDB, PostgreSQL)

 

[?] Would you scaffold out an authentication boilerplate? Yes

 

[?] Would you like to include additional oAuth strategies?
●Google
●Facebook
●Twitter

※今回のチュートリアルでは関係ないので何を選んでも良い
 

[?] Would you like to use socket.io? Yes

 

[?] What would you like to write tests with?
▼Jasmine
 Mocha + Chai + Sinon

 

1.3. 動作確認する

grunt serve

ブラウザが自動的に起動して下図の画面が表示されれば成功である。
yeoman.01.01
 

2. routerを作成する

yoで作られたMEANアプリにメンバー一覧ページを追加するというチュートリアルを説明する。
処理の流れとしては、
1.yoコマンドでrouterの雛形を作成する
2.routerのhtmlを編集する
3.routerのcontrolerを編集する
という流れになる。
 

2.1. ページを追加する

Ctrl + Cでgruntを止めて、下記のコマンドを実行する。

yo angular-fullstack:route member

下記のダイアログは全てデフォルトにする。
[?] Where would you like to create this route? client/app/
[?] What will the url of your route be? member
すると下記のファイルが作成される。
create client/app/member/member.js
create client/app/member/member.controller.js
create client/app/member/member.controller.spec.js
create client/app/member/member.html
create client/app/member/member.scss
 
動作確認する。

grunt serve

http://localhost:9000/memberにアクセスして、下図が表示されれば成功である。
yeoman.01.02
 

2.2. htmlを編集する

client/app/member/member.htmlを下記の通り編集する。

編集前
<div class="col-md-12">
This is the member view.
</div>

 

編集後
<h2>メンバー一覧</h2>
<ul>
  <li ng:repeat="member in members">
    {{member.name}}
  </li>
</ul>

 

2.3. ダミーデータを追加する

メンバーが正しく表示されるかを確認するためにダミーデータを追加する。(ダミーデータを後からmongoDBのデータに置き換える)
client/app/member/member.controller.jsを下記の通り編集する。

編集前
'use strict';

angular.module('sampleApp')
  .controller('MemberCtrl', function ($scope) {
    $scope.message = 'Hello';
  });

 

編集後
'use strict';

angular.module('sampleApp')
  .controller('MemberCtrl', function ($scope) {
    $scope.members = [{name:'田中'},{name:'鈴木'}];
  });

 

2.4. 動作確認する

http://localhost:9000/memberにアクセスして、下図の通り表示されれば成功である。
yeoman.01.03
 

3. clientからserverをGETする

処理の流れとしては、
1.clientからserverの/api/membersをGETで呼び出し
2.serverはcontroller.index関数へルーティング
3.controller.index関数でMongoDBにアクセス
4.MongoDBがデータを返す
という流れになる。
 

3.1. APIを追加する

Ctrl + Cでgruntを止めて、下記のコマンドを実行する。

yo angular-fullstack:endpoint member

下記のダイアログは全てデフォルトにする。
[?] What will the url of your endponit to be? /api/members
すると下記のファイルが作成される。
create server/api/member/index.js
create server/api/member/member.controller.js
create server/api/member/member.model.js
create server/api/member/member.socket.js
create server/api/member/member.spec.js
 

3.2. スキーマを定義する

メンバー一覧に表示する名前を定義する。
server/api/member/member.model.jsを下記の通り編集する。

編集前
'use strict';

var mongoose = require('bluebird').promisifyAll(require('mongoose'));

var MemberSchema = new mongoose.Schema({
  name: String,
  info: String,
  active: Boolean
});

export default mongoose.model('Member', MemberSchema);

 

編集後
'use strict';

var mongoose = require('bluebird').promisifyAll(require('mongoose'));

var MemberSchema = new mongoose.Schema({
  name: String,
});

export default mongoose.model('Member', MemberSchema);

 

3.3. MongoDBにデータを追加する

MongoDBに初期データを追加する。
server/api/member/member.controller.jsのMember変数定義以降に下記を追加する。

編集前
'use strict';

import _ from 'lodash';
import Member from './member.model';

 

編集後
'use strict';

import _ from 'lodash';
import Member from './member.model';

Member.find({}).remove(function() {
  Member.create({
    name: '田中_2'
  }, {
    name: '鈴木_2'
  }, function(err) {
    console.log('finished populating Members');
  });
});

 

3.4. サーバーにGETが定義されていることを確認する

server/api/member/index.jsに下記の通りgetが定義されていることを確認する。

router.get('/', controller.index);

 
server/api/member/member.controller.jsに下記の通りindex関数が定義されていることを確認する。

// Gets a list of Members
export function index(req, res) {
  Member.findAsync()
    .then(respondWithResult(res))
    .catch(handleError(res));
}

※上記のコードはangular-fullstackによって自動生成される。
 

3.5. APIに接続する

clientからserverのapiを参照する。
client/app/member/member.controller.jsを下記の通り編集する。

編集前
'use strict';

angular.module('sampleApp')
  .controller('MemberCtrl', function ($scope) {
    $scope.members = [{name:'田中'},{name:'鈴木'}];
  });

 

編集後
'use strict';

angular.module('sampleApp')
  .controller('MemberCtrl', function ($scope, $http) {
    $scope.members = [];

    $http.get('/api/members').success(function(members) {
      $scope.members = members;
    });
  });

 

3.6. 動作確認する

http://localhost:9000/memberにアクセスして、下図の通り表示されれば成功である。
yeoman.01.04
 

4. clientからserverへPOSTする

処理の流れとしては、
1.clientからserverの/api/membersをPOSTで呼び出し
2.serverはcontroller.create関数へルーティング
3.controller.create関数でMongoDBにアクセス
4.MongoDBへデータを保存する
という流れになる。
 

4.1. メンバー登録フォームを追加する

メンバーページからメンバーを登録できるようにする。
client/app/member/member.htmlを下記の通り編集する。

編集前
<h2>メンバー一覧</h2>
<ul>
  <li ng:repeat="member in members">
    {{member.name}}
  </li>
</ul>

 

編集後
<div ng-controller="MemberCtrl">
  <h2>メンバー登録</h2>
  <form >
    <legend>メンバー登録</legend>
    <label>名前</label> 
    <input type="text" id="name" name="name" ng-model="member.name"> 
    <button class="btn btn-primary" ng-click="createMember()">登録</button>
  </form>

  <h2>メンバー一覧</h2>
  <ul>
    <li ng:repeat="member in members">
      {{member.name}}
    </li>
  </ul>
</div>

 

4.2. 登録ボタンのイベントハンドラーを追加する

ボタンが動作するように、イベントハンドラーを追加する。
client/app/member/member.controller.jsを下記の通り編集する。

編集前
'use strict';

angular.module('sampleApp')
  .controller('MemberCtrl', function ($scope, $http) {
    $scope.members = [];

    $http.get('/api/members').success(function(members) {
      $scope.members = members;
    });
  });

 

編集後
'use strict';

angular.module('sampleApp')
  .controller('MemberCtrl', function ($scope, $http) {
    $scope.members = [];

    $http.get('/api/members').success(function(members) {
      $scope.members = members;
    });

    $scope.createMember = function() {
      if ($scope.member && $scope.member.name) {
        $scope.members.push({
          name: $scope.member.name
        });
        $scope.member.name = '';
      }
    };
  });

 

4.3. 動作確認する

http://localhost:9000/memberにアクセスして、任意の名前(例では佐藤)を入力して登録ボタンを押して、下図の通り表示されれば成功である。
yeoman.01.05
 

4.4. 登録データをPOSTする

このままではサーバーにデータが保存されていないので、データをサーバーへPOSTする。
client/app/member/member.controller.jsを下記の通り編集する。

編集前
'use strict';

angular.module('sampleApp')
  .controller('MemberCtrl', function ($scope, $http) {
    $scope.members = [];

    $http.get('/api/members').success(function(members) {
      $scope.members = members;
    });

    $scope.createMember = function() {
      if ($scope.member && $scope.member.name) {
        $scope.members.push({
          name: $scope.member.name
        });
        $scope.member.name = '';
      }
    };
  });

 

編集後
'use strict';

angular.module('sampleApp')
  .controller('MemberCtrl', function ($scope, $http) {
    $scope.members = [];

    $http.get('/api/members').success(function(members) {
      $scope.members = members;
    });

    $scope.createMember = function() {
      if ($scope.member && $scope.member.name) {
        $http.post('/api/members', $scope.member).success(function() {
          $scope.members.push({
            name: $scope.member.name
          });
          $scope.member.name = '';
        });
      }
    };
  });

 

4.5. サーバーにPOSTが定義されていることを確認する

server/api/member/index.jsに下記の通りpostが定義されていることを確認する。

router.post('/', controller.create);

 
server/api/member/member.controller.jsに下記の通りcreate関数が定義されていることを確認する。

// Creates a new Member in the DB
export function create(req, res) {
  Member.createAsync(req.body)
    .then(respondWithResult(res, 201))
    .catch(handleError(res));
}

※上記のコードはangular-fullstackによって自動生成される。
 

4.6. 動作確認する

http://localhost:9000/memberにアクセスして、任意の名前(例では佐藤)を入力して登録ボタンを押して、さらにページ更新して、下図の通り表示されれば成功である。
yeoman.01.05
 

5. socket.ioでページを同期させる

angular-fullstackではsocket.ioをページの同期に使っているようなので、それについても説明する。
処理の流れとしては、
1.clientでページ読み込み時(/api/memberをGETする時)にsocket通知によるデータ配列追加/削除処理を登録しておく。(syncUpdates関数)
2.serverではMongoDBにデータを追加した際にMongoDBのポストホック機能(Member.schema.post関数)からホックしている全てのclientへsocket通知を送信する。
3.clientではsocket通知を受け取ったら、データ配列に追加する。(データは$scope内に定義しているので、angularJSにより自動的にページ更新がかかる。)
という流れになる。
 

5.1. socket.syncUpdates()を呼び出す

client/app/member/member.controller.jsを下記の通り編集する。

編集前
'use strict';

angular.module('sampleApp')
  .controller('MemberCtrl', function ($scope, $http) {
    $scope.members = [];

    $http.get('/api/members').success(function(members) {
      $scope.members = members;
    });

    $scope.createMember = function() {
      if ($scope.member && $scope.member.name) {
        $http.post('/api/members', $scope.member).success(function() {
          $scope.members.push({
            name: $scope.member.name
          });
          $scope.member.name = '';
        });
      }
    };
  });

 

編集後
'use strict';

angular.module('sampleApp')
  .controller('MemberCtrl', function ($scope, $http, socket) {
    $scope.members = [];

    $http.get('/api/members').success(function(members) {
      $scope.members = members;
      socket.syncUpdates('member', $scope.members);
    });

    $scope.createMember = function() {
      if ($scope.member && $scope.member.name) {
        $http.post('/api/members', $scope.member).success(function() {
          $scope.member.name = '';
        });
      }
    };
  });

変更点は、
・/api/membersのGET時にsocket.syncUpdates呼び出し
・socket.syncUpdatesでメンバー一覧の更新が行われるので、createMember関数でのメンバー追加処理は削除する
 

5.2. socket.syncUpdates()の定義を確認する

/client/components/socket/socket.service.jsにsyncUpdates関数が定義されていることを確認する。

      /**
       * Register listeners to sync an array with updates on a model
       *
       * Takes the array we want to sync, the model name that socket updates are sent from,
       * and an optional callback function after new items are updated.
       *
       * @param {String} modelName
       * @param {Array} array
       * @param {Function} cb
       */
      syncUpdates: function (modelName, array, cb) {
        cb = cb || angular.noop;

        /**
         * Syncs item creation/updates on 'model:save'
         */
        socket.on(modelName + ':save', function (item) {
          var oldItem = _.find(array, {_id: item._id});
          var index = array.indexOf(oldItem);
          var event = 'created';

          // replace oldItem if it exists
          // otherwise just add item to the collection
          if (oldItem) {
            array.splice(index, 1, item);
            event = 'updated';
          } else {
            array.push(item);
          }

          cb(event, item, array);
        });

        /**
         * Syncs removed items on 'model:remove'
         */
        socket.on(modelName + ':remove', function (item) {
          var event = 'deleted';
          _.remove(array, {_id: item._id});
          cb(event, item, array);
        });
      },

※上記のコードはangular-fullstackによって自動生成される。
 

5.3. MongoDBのポストフック機能を使ってsocket通知が呼び出されていることを確認する

server/api/member/member.socket.jsにMongoDBへのデータ追加時にsocketからmember:saveが通知されることを確認する。

/**
 * Broadcast updates to client when the model changes
 */

'use strict';

var MemberEvents = require('./member.events');

// Model events to emit
var events = ['save', 'remove'];

export function register(socket) {
  // Bind model events to socket events
  for (var i = 0, eventsLength = events.length; i < eventsLength; i++) {
    var event = events[i];
    var listener = createListener('member:' + event, socket);

    MemberEvents.on(event, listener);
    socket.on('disconnect', removeListener(event, listener));
  }
}


function createListener(event, socket) {
  return function(doc) {
    socket.emit(event, doc);
  };
}

function removeListener(event, listener) {
  return function() {
    MemberEvents.removeListener(event, listener);
  };
}

※上記のコードはangular-fullstackによって自動生成される。
 

5.4. socket通知の登録処理が呼び出されていることを確認する

server/config/socketio.jsでregistar関数が呼ばれていること確認する。

// When the user connects.. perform this
function onConnect(socket) {
  // When the client emits 'info', this listens and executes
  socket.on('info', data => {
    socket.log(JSON.stringify(data, null, 2));
  });

  // Insert sockets below
  require('../api/member/member.socket').register(socket);
  require('../api/thing/thing.socket').register(socket);

}

※上記のコードはangular-fullstackによって自動生成される。
 

5.5. 動作確認する

ブラウザを2ウィンドウ開き、http://localhost:9000/memberにアクセスして、一方に任意の名前(例では藤原)を入力して登録ボタンを押して、もう一方に登録内容が反映されれば成功である。
yeoman.01.06
 

6. デバッガ(node-inspector)を起動する

デバッガの起動方法を説明しておく。
下記のコマンドを実行する。

grunt serve:debug

するとブラウザが自動起動してnode-inspectorが開く。
‘–debug-brk’オプション付きで起動されるので、必ずサーバー側のソースコードの先頭でブレイクする。

7. gruntでブラウザ指定する

起動するブラウザを変えたい場合、Gruntfile.jsを下記の通り変更する。

通常起動のブラウザを指定する方法(例ではchromium-browserを指定している)

  grunt.initConfig({
  //省略...
    open: {
      server: {
        url: '<%= express.options.domain %>:<%= express.options.port %>',
        app: 'chromium-browser'
      }
    },

デバッガのブラウザを指定する方法(例ではchromium-browserを指定している)

    nodemon: {
    //省略...
      debug: {
      //省略...
        options: {
       //省略...
          callback: function (nodemon) {
            // opens browser on initial server start
            nodemon.on('config:update', function () {
              setTimeout(function () {
                require('open')('http://localhost:8080/debug?port=5858', 'chromium-browser');
              }, 500);
            });
          }
        }
      }
    },

1