Aurelia hack 05 ～DI を便利に使ってみる～


# Aurelia Dependency Injection

通常 DI というと、各サービスの interface に対するインスタンスの生成方法などを事前に定義するイメージがあると思いますが、Aurelia の場合は必須ではありません。

## @inject()

```js:src/services.js
// サービスとして使うクラスの定義
export class SomeService {
    _count = 0;
    get count() {
        return ++this._count;
    }
}
```

例えば上記のような、カウンタを持ったサービスを定義してみます。
これを使うにはただ↓のように inject するだけでよいのです。

```js:src/example1.js
import {inject} from 'aurelia-framework'
import {SomeService} from 'services'

@inject(SomeService)
export class Example1 {
    
    constructor(someService) {
        console.log(someService.count); // -> 1
    }
    
}
```

単にコンテナがインスタンス化してくれるだけでなく、ちゃんとシングルトンになっています。同じサービスを別のクラスで inject してカウンタを参照してみると、数が増えていることが確認できます。つまり、一度生成されたインスタンスが使いまわされています。

```js:src/example1.js
import {inject} from 'aurelia-framework'
import {SomeService} from 'services'

@inject(SomeService)
export class Example2 {
    
    constructor(someService) {
        console.log(someService.count); // -> 2
    }
    
}
```

## @transient()

シングルトンではなく毎回別のインスタンスとして生成してほしい場合は、サービスクラスに `@transient()` というデコレータをつけます。

```js:src/services.js
import {transient, inject} from 'aurelia-framework'
import {HttpClient} from 'aurelia-fetch-client'
import {Dep1, Dep2} from 'other-services'

@transient()           // inject のたびに新しい SomeService インスタンスが生成される
@inject(HttpClient, Dep1, Dep2)    // もちろんサービスにも inject できる
export class SomeService {

    constructor(client, dep1, dep2) { // inject に書いた順番に引数となる
        this.client = client;
        this.dep1 = dep1;
        this.dep2 = dep2;
    }
    
    _count = 0;
    get count() {
        return ++this._count;
    }
}
```

## 自分で DI コンテナに突っ込む方法

もちろん自前のインスタンスを登録することもできます。
コンテナにアクセスするために main.js などの初期設定部分で行います。

```js:src/main.js
import {SomeService} from 'services'

export function configure(aurelia) {
    // プラグイン設定部分
    aurelia.use
        .standardConfiguration()
        .developmentLogging()
        .feature('resources');
        
    // インスタンスを生成
    let someService = new SomeService();
    // DI に登録する
    aurelia.container.registerInstance(SomeService, someService);
    
    aurelia.start().then(a => a.setRoot());
}
```

上記のように `aurelia.container.registerInstance` を使うことで、自前のインスタンスを登録することができます。しかし SomeService 自体がほかのサービスに依存している (= inject される) 場合、自分でインスタンス化することができません。

それでも初期設定の時点でサービスをセットアップしたい場合は、一度コンテナにインスタンス化してもらうという手があります。

```js:src/main.js
import {SomeService} from 'services'

export function configure(aurelia) {
    // プラグイン設定部分
    aurelia.use
        .standardConfiguration()
        .developmentLogging()
        .feature('resources');
        
    // インスタンスを生成 ...登録しなくてもインスタンス化できる利点
    let someService = aurelia.container.get(SomeService);
    // データを設定するなど
    someService._count = 100;
    
    aurelia.start().then(a => a.setRoot());
}
```

あとからサービスがほかのサービスに依存するとなった場合に修正が大変なので、インスタンス化は最初からコンテナに任せたほうがいいかもしれませんね。

# 自作 API クライアント

[前回](http://qiita.com/sukobuto/items/3892635e0caea19a2402) 宣言したとおり、キャッシュ機能を持った ApiClient クラスを自作してみましょう。
前回使用した SuperAgent を使いますので、jspm でインストールされていることを前提とします。

さらにキャッシュの有効期限管理のために [moment.js](http://momentjs.com/) を使います。

```shell-session:moment.jsインストール
[you@server aurelia-skeleton-navigation]$ jspm install npm:moment
```

```js:src/services.js
import superagent from 'superagent'
import moment from 'moment'

export class ApiClient {
    
    token;      // セキュリティトークン
    cache = {}; // キャッシュデータ格納オブジェクト
    
    /**
     * キャッシュを保存する
     * @param {string} key
     * @param {any} content
     */
    saveCache(key, content) {
        this.cache[key] = {
            content,
            since: moment().unix()
        };
        return this;
    }
    
    /**
     * キャッシュを取得する
     * @param {string} key
     * @param {number} ttl
     */
    getCache(key, ttl) {
        if (!this.cache.hasOwnProperty(key)) return null;
        let item = this.cache[key];
        if (item.since < moment().unix() - ttl) {
            this.purgeCache(key);
            return null;
        }
        return item.content;
    }
    
    /**
     * キャッシュを削除する
     * @param {string} key
     */
    purgeCache(key) {
        if (this.cache.hasOwnProperty(key)) {
            delete this.cache[key];
        }
    }
    
    /**
     * プレフィクスを指定してキャッシュを削除する
     * @param {string} prefix
     */
    purgeCaches(prefix) {
        for (var key in this.cache) {
            if (this.cache.hasOwnProperty(key) && key.lastIndexOf(prefix, 0) === 0) {
                delete this.cache[key];
            }
        }
    }
    
    /**
     * GET リクエスト
     * @param {string} url リクエストURL
     * @param {any} [params] リクエストパラメータ
     * @param {string} [cacheKey] キャッシュ保存キー
     * @param {number} [cacheTtl] キャッシュ有効秒数
     */
    async get(url, params, cacheKey, cacheTtl = 60) {
        // キャッシュチェック
        if (cacheKey) {
            let content = this.getCache(cacheKey, cacheTtl);
            if (content) return content;
        }
        
        // セキュリティトークンがある場合は付与
        params = params || {};
        if (this.token) params._token = this.token;
        let res = await new Promise((fulfilled, rejected) => {
            // GET リクエスト送信
            superagent.get(url)
                .query(params)
                .end((err, res) => {
                    if (err) rejected(err);
                    fulfilled(res.body);
                });
        });
        
        // キャッシュ保存
        if (cacheKey) {
            this.saveCache(cacheKey, res);
        }
        return res;
    }
    
	/**
	 * POST request
	 * @param {string} url リクエストURL
	 * @param {Object} [params] リクエストパラメータ(フォームデータ)
	 * @param {NodeList|Node} [files] input[type="file"] またはその NodeList
	 */
    async post(url, params, files) {
			params = params || {};
			if (this.token) params._token = this.token;
			let file_input;
			let req = superagent.post(url);
			for (var prop in params) {
				if (params.hasOwnProperty(prop)) {
					req.field(prop, params[prop]);
				}
			}
            // ファイルがある場合は添付する
			if (files) {
				if (files.hasOwnProperty(length)) {	// when the NodeList
					for (var i = 0; i < files.length; i++) {
						file_input = files[i];
						req.attach(file_input.name, file_input.files[0], file_input.value);
					}
				} else {	// when an Element
					file_input = files;
					req.attach(file_input.name, file_input.files[0], file_input.value);
				}
			}
            let res = await new Promise((fulfilled, rejected) => {
                req.end((err, res) => {
                    if (err) rejected(err);
                    fulfilled(res.body);
                });
            });
            return res;
    }
    
}
```

GET リクエストではキャッシュができて、POST リクエストではファイルアップロードもできるようになっています。ファイルをアップロードする場合は `post` の第三引数に `<input type="file" name="file1">` のような入力エレメントを渡せばOKです。

```js:src/main.js(初期化例)
import {ApiClient, Account} from 'services'

export function configure(aurelia) {
    // プラグイン設定部分
    aurelia.use
        .standardConfiguration()
        .developmentLogging()
        .feature('resources');
        
    let client = aurelia.container.get(ApiClient);
    // 初期化 API を呼び、ログインユーザ情報やトークンなどを取得する
    client.get('api/init')
        .then(res => {
            let account = aurelia.container.get(Account);
            account.setData(res.account);
            client.token = res.token;
            // 初期化後にアプリケーションを起動
            aurelia.start().then(a => a.setRoot());
        });
}
```

実際にページで使う例です。

```js:src/example1.js(使用例)
import {inject} from 'aurelia-framework'
import {ApiClient, Account} from 'services'

@inject(ApiClient, Account)
export class Example1 {

    constructor(client, account) {
        this.client = client;
        this.account = account;
    }
    
    async activate() {
        // 'articles:all' というキーで10分間有効なキャッシュを使う
        let res = await this.client.get('api/getArticles', null, 'articles:all', 600);
        this.articles = res.articles;
    }
    
    purge() {
        // キーを指定してキャッシュを消す例(データを変更した後など)
        this.client.purgeCache('articles:all');
        // プレフィックスを指定してキャッシュを消す例
        this.client.purgeCaches('articles:');
    }

}
```

今回はここまでです。
今後の記事でも DI はばんばん使っていきます。