37
25

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Angular入門 未経験から1ヶ月でサービス作れるようにする その5. サービスとTypescriptの基礎

Last updated at Posted at 2018-07-30

前回は、

  • Angularでのデータ管理の基礎(モデル作成)
  • HTMLでの分岐(*ngIf)
  • HTMLでのループ(*ngFor)

を学びました。

本記事では、

  • ES6とTypescriptの基礎
    • Typescriptの型定義について
  • Angularでのデータ管理の基礎2
    • サービスクラスの作成

をやっていきます。

この記事のソースコード

https://github.com/seteen/AngularGuides/tree/入門その5

Typescriptの基礎

Angularでは、Typescriptを利用します。

今後の入門では、Typescriptのコードが増えていきますので、このあたりで基礎を学んでおきましょう。

型の指定

すでに何度か出てきましたが、 Typescript では、変数、メソッドそれぞれに型を定義します。

.ts
export class Test {
  name: string; // <- これがインスタンス変数

  toString(input: string): string { // <- これがインスタンスメソッド
    return input;
  }
}

このように、

  • 変数定義は 変数名: 型
  • メソッド定義は メソッド名(引数名: 引数名の型): メソッドの型 { メソッドの内容 }

と書く必要があります。

型の種類

Typescriptで用意されている型として、よく使うのは、

  • string : 文字列
  • number : 数値
  • boolean : 真偽値
  • void: 何も返さないメソッドの型

です。また、自分で定義したクラスなどのクラスも型となります。
また、配列については、 string[] のように型の後ろに [] をつけることで表現します。

.ts
names: string[]

また、 なんでも良い という型の any があります。

どの型を指定したら良いかわからないというときの最終手段です。

Interface

Typescript には、 Interface があります。
これは、Javaなどのように Class に継承するInterfaceとしても使えますが、
もう一つの使い方としてメソッドの引数などが複雑なときに利用することが多く、ObjectのKeyなどに制約をもたせることができます。

例を見ていきます。

.ts
export interface TestInterface {
  name: string;
  price: number;
  description?: string;
}

という Interface があり、

.ts
testMethod(input: TestInterface): void {
  ...
}

というTestInterfaceを引数に取るメソッドが合ったとします。

Interfaceを使うことによって、少し複雑な引数に対して制約をもたせることができ、上記のコードの意味は、 nameprice は必須で、 description には ? がついているので、これは optional(合ってもなくても良い) という意味になります。

そのため、例えばこのメソッドに対して、

.ts
this.testMethod({name: 'test'});

のように実行しようとすると、 price が存在しないため、エラーが出ます。

001.png

また、下記のように存在しない要素を追加してもエラーが出ます。

.ts
this.testMethod({name: 'test', price: 100, extend: 'feawfeaw'});

002.png

Enum

また、意外とよく使うのが、 Enum です。
これは、ある変数が決まった値しか取らないときなどに使います。
例えばステータスとして、 非公開( unpublished ) , 公開 ( published ) があり、
それ以外の値をとらない変数があるような場合

.ts
export enum Status {
  unpublished = 'unpublished',
  published = 'published'
}

と記載します。
もし数値しか取らないで、 非公開 (0) , 公開 (1) であったとすれば、下記のように記載できます。

.ts
export enum Status {
  unpublished,
  published
}

つまり、何も指定しなければ、上から順に 0 から数値が割り当てられます。

これを使う側としては、

.ts
export class TestClass {
  status: Status;
}

のように記載します。

これにより、 status には Status で定義した値しか入らない(静的に)ようになります。

Enum含めた型による制約の注意事項

string , number などの変数での型指定、 Enumによる値の制約、これらは、静的な解析時にのみ有効です。
そのため、動的に 別の型の値を入れられたとしても、その時点でエラーを起こしてくれたりはしません。(Javaなどとは違う)

これは、実行時には Javascript として実行されるため、Typescriptの恩恵は受けられないからです。例えばAPIで 本来は string を想定していたのに number が入っていたとして、
それを代入してもエラーになったりはしません。

型の制約は強力ですが、そのような場合もあるため、あまり頼りすぎて思考を停止しないように気をつけましょう。

TIPS: Typescriptの便利な記法
例えば params という変数が定義されているとすると、
{ params } と記載するだけで、 { params: params } と解釈してくれます。
地味に便利です。

private, protected, public

Typescript では、 private , protected , public という変数やメソッドのスコープを指定する修飾子が用意されています。

修飾子 スコープ
private 定義されたクラス内
protected 定義されたクラスとそれを継承しているクラス
public 外部からアクセス可能

この修飾子は、

.ts
export class Test {
  private name: string;

  protected testMethod(): string {
    ...
  }
}

のように使います。何も修飾子を指定しなければ、 public として扱われます。

Angularでのデータ管理の基礎2

Angularでは、APIアクセスやコンポーネントをまたがるデータの管理などを、 Service クラスを使って行うことができます。

今回は、 ProductService クラスを作って Product のデータ管理をコンポーネントの外に出してみましょう。

サービスクラスの作成

Angular CLI を用いてサービスを作成しましょう。

.shell
# ng g service shared/services/product
CREATE src/app/shared/services/product.service.spec.ts (380 bytes)
CREATE src/app/shared/services/product.service.ts (136 bytes)

今回は、 src/app/shared/services/ ディレクトリにサービスを配置したかったので、上記のような記載にしました。

作成されたファイルを見てみましょう。 spec.ts ファイル(テストファイルです)もできていますが、今は無視します。

src/app/shared/services/product.service.ts
import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class ProductService {

  constructor() { }
}

@Injectable というのは、サービスクラスに指定するおまじないです。
中に { providedIn: 'root' } が入っていますが、これはルートレベルでこのサービスを利用するということになります。

サービスクラスは、指定した providedIn の単位でインスタンス化され(今回だとルートレベル。つまりプロジェクト全体)、一つのインスタンスが使い回されます。
いわゆるシングルトンに近い状態と思っていただけると良いかと思います。

これに Product のリストを取得するメソッドを追加していきます。

最終的には、ここでAPIアクセスを行うようになりますが、今はAPIアクセスしないようにします。

src/app/shared/services/product-service.ts
import { Injectable } from '@angular/core';
import { Product } from '../models/product';
import { Observable, of } from 'rxjs/index';

@Injectable({
  providedIn: 'root'
})
export class ProductService {
  products = [
    new Product(1, 'Angular入門書「天地創造の章」', 3800, '神は云った。「Angularあれ」。するとAngularが出来た。'),
    new Product(2, 'Angularを覚えたら、年収も上がって、女の子にももてて、人生が変わりました!', 410, '年収300万のSEが、Angularと出会う。それは、小さな会社の社畜が始めた、最初の抵抗だった。'),
    new Product(3, '異世界転生から始めるAngular生活(1)', 680,
      'スパゲッティの沼でデスマーチ真っ最中の田中。過酷な日々からの現実逃避か彼は、異世界に放り出され、そこでAngularの入門書を拾う。現実逃避でさえ、プログラミングをするしかない彼に待ち受けるのは!?'),
  ];

  constructor() { }

  list(): Observable<Product[]> {
    return of(this.products);
  }
}

list() メソッドを追加しました。型は、 Observable<Product[]> を指定しています。

現状では、API通信がないので同期処理になりますが、最終的にはAPI通信を行うので list() メソッドは非同期処理になります。

Angularでは、非同期処理には RxJS を利用します。 Observable というのは RxJS のクラスで、非同期処理におけるメインのクラスになります。
今の所は、 Promise のようなものだと思っていただければ良いかなと思います。

of というのも RxJS のメソッドで、これは与えられた内容で Observable を作るというものです。

つまり上記コードは、 Observable (観測可能) な Product の配列を返すメソッドになります。

product-list.component.ts の変更

次に、 product-list.component.ts を変更していきます。

src/app/product/product-list/product-list.component.ts
import { Component, OnInit } from '@angular/core';
import { Product } from '../../shared/models/product';
import { ProductService } from '../../shared/services/product.service';

@Component({
  selector: 'app-product-list',
  templateUrl: './product-list.component.html',
  styleUrls: ['./product-list.component.scss']
})
export class ProductListComponent implements OnInit {
  products: Product[] = null;

  constructor(
    private productService: ProductService,
  ) {}

  ngOnInit() {
    this.productService.list().subscribe((products: Product[]) => {
      this.products = products;
    });
  }
}

元々は ngOnInit 内で Product の配列を作成していましたが、 ProductService を利用するようになりました。

constructor の解説

constructorProductService の引数を呼んでいます。

Angularでは、この記述により ProductService をコンポーネントやサービスに挿入できます。 (いわゆる DI )

つまり、この記載で ProductListComponent に、 productService という変数が生成されたことになります。

ngOnInit の解説

this.productService.list() は、 Obserbable<Product[]> を返します。

先程記載した通り、これは Observable (観測可能) な Product の配列を返します。

subscribe は、 Observable のメソッドで対象の Observable の観測を開始するものです(APIアクセスの非同期処理を開始)。

観測が完了すると、 subscribe の引数のコールバックメソッドが呼ばれます。この引数には、 Observable<Product[]>Product[] が入ることになります。

そのため、 ProductServicelist() メソッドで返した配列がこのメソッドに入ります。

つまり、

.ts
this.productService.list().subscribe((products: Product[]) => {

products に入っているということになります。

このメソッドの中で

.ts
this.products = products;

としてインスタンス変数に代入しています。
これにより、product-list.component.tsproducts を表示するように記載しているViewに反映されます。

ただ、今回で、前回の 3秒待つ という記述を削除したので、少しだけ挙動が変わりました(すぐに商品一覧が表示される)。

動作確認

ng serve で起動して、 http://localhost:4200/products にアクセスして挙動を確認してみましょう。

003.png

ここまでのコミット

product-detail.component.ts からの ProductServiceの利用

続いて、 product-detail.component.ts から ProductService を呼ぶように修正してみましょう。

ProductServiceにidで商品が取得できるメソッドを追加する

ProductService に、 get(id) を追加します。

src/app/shared/services/product.service.ts
import { Injectable } from '@angular/core';
import { Product } from '../models/product';
import { Observable, of } from 'rxjs/index';

@Injectable({
  providedIn: 'root'
})
export class ProductService {
  products = [
    new Product(1, 'Angular入門書「天地創造の章」', 3800, '神は云った。「Angularあれ」。するとAngularが出来た。'),
    new Product(2, 'Angularを覚えたら、年収も上がって、女の子にももてて、人生が変わりました!', 410, '年収300万のSEが、Angularと出会う。それは、小さな会社の社畜が始めた、最初の抵抗だった。'),
    new Product(3, '異世界転生から始めるAngular生活(1)', 680,
      'スパゲッティの沼でデスマーチ真っ最中の田中。過酷な日々からの現実逃避か彼は、異世界に放り出され、そこでAngularの入門書を拾う。現実逃避でさえ、プログラミングをするしかない彼に待ち受けるのは!?'),
  ];

  constructor() { }

  list(): Observable<Product[]> {
    return of(this.products);
  }

  get(id: number): Observable<Product> {
    return of(this.products[id - 1]);
  }
}

get(id) メソッドは、idで指定されたProductの Observable を返すというものです。

product-detail.component.ts からの利用

ほぼ product-list.components.ts と同様です。

src/app/product/product-detail/product-detail.component.ts
import { Component, OnInit } from '@angular/core';
import { Product } from '../../shared/models/product';
import { ProductService } from '../../shared/services/product.service';

@Component({
  selector: 'app-product-detail',
  templateUrl: './product-detail.component.html',
  styleUrls: ['./product-detail.component.scss']
})
export class ProductDetailComponent implements OnInit {
  product: Product;

  constructor(
    private productService: ProductService,
  ) {}

  ngOnInit() {
    this.productService.get(2).subscribe((product: Product) => {
      this.product = product;
    });
  }
}
  • constructor にて ProductService の変数を得る
  • ngOnInitget(id)subscribe (観測) して商品を取得しています
  • あと、特に理由はありませんが、取得するものを 2 に変えました

動作確認

ng serve で起動して、 http://localhost:4200/products/test にアクセスしてみましょう。

004.png

ここまでのコミット

まとめ

今回は、Typescriptの基礎 と サービスクラスを使ったデータ管理の方法を学びました。

Observable という非同期処理の話が出てきました。
少し難しく感じたかもしれませんが、これも使っていくうちにわかっていきますので(私も最初はわけわからん、と思いつつ使っていて自然とわかるようになりました)、まずは使っていきましょう。

次回は、 Angularでのフォームの作り方や、クリックイベントの受け取り方などを解説していきます。

Angular入門 未経験から1ヶ月でサービス作れるようにする その6. ngModelを使ったフォーム

入門記事一覧

「Angular入門 未経験から1ヶ月でサービス作れるようにする」は、記事数が多いため、まとめ記事 を作っています。
https://qiita.com/seteen/items/43908e33e08a39612a07

37
25
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
37
25

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?