グルチャ

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

maxresdefault

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
    });
});

Leave a reply

Your email address will not be published.

Time limit is exhausted. Please reload CAPTCHA.

You may use these HTML tags and attributes:

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>