Vue 経験者が Angular のチュートリアルを通して概要を掴む

Vue
Vueフロントエンド

背景

これまで React や Vue を使った開発はしたことありますが、Angular を使ったことはありませんでした。今後関わることがありそうなので、概要を掴むためにチュートリアルをやってみました。

チュートリアル含め、Angular の公式ドキュメントは非常に充実しています。

https://angular.jp/start
ので、ここでは体系だって書いてるというよりは、Vue の基本を知っている前提で、Angular を触ってみたときのメモです(ドキュメントが多すぎるので、ざっと雰囲気をつかむ)。Vue にもほぼ同等のものがある場合は Vue の該当する機能を記載しています。

今後も勉強しつつ更新していければと思います。

対象

Vue を知っていて、Angular の概要を知りたい人

先に感想

Angular はもともと、学習コストが高いと言われていました。
正直、とっかかりが難しいはその通りだなというのが感想。プログレッシブに進められる他のライブラリと比較すると理解すべき概念が多い / どれを理解したら Angular の最初のステップを理解したことになるのか、あたりで ??、となりました(今も道半ばですが)。

ただ、個人的には、Angular が他に比べて学習コストが高いというよりは、単に「フレームワークか否か」ということかなとも思います。
React でも Vue でも、これ単体でアプリケーションを作ることはなく、様々な他の仕組みを組み合わることになりますが、それら周辺をすべて理解し選定し導入するに相当するのが Angular の全貌の理解なのかなと。

なお、https://2021.stateofjs.com/en-US/libraries/front-end-frameworks/ では、年々 Angular の人気は下がっている模様です。

https://2021.stateofjs.com/en-US/libraries/front-end-frameworks/
https://2021.stateofjs.com/en-US/libraries/front-end-frameworks/

流行り廃りも激しい領域なので仕方ないですが、ただ、繰り返しになりますが、ライブラリというよりかは、本当にアプリケーションを開発するためのフレームワークという感じです。エコシステム含め公式できちっとされている分、業務アプリケーションを作る場合にはこっちの方が向いてるなと思いました。リリースポリシーも明確ですし。

同列に語れるフレームワークはそうないと思うので今後も期待したいところです(あれば教えてください)。

インストール

以下を実行して cli をインストールします。

npm install -g @angular/cli

コマンド

他のライブラリ同様、dev サーバ起動やテスト、ビルドのコマンドがあります。
また、Vue にはないものとしては、各種役割(component / service / パイプ など)に応じた雛形をジェネレートしてくれる仕組みがあります。

// プロジェクト作成
ng new angular-tour-of-heroes

// ビルド vue の yarn build
ng build

// dev サーバ起動。yarn serve
ng serve

// Component や Service のテンプレートを生成してくれます
// 生成できる一覧はこちら https://angular.jp/cli/generate
// 例:次のコマンドを実行すると、以下が一式作成されます。
// hoge
//  - hoge.component.html
//  - hoge.component.scss
//  - hoge.component.spec.ts
//  - hoge.component.ts
ng generate component hoge

// 単体テスト実行。karma とのこと。yarn test:unit
ng test

// e2e テスト実行
// 以下によると、angular 標準に含まれていた protector というツールのサポートが angular 15 で終わるとのこと
// https://www.alpha.co.jp/blog/202204_01
ng e2e

アーキテクチャ

Angular の基本的な構成要素は、NgModulesで組織されたAngularコンポーネントとのこと。

NgModule

  • アプリケーションドメイン、ワークフロー、あるいは一連の機能と密接に関連するコンポーネントセット。すべてのAngularアプリケーションには、通常は AppModule という名前の ルートモジュールがあり、ここからはじまる。
  • JavaScript のモジュールと同様に、NgModule は他の NgModule から機能をインポートしたり、独自の機能をエクスポートして他の NgModule から使用できるようにすることが可能。
  • NgModule の単位でわけると遅延ロードできる(他の方法もありそう)。
  • ライブラリを作る場合、この単位にする。

NgModule 例

  • declarations :利用するコンポーネントやディレクティブ、パイプ
  • import:使用する他の NgModule
  • exports:他の NgModule で使用可能なコンポーネント
  • provider:アプリケーションが利用するサービスを記載
  • bootstrap:アプリケーションは、ルートのAppModuleをブートストラップすることで起動する(エントリポイント)。 その中で、ここでリストされているコンポーネントを作成しDOMに挿入する
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms'; 
import { HttpClientModule } from '@angular/common/http';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { HeroesComponent } from './heroes/heroes.component';
import { HeroDetailComponent } from './hero-detail/hero-detail.component';
import { MessagesComponent } from './messages/messages.component';
import { DashboardComponent } from './dashboard/dashboard.component';
import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api';
import { InMemoryDataService } from './in-memory-data.service';
import { HeroSearchComponent } from './hero-search/hero-search.component';

@NgModule({
  declarations: [
    AppComponent,
    HeroesComponent,
    HeroDetailComponent,
    MessagesComponent,
    DashboardComponent,
    HeroSearchComponent,
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    FormsModule,
    HttpClientModule,
    HttpClientInMemoryWebApiModule.forRoot(
      InMemoryDataService, { dataEncapsulation: false}
    )
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Component

  • ビューを定義(Vueも同じ)
  • ルートコンポーネントがありそこから他のコンポーネントが階層で存在する。
https://angular.jp/ コンポーネントの関係
https://angular.jp/ コンポーネントの関係
  • コンポーネントのうち、ビューに直接関係しない特定の機能を提供するものを サービス と呼ぶ
  • サービスは、プロバイダーとして、コンポーネントに注入することができる(DI:依存性の注入)。また、コードがモジュール化され、再利用も可能になる。
  • 全体のイメージは以下(公式より)
https://angular.jp/ 全体イメージ
https://angular.jp/ 全体イメージ

コンポーネント例

  • @Component デコレータをつける
    • ng generate component <コンポーネント名> で生成できる。
    • Vue のクラスコンポーネント的な感じ。以下は、Vue の SFC を分割しているイメージ。
    • 指定オプション
      • selector: コンポーネント名
      • template:コンポーネントの html(基本は下の templateUrl)
      • templateUrl: コンポーネントの html
      • styleUrls: コンポーネントの Style css
  • 定義した class にロジックを書く。ここで宣言した変数が template で使える。
import { Component } from '@angular/core';

@Component({
  selector: 'hello-world', 
  template: `
    <h2>Hello World</h2>
    <p>This is my first component!</p>
  `
})
export class HelloWorldComponent {
  // The code in this class drives the component's behavior.
}

// 次のようにして使える
<hello-world></hello-world>

Template(html)

  • {{ 変数 }} でコンポーネントクラスで定義したメンバ変数を利用可能(vue でも同じ)
  • # でローカル変数も定義できる(指定した要素の DOM 要素が変数でアクセス可能になる)。
  • [属性] で要素属性に変数(おそらく式も)が使える(vue だと :属性
  • (イベント) でイベントハンドラ(vue だと @イベント
  • [(ngModel)]=”hero.name” で v-model 。双方向バインディング
  • | パイプ使える。vue 3ではなくなったけど、angular は普通にある。@Pipe でカスタムのパイプが作成可能。
<p
  [id]="sayHelloId"
  [style.color]="fontColor">
  {{ message }}
</p>

<button
  type="button"
  [disabled]="canClick"
  (click)="sayMessage()">
  Trigger alert message
</button>


// Angular では、タグにシャープ(#)から始まる変数名をつけることで、HTML 内で使えるローカル変数を定義することができる。
// 例えば、次のように書くことで、 input タグに name というローカル変数名を付けられる。
// 宣言したローカル変数は、同じ HTML 内からアクセス可能。
// <input> 要素などの HTML 標準タグに変数名を付けた場合、変数の値はネイティブの HTML 要素なり、
// Angular コンポーネントのタグに変数名をつけると、変数の中身はコンポーネントのインスタンスになります。
<input type="text" #name>
<button (click)="onClick(name.value)">SET</button>

構造ディレクティブ

コンポーネントも技術的にはディレクティブ。
構造ディレクティブと属性ディレクティブがある。
@Directive で宣言可能。
構造ディレクティブは ng-if とか ng-for のこと。

  • *ngIf で if-else ができる。else は ng-template というのを使う?(vue だと v-if
  • ngForOf が for。*ngFor は非推奨とのこと。(vue だと v-for
// *ngIf
<div *ngIf="condition; else elseBlock">
	condition === true の場合こちら
</div>
<ng-template #elseBlock>
	condition === false の場合はこっちがレンダリングされる
</ng-template>


// ngForOf
// * は短縮形
// let i=index で配列のインデックスが使える
// trackBy は一意な id 
//   .ts ファイルで trackByItems(index: number, item: Item): number { return item.id; }
//   などメソッドを用意したりする。
// Vue では :key
<div 
	*ngFor="let hero of heroes; let i=index; let odd=odd; trackBy: trackById"
	[class.odd]="odd">
  ({{i}}) {{hero.name}}
</div>

// 短縮じゃない版
<ng-template 
	ngFor let-hero [ngForOf]="heroes" let-i="index" let-odd="odd" [ngForTrackBy]="trackById">
  <div [class.odd]="odd">({{i}}) {{hero.name}}</div>
</ng-template>

属性ディレクティブ

ngModel など。以下は上で書いたように双方向バインディング

<input type="text" id="hero-name" [(ngModel)]="hero.name">

コンポーネント間のデータ受け渡し

@Input@Output を利用する。
書き方の違いはあれど似ていて、Vue での、@Prop@Emit に相当する。

以下は例

親コンポーネント

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

@Component({
  selector: 'app-inout-parent',
  templateUrl: './inout-parent.component.html',
  styleUrls: ['./inout-parent.component.scss']
})
export class InoutParentComponent implements OnInit {
 // 内部変数
  name: string = '';
  nameOptions = ['Ymada', 'Sato', 'Watanabe'];

  constructor() {}
  ngOnInit(): void {}

  // 子から呼んでもらうメソッド
  selectedName(name: string) {
    this.name = name;
  }
}
<div>
  <!- 子コンポーネントを利用. selectedName イベントで selectedName メソッドを実行($event はイベントペイロード)-->
  <app-inout-child
    [nameOptions]="nameOptions"
    (selectedName)="selectedName($event)"
  >
  </app-inout-child>

  <p id="userInfo" *ngIf="name">
    name =  
    <span style="font-size: 1.5em; color: green">{{ name }}</span>
    が選ばれました。
  </p>
</div>

子コンポーネント

import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';

@Component({
  selector: 'app-inout-child',
  templateUrl: './inout-child.component.html',
  styleUrls: ['./inout-child.component.scss']
})
export class InoutChildComponent implements OnInit {

  constructor() {}
  ngOnInit(): void {}

  name: string = '';

  // 親から来た値をセット
  @Input() nameOptions: string[] = [];
  // emit するために Emitter を作成
  @Output() selectedName = new EventEmitter<string>();

  handleOnClick() {
    // イベントを emit する。親側では $event は this.name の値
    this.selectedName.emit(this.name);
  }

}
<div>
  <div class="suggestedUserNames">
    <select #selectUser [(ngModel)]="name">
      <option
      *ngFor="let name of nameOptions" // 親から来た情報を表示
      style="margin-left:15px"
      [value]="name"
      >{{ name }}</option>
    </select>
  </div>

  <div>
    <label for="userName">{{name}}</label>
    <!- 選択情報を親に emit -->
    <button (click)="handleOnClick()">"選択</button>
  </div>
</div>

画面遷移

@angular/router を利用する。以下はチュートリアルのソース。
Vue router とイメージは同じなので特に違和感はなし。
これを AppModule の imports に追加すればよい。

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HeroesComponent } from './heroes/heroes.component';
import { DashboardComponent } from './dashboard/dashboard.component';
import { HeroDetailComponent } from './hero-detail/hero-detail.component';
import { InoutParentComponent } from './inout-parent/inout-parent.component';

const routes: Routes = [
  { path: 'heroes', component: HeroesComponent},
  { path: 'dashboard', component: DashboardComponent},
  { path: '', redirectTo: '/dashboard', pathMatch: 'full' },
  { path: 'detail/:id', component: HeroDetailComponent},
  { path: 'inout', component: InoutParentComponent},

];

@NgModule({
  // ルートに routes を設定する
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

HTTP通信

通常の方法でももちろんアクセスは可能ですが、Angular としては、@angular/common/http を提供しており、その HttpClient を利用する。以下もチュートリアルのコードの抜粋。


サービスクラスで HttpClient を DI し、処理を実装、コンポーネントクラスでこのサービスクラスを利用することで実現可能。取得の具体的な処理のコメントはコード中のコメントを参照。

import { Injectable } from '@angular/core';
import { Hero } from './hero';
import { HEROES } from './mock-heroes';
import { Observable, of } from 'rxjs';
import { MessageService } from './message.service';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { catchError, map, tap } from 'rxjs/operators';

@Injectable({
  providedIn: 'root'
})
export class HeroService {
  constructor(private http: HttpClient,
              private messageService: MessageService) { }

  private heroesUrl = 'api/heroes'; 
  httpOptions = {
    headers: new HttpHeaders({ 'Content-Type': 'application/json' })
  };

  getHeroes(): Observable<Hero[]> {
    // HttpClient の処理は Observable オブジェクトを返します。
    // pipe で取得結果の Observable オブジェクトに対して処理を行うことができます。
    // tap は Observable オブジェクトに対して同期的な処理を行う Rxjs の operator。
    // 例外時には catchError に流れます。
    return this.http.get<Hero[]>(this.heroesUrl).pipe(
      tap(heroes => this.log('fetched heroes')),
      catchError(this.handleError<Hero[]>('getHeroes', [])))
  }
  ...
}

利用側は以下の通り。
ここでは、ngOnInit (Angularで初期処理を行うライフサイクルフック)で getHeros を呼び、その中で上記の Service クラスの getHeros() を subscribe しています。subscribe することで初めて上の処理が実行されます。
heros には今回の場合はサーバからの取得結果が入っています(上では tap で console.logしているだけなので)。これを インスタンス変数に入れ、これが template の html で画面表示に使われています。

import { Component, OnInit } from '@angular/core';
import { Hero } from '../hero';
import { HeroService } from '../hero.service';
import { MessageService } from '../message.service';

@Component({
  selector: 'app-heroes',
  templateUrl: './heroes.component.html',
  styleUrls: ['./heroes.component.scss']
})
export class HeroesComponent implements OnInit {
  heroes: Hero[] = []

  constructor(private heroService: HeroService, private messageService: MessageService) {}

  ngOnInit(): void {
    this.getHeros()
  }

  getHeros(): void {
    this.heroService.getHeroes().subscribe(heroes => this.heroes = heroes);
  }
}

ライフサイクルフック

https://angular.jp/guide/lifecycle-hooks 公式のライフサイクルフック(angular)
https://angular.jp/guide/lifecycle-hooks 公式のライフサイクルフック(angular)
https://v3.ja.vuejs.org/guide/instance.html 公式のライフサイクルフック(vue)
https://v3.ja.vuejs.org/guide/instance.html 公式のライフサイクルフック(vue)

対応付けは、以下にまとめてくださってたものがありました(他のライブラリの○○に相当する、はスーッと入ってきて助かりますね)。自分でも検証しないとです・・
https://qiita.com/yosgspec/items/3cf93e70a81805d70d29#4-ライフサイクルメソッド

タイミングVueAngularメモ
初期化前beforeCreateconstructor※3constructor は初期化などシンプル
なものに留めるべき
初期化後created
レンダリング前beforeMount
レンダリング後mountedngOnInitngOnChanges (データバインディング
している値が変わったイベント)は
ngOninit の前にも発生する
変更前beforeUpdatengDoCheck
ngAfterViewInit
変更後updatedngAfterViewChecked
アクティブ化時activated
非アクティブ化時deactivated
破棄前beforeUpdate
破棄時beforeDestroyngOnDestroy
https://qiita.com/yosgspec/items/3cf93e70a81805d70d29#4-ライフサイクルメソッド をベースに作成

DI 依存性の注入(サービス)

データやロジックが特定のビューに関連付かず、かつコンポーネント間で共有したい場合は、 サービスクラスを作成する。

  • サービスは、アプリケーションが必要とするあらゆる値、関数、機能を含む幅広いカテゴリー。 サービスは通常、目的が明確な小規模のクラス。
  • コンポーネントはサーバーからデータを取得したり、ユーザーの入力を検証したり、コンソールに直接ログするなど、特定のタスクをサービスに委任する。

サービスをコンポーネントに注入して、コンポーネントにそのサービスクラスへのアクセスを与える。

  • メリットは独立性。テストがしやすい。このあたりは Java の Spring の DI と同じ。
  • Vue にも Provide と Inject がある(https://v3.ja.vuejs.org/guide/component-provide-inject.html)。props のバケツリレーを避けたい場合に、という文脈が多い。個人的にはデータの流れがわかりにくくなるので多様は微妙。

サービスを定義する側、@Injectable で定義。

  • providedIn はDIのレベル。root を指定すると、ngModule 全体で共有インスタンスを1つ作る。
  • プロバイダーを特定の NgModule に登録すると、そのNgModule内のすべてのコンポーネントで同じサービスのインスタンスを使用。 このレベルで登録するには、@NgModulの providers プロパティに指定。
  • コンポーネントレベルでプロバイダーを登録すると、そのコンポーネントの新しいインスタンスごとに新しいサービスインスタンスが取得される。 コンポーネントレベルで @Component の providers プロパティに指定。

以下、公式のコード例

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

@Injectable({providedIn: 'root'}) // アプリ全体で共有
export class Logger {
  writeCount(count: number) {
    console.warn(count);
  }
	log(msg: any)   { console.log(msg); }
  error(msg: any) { console.error(msg); }
  warn(msg: any)  { console.warn(msg); }
}
  • 使いたいコンポーネントの constructor で指定する。と利用できるようになる。
import { Component } from '@angular/core';
import { Logger } from '../logger.service';

@Component({
  selector: 'hello-world-di',
  templateUrl: './hello-world-di.component.html'
})
export class HelloWorldDependencyInjectionComponent  {
  count = 0;

  // DI
  constructor(private logger: Logger) { }

  onLogMe() {
    // this.logger でアクセスできます 
    this.logger.writeCount(this.count);
    this.count++;
  }
}

まとめ

まだまだ理解が及んでいない部分も多々ありますが、ざっくり概念は理解できたかなと思います。RxJS あたりはまた詳しく見ていきたいと思います。

コメント