Angular Tutorial 7章

コンポーネント、サービスの連携(P4233)

src/app/app.module.ts

import { NgModule, Provider } from '@angular/core'; // プロバイダインポート
import { BrowserModule } from '@angular/platform-browser';

import { AppComponent }  from './app.component';
import { BookService }  from './book.service'; // サービスを登録

@NgModule({
  imports:      [ BrowserModule ],
  declarations: [ AppComponent ],
  // providers:    [ BookService ], // サービスを登録(以下のより簡易的な書き方)

  // サービスを登録(依存注入の方法を決定)
  providers: [
    <Provider> { provide: BookService, useClass: BookService, multi: false }
  ],
  bootstrap:    [ AppComponent ]
})
export class AppModule { }

src/app/app.component.ts

import { Component, OnInit } from '@angular/core';
//import { Component, Inject, OnInit } from '@angular/core';

import { Book } from './book';
import { BookService } from './book.service'; // サービスを登録

@Component({
  selector: 'my-app',
  template: `
    <table class="table">
      <tr>
      <th>ISBNコード</th><th>書名</th><th>価格</th><th>出版社</th>
      </tr>
      <tr *ngFor="let b of books">
        <td>{{b.isbn}}</td>
        <td>{{b.title}}</td>
        <td>{{b.price}}円</td>
        <td>{{b.publisher}}</td>
      </tr>
    </table>
  `,
})
export class AppComponent implements OnInit {
  books: Book[];

  // constructor(private bookservice: BookService) {} // BookServiceを注入
  constructor(@Inject(BookService)private bookservice: BookService) {} // BookServiceを注入(上記より省略しない書き方)

  ngOnInit() {
    this.books = this.bookservice.getBooks(); // bookserviceのメソッドを実行
  }
}

src/app/book.service.ts

import { Injectable } from '@angular/core';

import { Book } from './book';

@Injectable() // Ingectableデコレーターを付与するとコンポーネントに対してサービスを引き渡せる
export class BookService {
  getBooks(): Book[] {
    return [
      {
        isbn: '978-4-7741-8411-1',
        title: '改訂新版JavaScript本格入門',
        price: 2980,
        publisher: '技術評論社',
      },
      {
        isbn: '978-4-7980-4853-6',
        title: 'はじめてのAndroidアプリ開発 第2版',
        price: 3200,
        publisher: '秀和システム',
      },
    ];
  }
}

src/app/book.ts

export class Book {
  isbn: string;
  title: string;
  price: number;
  publisher: string;
}

依存性注入(P4345)

https://github.com/YoheiMiyamoto/angular-tutorial/pull/3

useClassプロパティとvalueClassプロパティ

src/app/app.module.ts

import { NgModule }      from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppComponent }  from './app.component';
import { UseService }  from './use.service';
import { UseComponent }  from './use.component';

@NgModule({
  imports:      [ BrowserModule ],
  declarations: [ AppComponent, UseComponent  ],
  bootstrap:    [ AppComponent ]
})
export class AppModule { }

src/app/app.component.ts

import { Component } from '@angular/core';
import { UseService } from './use.service';

@Component({
  selector: 'my-app',
  template: `
    <ul>
      <my-use></my-use>
      <my-use></my-use>
      <my-use></my-use>
    </ul>
  `,
})
export class AppComponent { }

src/app/use.component.ts

import { Component } from '@angular/core';
import { UseService } from './use.service';

@Component({
  selector: 'my-use',
  providers: [
    { provide: UseService, useClass: UseService } // useComponentが生成される度にuseServiceが生成される(すべて違う時間が出力される)
    // { provide: UseService, useValue: new UseService() }, // useComponentが生成。(すべて同じ時間が出力される)

    // useFactoryを使っての注入は以下。
    // { provide: UseService, useFactory: () => {
    //     let service = new UseService();
    //     service.created.setSeconds(0);
    //     service.created.setMilliseconds(0);
    //     return service;
    //   }
    // }  

],
  template: `<li>UseService:{{current}}</li>`
})
export class UseComponent {
  current: string;

  constructor(private use: UseService) {
    this.current = use.show();
  }
}

src/app/use.service.ts

import { Injectable } from '@angular/core';

@Injectable()
export class UseService {
  created: Date;

  constructor() {
    this.created = new Date();
    this.sleep(2000);
  }

  show() {
    return this.created.toLocaleString();
  }

  sleep(delay: number) {
    let tmp1 = new Date();
    while (true) {
      let tmp2 = new Date();
      if (tmp2.getTime() - tmp1.getTime() > delay) {
        return;
      }
    }
  }
}

ngModuleでプロバイダーを生成した場合

ngModuleでプロバイダーを生成した場合、 useClassで、useComponentを3回呼び出しても、serviceオブジェクトは一度しか生成されない。
providerは常にシングルトンのインスタンスを生成する。Componentのprovidersに指定されたproviderはそのコンポーネントの範囲でシングルトンにインスタンスを生成する

import { NgModule }      from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppComponent }  from './app.component';
import { UseService }  from './use.service';
import { UseComponent }  from './use.component';

@NgModule({
  imports:      [ BrowserModule ],
  declarations: [ AppComponent, UseComponent  ],
  providers: [{ provide: UseService, useClass: UseService }], // プロバイダー生成
  bootstrap:    [ AppComponent ]
})
export class AppModule { }

途中省いている

非同期処理をサービスに分離(P8539)

src/app/app.module.ts

import { NgModule }      from '@angular/core';
import { FormsModule }   from '@angular/forms';
import { JsonpModule }  from "@angular/http"; // JsonpModuleをインポート
import { BrowserModule } from '@angular/platform-browser';

import { AppComponent }  from './app.component';

@NgModule({
  imports:      [ BrowserModule, FormsModule, JsonpModule ], // JsonpModuleをインポート
  declarations: [ AppComponent ],
  bootstrap:    [ AppComponent ]
})
export class AppModule { }

src/app/hatena.service.ts

import { Injectable } from '@angular/core';
import { Jsonp, URLSearchParams } from '@angular/http';
import { Observable } from 'rxjs/Observable';

import 'rxjs/add/observable/throw';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/catch';

@Injectable()
export class HatenaService {

  constructor(private jsonp: Jsonp) { }

  requestGet(url: string): Observable<any> {
    let params = new URLSearchParams();
    params.set('url', url);
    params.set('callback', 'JSONP_CALLBACK');

    return this.jsonp.get('http://b.hatena.ne.jp/entry/jsonlite/', { search: params })
      .map(
        response => {
          return response.json() || {};
        }
      )
      .catch(
        error => {
          return Observable.throw(error.statusText);
        }
      );
  }
}

src/app/app.component.ts

import { Component } from '@angular/core';
import { Jsonp, URLSearchParams }  from '@angular/http';
import { HatenaService } from './hatena.service'; // serviceインポート

@Component({
  selector: 'my-app',
  providers: [ HatenaService ],
  template: `
  <form>
    <label for="url">URL:</label>
    <input id="url" name="url" type="url" size="50" [(ngModel)]="url" />
    <button (click)="onclick()">検索</button>
  </form>
  <div>{{count}}件</div>
  <ul>
    <li *ngFor="let comment of comments">{{comment}}</li>
  </ul>
  `,
})
export class AppComponent {
  url = 'http://gihyo.jp/';
  count = 0;
  comments: string[] = [];

  constructor(private hatena:HatenaService) {} // serviceのインポート

  onclick() {
    this.hatena.requestGet(this.url) // serviceで定義したメソッドの実行
      .subscribe(
        data => {
          let result: string[] = [];
          data.bookmarks.forEach(function(value: any) {
            if (value.comment !== '') {
              result.push(value.comment)
            }
          });
          this.comments = result;
          this.count = data.count;
        },
        error => {
          this.count = 0;
          this.comments = [];
          console.log('はてなサービスのアクセスに失敗しました。');
        }
      );
  }
}

Provideプロパティ名の変更(P4458)

use.component.ts

import { Component, Provider, Inject } from '@angular/core';
import { UseService } from './use.service';
import { AliasService } from './alias.service';
import { Hoge } from './hoge'; // 実体は不要

@Component({
  selector: 'my-use',
  providers: [
     { provide: Hoge, useClass: UseService }, // 注入方法設定(provide名を変更)
  ],
  template: `<li>UseService:{{current}}</li>`
})
export class UseComponent {
  current: string;

  // 注入(実際の型アノテーション[UseService]と注入型トークン[Hoge]が名称が異なるため以下のように@Injectアノテーションが必要。ただ、そのようなケースはあまりない)
  constructor(@Inject(Hoge) private use: UseService) {
    this.current = use.show();
  }
}

hoge.ts

Hogeはあくまでトークン識別のためのキーなので実体は不要

export class Hoge {}

非クラス型の注入(P4475)

app-info.ts

import { InjectionToken } from '@angular/core';

export const MY_APP_INFO = {
  title: 'Angularサンプル',
  author: 'YAMADA, Yoshihiro',
  created: new Date(2017, 2, 14)
};

// my.app.info は説明文
export let APP_INFO = new InjectionToken('my.app.info');

use.component.ts

import { Component, Provider, Inject } from '@angular/core';
import { UseService } from './use.service';
import { AliasService } from './alias.service';
import { Hoge } from './hoge';
import { APP_INFO, MY_APP_INFO } from './app-info';

@Component({
  selector: 'my-use',
  providers: [
    { provide: APP_INFO, useValue: MY_APP_INFO },
  ],
  template: `<li>UseService:{{current}}</li>`
})
export class UseComponent {
  current: string;

  // 注入
  constructor(@Inject(APP_INFO)private info: any) {
    console.log(info);
  }
}

単一のトークンに複数のサービスを紐付ける(P4543)

info.ts

import { InjectionToken } from '@angular/core';

export const KEYWORDS = new InjectionToken('keyword');

use.component.ts

import { Component, Provider, Inject } from '@angular/core';
import { KEYWORDS } from './info';

@Component({
  selector: 'my-use',
  providers: [
    // 以下の場合、最後のAngular2のみKEYWORDSに登録される
    { provide: KEYWORDS, useValue: 'Angular1' },
    { provide: KEYWORDS, useValue: 'Angular2' }

    // 以下の書き方だと両方のサービスが登録される
    // { provide: KEYWORDS, useValue: 'Angular1', multi: true },
    // { provide: KEYWORDS, useValue: 'Angular2', multi: true }
  ],
  template: `<li>UseService:{{current}}</li>`
})
export class UseComponent {
  constructor(@Inject(KEYWORDS)private keywords: string) {
    console.log(keywords);
  }

  // multiでサービスを登録した場合は以下で呼び出す
  // constructor(@Inject(KEYWORDS)private keywords: string[]) {
  //   console.log(keywords);
  // }
}

インジェクターの階層構造(P4593)

app.component

import { Component, OnInit, Provider } from '@angular/core';
import { UseService } from './use.service';

@Component({
  selector: 'app-root',
  template: `
    <my-use></my-use>
    <my-use></my-use>
    <my-use></my-use>
  `,
  styleUrls: ['./app.component.css'],
  // app.componentで、サービスをプロバイダ登録する(useClassで登録)
  providers: [
    { provide: UseService, useClass: UseService },
  ]

})
export class AppComponent { }

use.component

注入する際に、use.componentの中からプロバイダを検索する。存在しないのでapp.componentのプロバイダを検索し、
それを使う。app.componentは一度しか初期化されないので、currentの時間はすべて同じものになる。

import { Component, Provider, Inject } from '@angular/core';
import { UseService } from './use.service';

@Component({
  selector: 'my-use',
  providers: [ ],
  template: `<li>UseService:{{current}}</li>`
})
export class UseComponent {
  current: string;

  // 注入(app.componentで登録されたサービスを使う)
  constructor(private use: UseService) {
    this.current = use.show();
  }
}

アプリ全体で使うサービスは、ルートモジュールでプロバイダ登録する(P4624)

main.ts

import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

import { AppModule } from './app/app.module';
import { environment } from './environments/environment';

import { UseService }  from './app/use.service';

if (environment.production) {
  enableProdMode();
}

platformBrowserDynamic().bootstrapModule(AppModule, {
  providers: [
    { provide: UseService, useClass: UseService },
  ],
}).catch(err => console.log(err));