Typescriptでモバイルアプリケーションを作ってみる2: Class化、webpack編

[前回](http://qiita.com/takeo-asai/items/52bcfc76c3f3455e26bb)はほとんどIonicの紹介で終わってしまったので、今回はIonicをTypescriptらしいClassを使った書き方へと変更することと、webpackを使ってコンパイルしてJSへと変換するところまでを扱います。

実際に手を動かしてみたい方は前回の終了時点のコードを用意してありますのでご利用ください。

```bash
$ git clone https://github.com/takeo-asai/ionic-begin2.git
```

 _master_ ブランチが前回終了時点、_class_ がClassを使った書き換え、
 _webpack_ が今回終了時のものになるのでお好きに切り替えてください。

# IonicをClassを使って書き換える

何も特別なことはなく、Ionicではfunctionで書いていたところをClassに置き換えるだけとお手軽です。この時DIしていたものはconstructor部分に移動させてください。Angularのお作法ではDIするときにはminify対策のために文字列を入れるべきとなっています。これをTypescriptで書く場合にはJavascriptでの場合とは違い、

```ts
class DashCtrl {
  public static $inject = ["$scope"];
  constructor($scope) { }
}
```
といった感じに書けます。
しかし順番を間違えたり忘れたりといったことがあるのでinjectを自動で挿入してくれるng-annotateというプラグインを導入することでその問題を回避できます。

今回TypescriptのClassで置き換えるのは、controllerとserviceの2つです。
まずはcontrollerですが、以下のように書き換えることが可能です。

```ts:controllers.ts
class DashCtrl {
  public static $inject = [""];
  constructor() { }
}

class ChatsCtrl {
  public chats;
  constructor(private Chats) {
    "ngInject";
    this.chats = Chats.all();
  }
  public remove(chat) {
    this.Chats.remove(chat);
  }
}

class ChatDetailCtrl {
  public chat;
  constructor($stateParams, Chats) {
    "ngInject";
    this.chat = Chats.get($stateParams.chatId);
  }
}

class AccountCtrl {
  public settings = {
    enableFriends: true
  };
  constructor() { }
}

angular.module('starter.controllers', [])
  .controller('DashCtrl', DashCtrl)
  .controller('ChatsCtrl', ChatsCtrl)
  .controller('ChatDetailCtrl', ChatDetailCtrl)
  .controller('AccountCtrl', AccountCtrl);
```

とりあえずクラスごとのファイル分割はしていません。
ng-annotateが正しくDIの位置を認識できるよう直後に

```ts
"ngInject";
```
と入れています。なくても認識して文字列を自動挿入してくれますが、書き方次第でうまくいかないことがあるので出来れば毎回記述すべきです。


Classを使った書き方へ変更すると、\$scopeが必要なくなります。現在のAngularjsでも推奨されない書き方ですし、Angular2ではそもそも \$scopeが廃止されていますので好ましい書き方になります。（が、Ionic1では一部機能(modalViewなど)を利用する場合には$scopeを使う必要があり回避不能です。）

 \$scopeを使わない場合にClass内部のインスタンス変数などをViewから呼び出すにはrouteを少し変更する必要があり、controllerAsで適当な文字列を指定する必要があります。

```ts:app.ts
  .state('tab.chats', {
      url: '/chats',
      views: {
        'tab-chats': {
          templateUrl: 'templates/tab-chats.html',
          controller: 'ChatsCtrl',
          controllerAs: "$ctrl",
        }
      }
    })
```

こうしてcontrollerAsを指定してやることで、該当のViewから

```html:templates/tab-chats.html
ng-repeat="chat in $ctrl.chats"
```

指定された名前を通じてClassのpublicなメソッド、インスタンスにアクセスすることができます。（実はprivateにもアクセスできますがおすすめはしません）


続けてserviceを書き換えます。
Angularはserviceとfactoryという非常に似ているが違う機能が用意されています。 _ionic start myApp tabs_ で作ったサンプルの中ではfactoryを使っているのですが、Classを直接置くことのできるserviceの方がTypescriptで書く分には優れているように感じます。

```ts:services.ts
class Chats {
  private chats = [{
    id: 0,
    name: 'Ben Sparrow',
    lastText: 'You on your way?',
    face: 'img/ben.png'
  }, {
      id: 1,
      name: 'Max Lynx',
      lastText: 'Hey, it\'s me',
      face: 'img/max.png'
    }, {
      id: 2,
      name: 'Adam Bradleyson',
      lastText: 'I should buy a boat',
      face: 'img/adam.jpg'
    }, {
      id: 3,
      name: 'Perry Governor',
      lastText: 'Look at my mukluks!',
      face: 'img/perry.png'
    }, {
      id: 4,
      name: 'Mike Harrington',
      lastText: 'This is wicked good ice cream.',
      face: 'img/mike.png'
    }];
  constructor() { }

  public all() {
    return this.chats;
  }
  public remove(chat) {
    this.chats.splice(this.chats.indexOf(chat), 1);
  }
  public get(chatId) {
    for (var i = 0; i < this.chats.length; i++) {
      if (this.chats[i].id === parseInt(chatId)) {
        return this.chats[i];
      }
    }
    return null;
  }
}

angular.module('starter.services', [])
  .service('Chats', Chats);
```


# webpackでコンパイルする

SCSSからCSSへのコンパイルについてはIonicのデフォルトの設定がやってくれるため敢えてやらないことにします。

webpackは非常に多機能でJavascriptを扱うだけでなく、画像すらrequireしてJSファイルへと取り込めるほど協力ですが、その分設定がややこしくなるのが難点です。

```bash
$ npm i -S ng-annotate-loader ts-loader webpack
```
webpackでTypescriptを扱うのに最低限必要なものに、Class化で触れたng-annotateを加えた構成です。これらを使いIonicをTypescriptでコンパイルしたうえでminifyを安全に行うwebpackの設定が下記のものです。

```js:webpack.config.js
var webpack = require('webpack');
var path = require('path');
module.exports = {
  entry: {
    app: './src/app.ts'
  },
  output: {
    filename: 'www/js/[name].js'
  },
  resolve: {
    root: [
      path.join(__dirname, 'node_modules')
    ],
    extensions: ['', '.ts', '.webpack.js', '.web.js', '.js']
  },
  moduleDirectories: [
    'node_modules'
  ],
  watch: true,
  cache: true,
  devtool: 'eval',
  module: {
    loaders: [
      { test: /\.ts$/, loaders: ['ng-annotate', 'ts-loader'] },
    ]
  },
  plugins: [
    new webpack.optimize.UglifyJsPlugin({ minimize: true }),
  ]
}
```

_src/app.ts_ をエントリーポイントとして _www/js/app.js_ を出力します。ng-annotateでminify対策もしてあります。

よくある普通のwebpack活用例とは違い、Ionicアプリを作成する場合にはよく使われるwebpackDevServerは必要ないかと思われます。なぜならば _ionic serve_ としてやるだけでIonicが簡易サーバーを用意してくれるからで、差分ビルドといった機能は _watch: true_ で十分機能します。

webpackの基本的な設定が終わりましたら、次はエントリーポイントを指定しただけでは他の必要なファイルは読み込まれないため必要な物をimportします。

```ts:app.ts
import "./controllers";
import "./services";
```

また、webpackの起動をいちいちコマンドから行うのもアレですのでgulpにタスクを追加します。

```js:gulpfile.js
var webpack = require('webpack');
var webpackConfig = require('./webpack.config');
gulp.task('webpackWatch', function () {
  webpack(webpackConfig, function (err, stats) {
    if (err) throw new gutil.PluginError("webpack", err);
    gutil.log("[webpack]", stats.toString({
      // output options
    }));
  });
});
```

先ほど設定したとおり _watch: true_ ですのでファイルを保存すればwebpackが自動でTypescriptからJSへのコンパイルを行い、
その結果 _ionic serve_ されているアプリもlivereloadから自動で更新されるようになります。

```bash
$ gulp webpackWatch
```

お疲れ様でした。

# 次回

次回はユニットテスト、e2eテストをCircleCI上で行えるよう設定をしていくことを考えていたのですが、webpackの設定が思っていたよりも長く掛かりまして次回もwebpackのお話となります。今回もお読みいただきありがとうございました。