Vueを長らく使ってた僕がAngularのチュートリアルをやったら結構便利だと思った件
VueもReactも標準的には扱えているとは思っているが、未だにAngularについては触れてみたことがない。Angularといえば、基本的にビューの部分を管轄するVue/Reactとは異なり結構全部入りっていうイメージがある。(逆にその程度しか知らない)
普段とりあえず手軽なWeb系のプロジェクトを作る際には、自分の環境に長らく使っているVueの秘伝のタレがある程度揃ってることからもVueを使っていることが多い。(いつもTS使っているせいで、いろんな問題があってVue3.x系列にはまだ移行できていないのだけれど)
とりあえず、Angularのチュートリアルをやってみたくなったのでツアー・オブ・ヒーローズ アプリケーションとチュートリアル
入をやってみることにする。
環境のセットアップ
最初は当然環境すらないのでangular-cliを入れる。
npm install -g @angular/cli
これで、Angular用のコマンドng
が使えるようになる。
最近はこういう全部入りスキャフォールドツールがフレームワークについていて便利だよね。vue-cliとか。
プロジェクトの作成
ng new angular-tour-of-heroes
チュートリアルにはとりあえずプロジェクトを作成するとしか記述されていないが、「Angular routingを用いるか?」と「どんなスタイルシートのフォーマットを用いるか?(CSS/SASS/SCSS/Stylus)」も聞かれる。
とりあえず僕はここでは、Angular routingは用いる
、SCSS
を選んだ。
cd angular-tour-of-heroes
ng serve --open
これでビルドが行われて対象となるアプリケーションが開く。さらに僕はここで、Angular用のVSCodeの拡張「Angular Language Service」を追加した。
あとはチュートリアルに沿って以下を行う。
- アプリケーションのタイトルを変更する
- アプリケーションのスタイルを変更する
とりあえず、Angularにおいてテンプレートになるのはsrc
配下にあるフォルダの中の*.component.html
になるみたいだ。*.component.scss
がそれに適用されているスタイルらしい。これはapp.component.ts
を見るとここから読み込まれていることが分かる。
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
title = 'Tour of Heroes';
}
さらに、app.component.spec.ts
を見ればユニットテスト用のファイルが追加されていることが分かる。さすが全部入りフレームワークといったところか。
さらにapp-routing.module.ts
とapp.module.ts
が同じフォルダに含まれているようだが多分これらも後で解説されるだろう。
ヒーローエディター
ng generate component heroes
このコマンドによってsrc/app/heroes
フォルダに新しいコンポーネントが生成される。
heroes.component.ts
を見る。どうやら、@Component
の引数のselector
はこのコンポーネントを指定したい親要素が用いる名前になるようだ。記事の翻訳ではCSS要素セレクタ
となっていてわかりにくいが、要するにただ単にタグ名
である。
どうやら、app.module.ts
にHeroesComponent
が追加されていることを見ると、コンポーネントはスキャフォールディングすると自動的にここに追加されてそのタグ名で使えるようになる様子である。
ここから更にプロパティを追加したり親コンポーネント<app>
で<app-heroes>
を埋め込んだりする。この際、僕の環境ではビルドが更新されなくなり、ng serve
をやり直すことによって正常に動作するようになった。(複雑なWeb開発ビルドツールではよくある話...)
パイプに関するチュートリアルもここに含まれている。
<h2>{{hero.name | uppercase}} Details</h2>
なるほど、こうすることで表示時に表示用にデータを整形することができるようだ。チュートリアルでは通貨や日付などの書式設定するのに適していると記述されている。そのとおりだろう。
リストの表示
リストを追加してテンプレートで*ngFor
を追加してリストを表示できるらしい。変数へのバインドの書き方がVueとは結構違うが、テンプレート自身にビュー側のロジックをもたせるという方針はReactよりもVueに近い。
-
*ngFor
,*ngIf
など: テンプレート自身に持たせるための制御構文 -
(click)
,(イベント名)
: テンプレートで行うイベントハンドラの登録。 -
[title]
,[属性名]
:テンプレートで行う属性のバインド
として記述できることがここまででわかった。
特定の条件のときにだけ割り当てるクラス名がある際に以下のように記述できるのはすごく便利だ。
[class.selected]="hero === selectedHero"
フィーチャーコンポーネントの作成
ヒーローエディタで追加した編集を行う部分をコンポーネントとして切り分けるチュートリアル。デコレータ記法でTypescriptでVueを書く際にも@Props
と書いて親要素から伝達可能であることを指すが、ここでは@Input()
と記述することによって可能な様子。
標準でデコレータ記法が前提なのはとても良い。Angularはv1とv2で全然違うって悪評があった気がするけれども、こういう先進的な記法を導入するならそれをせざるを得なかったんだろうなと思う。
サービスの追加
コンポーネント内では直接データの取得や保存を行うべきではありません。もちろん、故意に仮のデータを渡してもいけません。 コンポーネントはデータの受け渡しに集中し、その他の処理はサービスクラスへ委譲するべきです。
VueでのVuex、ReactでのReduxやMobxのようにAngularでもまた、ビューのロジックとアプリケーション全体のデータ管理は分割すべきであるという考えに立っている。どうやらこの役割をするべきなのはAngularではサービスらしい。
ng generate service hero
このコマンドで、component
ではなくservice
をスキャフォールディングすることができる。
どうやらサービスは、app
直下にスキャフォールディングされるようだ。実際、これによってhero.service.ts
とhero.service.spec.ts
が生成される。
このようなサービスを作ると、Componentのコンストラクタで同名の型の変数を作ると自動的に依存性が注入されるらしい。便利。
import { Component, OnInit } from '@angular/core';
import { Hero } from '../hero';
import {HeroService} from "../hero.service";
@Component({
selector: 'app-heroes',
templateUrl: './heroes.component.html',
styleUrls: ['./heroes.component.scss']
})
export class HeroesComponent implements OnInit {
constructor(private heroService:HeroService) { } // <これだけあれば勝手にheroServiceを渡してくれる
selectedHero:Hero;
heroes:Hero[];
ngOnInit(): void {
this.getHeroes();
}
getHeroes():void{
this.heroes = this.heroService.getHeroes();
}
onSelect(hero:Hero):void{
this.selectedHero = hero;
}
}
さらにこの後、service側のgetHeroes()
を非同期に対応するためにrxjs
のObservable
を用いて書き直す。Angularに含まれているhttpクライアントはObservableをデフォルトで返すと記述されている。Observableについてはredux-observable
が一時期はやった時に触れたが当時は難解なイメージがあった。もう少しやろうと思う。
この後、サービスが別のサービスを参照する際の依存性注入を学ぶためにmessageコンポーネントとmessageサービスを追加し、ログの表示のようなものをビューに実装した。
アプリ内ナビゲーションの追加
自分は最初にプロジェクトを生成時にルーティングを使うことを選択していたので、このチュートリアルで最初にモジュールを追加するコマンドを実行する必要はなかった。
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HeroesComponent } from './heroes/heroes.component';
const routes: Routes = [
{ path: 'heroes', component: HeroesComponent }
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
とりあえずこのroutes
にコンポーネントとパスを登録すれば、対応したコンポーネントが開くようだ。登録する手段は違うが、コンポーネントのルーティング上での扱いはほとんど他の主要フレームワークと同じようだ。実際、このrouterによってアドレスに基づいて指定したコンポーネントが<router-outlet>
に表示されるようだ。そのため、app.component.html
は以下のようにチュートリアル中ではいじっている。
<h1>{{title}}</h1>
<nav>
<a routerLink="/dashboard">Dashboard</a>
<a routerLink="/heroes">Heroes</a>
</nav>
<router-outlet></router-outlet>
<app-messages></app-messages>
途中、ルーティングのチュートリアルのコードブロックが表示されていない場所があるが、こちらのソースコードを参考にすれば良い。https://github.com/angular/angular/blob/5b31a0a2942c50059cac4c7ccb92047a50473347/aio/content/examples/toh-pt5/src/app/app-routing.module.ts
サーバからデータの取得
この章はとても学びの多い章だった。まず、APIサーバを立てるのは苦労するのでこのチュートリアルではangular-in-memory-web-api
というものを用いて仮想的にサーバと通信をしている。
チュートリアルではあまり深く触れられてないがangular-in-memory-web-api
を使うためにin-memory-data-service.ts
を作成する。
import { Injectable } from '@angular/core';
import { InMemoryDbService } from 'angular-in-memory-web-api';
import { Hero } from './hero';
@Injectable({
providedIn: 'root',
})
export class InMemoryDataService implements InMemoryDbService {
createDb() {
const heroes = [
{ id: 11, name: 'Dr Nice' },
{ id: 12, name: 'Narco' },
{ id: 13, name: 'Bombasto' },
{ id: 14, name: 'Celeritas' },
{ id: 15, name: 'Magneta' },
{ id: 16, name: 'RubberMan' },
{ id: 17, name: 'Dynama' },
{ id: 18, name: 'Dr IQ' },
{ id: 19, name: 'Magma' },
{ id: 20, name: 'Tornado' }
];
return {heroes};
}
// Overrides the genId method to ensure that a hero always has an id.
// If the heroes array is empty,
// the method below returns the initial number (11).
// if the heroes array is not empty, the method below returns the highest
// hero id + 1.
genId(heroes: Hero[]): number {
return heroes.length > 0 ? Math.max(...heroes.map(hero => hero.id)) + 1 : 11;
}
ここで、createDb
はInMemoryDbService
が呼ぶであろう継承するべきメソッドになっている。ここで、heroesだけをメンバに含むオブジェクトを返しているので、例えば以下のようなリクエストを投げると自動的にこのInMemoryDbServiceが返してくれるようだ。
- GET /heroes/ : ヒーロすべての配列を返す
- GET /heores/:id : 特定のヒーローのデータを返す
- PUT /heroes/:id : 特定のヒーローのデータを更新する
- POST /heores : 新しいヒーローを追加する
- DELETE /heroes/:id : 特定のヒーローを削除する。
また、これだけではなくプロパティの値による検索も自動的に実装されているようだ。
- GET /heroes/?name=***
とすると部分一致する要素を得ることができる。
このチュートリアルではこれによって実際にバックエンドのAPIを記述することなくデバッグすることができる。
さらに、この章では部分一致の検索と検索の予測変換を行えるようにしているようだ。
hero-serach.component.ts
では以下のコードでngInit
でsearchTerms
が変わった際に常にheroes$
から検索しているヒーローのリストを得ることができるコードが記述されている。
this.heroes$ = this.searchTerms.pipe(
// 各キーストロークの後、検索前に300ms待つ
debounceTime(300),
// 直前の検索語と同じ場合は無視する
distinctUntilChanged(),
// 検索語が変わる度に、新しい検索observableにスイッチする
switchMap((term: string) => this.heroService.searchHeroes(term)),
);
こうしてできたObservableそのものを、Angularではasync
パイプを使うと取り出すことができるようで、これはとてもイケているように見える。
<ul class="search-result">
<li *ngFor="let hero of heroes$ | async" >
...
</li>
</ul>
テンプレート自身がasyncなものを表示できる機構を持っているのはとても面白い。ドキュメントを見る限り、ObservableだけではなくPromiseでもこれをすることができるようだ。
まとめ
触れたことがなかったAngularを触れてみて思ったのは、ビルドシステム、ツール群にほとんど意識をしなくていい点は良いと思った。最近ではVueもvue-cliがあるからそうだと思うが、テスト環境の設定や、TS入れたり、CSSの言語を変えたり、複数のバンドリング対象があったりするとどうしても自分で設定を編集をせざるを得ない。
とはいえ、webpack.config.js
やvue.config.js
を書くのに数日要するのは目に見えて辟易するし、とりあえず必要そうなテストやらビルド環境を生成してくれるng
コマンドの便利さはなかなか良いと思う。(少なくともVue-cli
とかの潮流を見ているとVueもこの方向性に舵を切っているように見える。しかし、既に記法が複数あってさらにTSで書く場合もデコレータ使うパターンと使わないパターンなど色々ある中では結局自分好みにカスタマイズするにはある程度の知識が要求されてしまうだろう。その点、AngularはTSにシフトしてとても良かったのではないかと思える。)
Angular用のHttpモジュールを有効化してDIしてもらうなど、Angularが全部入りと言われる所以を見せている気がするが、新たなプロジェクトでコードを書く際には標準となるパターンを示してくれていてとてもいいように思える。一方、様々な記事で述べられている通り、既存のプロジェクトに用いて少しずつリプレースするような用途では導入のハードルが高いように思える。
テンプレート記法については、Vueに比べてもそれ以上に高度なことができるように思えた。特にasyncパイプはかなり便利そうだ。様々なAPIと連携をしなければならない昨今のフロントエンドでは、よくある非同期処理を行うだけのために複雑なストアの処理をVuexやreduxを介して書かなければならないし肥大化する上に面倒だ。しかし、AngularではあくまでObservableをそのままビューまで渡して、ビュー上で展開できるのはとても便利だ。
思っていたより圧倒的に便利だったので良ければReact大好き勢やVue大好きな人もチュートリアルをやってみてほしい。
Discussion