前回の話
tercel-tech.hatenablog.com
Angular In Memory Web API とは
(略) Angular では、Web API をエミュレートするためのライブラリ (angular-in-memory-web-api) を提供しています。 これを使うと、Http / Json サービス[原文ママ]が利用するバックエンドを置き換え、(ネットワーク上のデータではなく)メモリ上のデータを読み書きするようになります。 本来の Web API に代わって擬似データを返す、テストダブルの一種と言っても良いでしょう。
Angular In Memory Web API を使ってみよう
Angular In Memory Web API はオプションなので、別途インストールが必要だよ。
とりあえず入れてしまおう。
$ npm i angular-in-memory-web-api -S
続いて、擬似データベースを作る。
これは InMemoryDbService
を実装したクラスを作るだけでいい。
$ ng g class MockWebApi
src/app/mock-web-api.ts
import { InMemoryDbService } from 'angular-in-memory-web-api'; export class MockWebApi implements InMemoryDbService { private api = { heroes: [ { id: 1, name: 'Tercel' }, { id: 2, name: 'Bob' } ] }; createDb() { return this.api; } }
createDb()
メソッドは必ず書く必要があるんだね。続いて、これを app.module.ts
に登録する。
差分を示すとこんな感じ。
src/app/app.module.ts (差分のみ)
+ import { InMemoryWebApiModule } from 'angular-in-memory-web-api'; + import { MockWebApi } from './mock-web-api'; @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, AppRoutingModule, HttpClientModule, + InMemoryWebApiModule.forRoot(MockWebApi), EffectsModule.forRoot([AppEffects]), StoreModule.forRoot(reducers, { metaReducers, runtimeChecks: { strictStateImmutability: true, strictActionImmutability: true } }), StoreDevtoolsModule.instrument({ maxAge: 25, logOnly: environment.production }), EntityDataModule.forRoot(entityConfig) ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }
HTTP METHOD | MEANING |
---|---|
POST api/heroes |
新しいエンティティを追加します |
DELETE api/heroes/5 |
主キーの値でエンティティを削除します |
GET api/heroes |
このエンティティタイプのすべてのインスタンスを取得します |
GET api/heroes/5 |
主キーでエンティティを取得します |
GET api/heroes?name=bombasto |
クエリを満たすエンティティを取得します |
PUT api/heroes/5 |
既存のエンティティを更新します |
@ngrx/data | Angular In Memory Web API |
---|---|
POST api/hero/ |
POST api/heroes |
DELETE api/hero/5 |
DELETE api/heroes/5 |
GET api/heroes/ |
GET api/heroes |
GET api/hero/5 |
GET api/heroes/5 |
GET api/heroes/?name=bombasto |
GET api/heroes?name=bombasto |
PUT api/hero/5 |
PUT api/heroes/5 |
とりあえずの動作確認
src/app/app.component.ts
import { Component, OnInit } from '@angular/core'; import { EntityCollectionService, EntityServices } from '@ngrx/data'; import { Observable } from 'rxjs'; import { Hero } from './hero'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.scss'] }) export class AppComponent implements OnInit { private heroService: EntityCollectionService<Hero>; heroes$: Observable<Hero[]>; constructor(entityServices: EntityServices) { this.heroService = entityServices.getEntityCollectionService<Hero>('Hero'); this.heroes$ = this.heroService.entities$; } ngOnInit() { this.heroService.getAll() .subscribe( () => console.log('エンティティを取得しました'), err => console.error(`エンティティの取得に失敗しました: ${err}`)); } }
src/app/app.component.html
<table> <tr> <th>id</th> <th>name</th> </tr> <tr *ngFor="let hero of heroes$ | async"> <td>{{hero.id}}</td> <td>{{hero.name}}</td> </tr> </table>
src/app/app.component.scss
table, th, td { border: 1px darkgray solid; border-collapse: collapse; } th, td { padding-left: 1rem; padding-right: 1rem; } th { background-color: lightgray; }
ここまで書けたら、動かしてみるのだ。
CSS は面倒なら書かなくていいのだ。
$ ng serve
そうだね。
それに、@ngrx/store-devtools を入れたおかげで、Redux DevTools が使えるようになっている。
エンティティのキャッシュが、いつどうやって更新されたかを容易にデバッグできるツールだよ。
[Hero] @ngrx/data/query-all
がクエリ開始、[Hero] @ngrx/data/query-all/success
がクエリ正常終了という具合に、アプリケーションの内部状態の変化を観察できる。
── と同時に、NgRx が管理している情報がどう変化したか、差分を確認することもできるんだ。
API URL のアダプタを作ろう
urlTranslate()
メソッドを側だけ作っておこう。src/app/mock-web-api.ts
import { InMemoryDbService } from 'angular-in-memory-web-api'; export class MockWebApi implements InMemoryDbService { private api = { heroes: [ { id: 1, name: 'Tercel' }, { id: 2, name: 'Bob' } ] }; createDb() { return this.api; } + urlTranslate(url: string): string { + throw new Error('まだ実装していません'); + } }
今回ばかりはテストコードを書いておこう。
変換のパターンが複雑だからね。
@ngrx/data が放つリクエスト | Angular In Memory Web API が放つリクエスト |
---|---|
POST api/hero/ |
POST api/heroes |
DELETE api/hero/5 |
DELETE api/heroes/5 |
GET api/heroes/ |
GET api/heroes |
GET api/hero/5 |
GET api/heroes/5 |
GET api/heroes/?name=bombasto |
GET api/heroes?name=bombasto |
PUT api/hero/5 |
PUT api/heroes/5 |
単数形を複数形にすればいいというものじゃないね。
初めから複数形が飛んでくることもあるし、間のスラッシュを抜かなきゃいけないパターンもある。
src/app/mock-web-api.spec.ts
import { MockWebApi } from './mock-web-api'; describe('MockWebApi', () => { it('should create an instance', () => { expect(new MockWebApi()).toBeTruthy(); }); describe('urlTranslate() [ EntityName: Hero ]', () => { let mockWebApi: MockWebApi; beforeEach(() => mockWebApi = new MockWebApi()); it('api/hero/ -> api/heroes', () => { expect(mockWebApi.urlTranslate('api/hero/')).toEqual('api/heroes'); }); it('api/hero/5 -> api/heroes/5', () => { expect(mockWebApi.urlTranslate('api/hero/5')).toEqual('api/heroes/5'); }); it('api/heroes/ -> api/heroes', () => { expect(mockWebApi.urlTranslate('api/heroes/')).toEqual('api/heroes'); }); it('api/heroes/?name=bombasto -> api/heroes?name=bombasto', () => { expect(mockWebApi.urlTranslate('api/heroes/?name=bombasto')) .toEqual('api/heroes?name=bombasto'); }); }); });
urlTranslate()
メソッドを書いてください。src/app/mock-web-api.ts
import { InMemoryDbService } from 'angular-in-memory-web-api'; import { entityConfig } from './entity-metadata'; export class MockWebApi implements InMemoryDbService { private api = { heroes: [ { id: 1, name: 'Tercel'}, { id: 2, name: 'Bob' } ] }; createDb() { return this.api; } private get lowerCasePluralNames(): {} | { [name: string]: string } { return !entityConfig.pluralNames ? {} : Object.entries(entityConfig.pluralNames) .reduce((x, y) => Object.assign(x, { [y[0].toLowerCase()]: y[1].toLowerCase() }), {}); } private toPlural(singluarName: string): string { return this.lowerCasePluralNames[singluarName] || `${singluarName}s`; } private convertApiEntityName(entityName: string): string { return Object.keys(this.api) .map(x => x.toLowerCase()) .includes(entityName) ? entityName : this.toPlural(entityName); } urlTranslate(url: string): string { const regexp = /(\/?[^\/]+\/)([^\/]+)(\/?.*)/g; const match = regexp.exec(url); if(!match || match.length !== 4 ) { throw new Error('URL形式が不正です')} const requestEntityName = match[2]; const apiEntityName = this.convertApiEntityName(requestEntityName); return url.replace(regexp, `$1${apiEntityName}$3`) .replace(/\/\?/g, '?') .replace(/\/$/g, ''); } }
$ ng test
インターセプタ
InMemoryDbService
の parseRequestUrl()
メソッドをオーバーライドすればいいよ。src/app/mock-web-api.ts(差分のみ)
- import { InMemoryDbService } from 'angular-in-memory-web-api'; + import { InMemoryDbService, RequestInfoUtilities, ParsedRequestUrl } from 'angular-in-memory-web-api'; import { entityConfig } from './entity-metadata'; export class MockWebApi implements InMemoryDbService { private api = { heroes: [ { id: 1, name: 'Tercel'}, { id: 2, name: 'Bob' } ] }; createDb() { return this.api; } /* 略 */ urlTranslate(url: string): string { /* 略 */ } + parseRequestUrl(url: string, utils: RequestInfoUtilities): ParsedRequestUrl { + return utils.parseRequestUrl(this.urlTranslate(url)); + } }
今回はほとんど NgRx 関係なかったね。
Angular In Memory Web API とか Jasmine とか……。
あの、アダプタを作るところで思ったんだけど、たーせるくん面倒くさいコード書きすぎ。
Angular In Memory Web API なんて環境を用意できない開発の初期にちょっとだけ使ってすぐ捨てるモノだから、変に汎用性とか意識しなくていいのに。
完結編につづく
tercel-tech.hatenablog.com