schematicsとは
@angular-devkit
に含まれている,コード生成のための仕組みです.
yeomanのようなものをイメージしてもらえればわかりやすと思います.
テンプレートを用意しておいて,それをもとにコードを生成する感じです.
angular-cli
の内部でも使われていて,例えば
ng new app-name
ng generate component Hoge
このようなコード生成のコマンドの裏側ではschematicsが使われています.
schematicsは,--collection(-c)
オプションで,利用するコレクションを指定することで,そのコレクションで定義されているテンプレートを利用することができます.
予め用意されているコレクション以外のものを,自分で用意することもできるわけです.
@ngrx/schematicsについて
さて,@ngrx/store
を始めとするパッケージ群は,Angularでreduxアーキテクチャを利用するためのものです.
しかし,Actionの型定義等,多くのボイラープレートを書くことが要求され,いささか面倒な部分があることも事実です.
これはngrxに限ったことではないと思います.
そこで,schematicsのコレクションを提供することでボイラープレートを自動生成できるようにし,これらを書く手間を減らそう,というのが@ngrx/schematics
というプロジェクトです.
使い方
Angularプロジェクトを作成する
特に変わったところはないですね.
ng new try-ngrx-schematics
また,@ngrx/schematics
をインストールしておきます.
本記事執筆段階で,npmに公開されていないようですので,gitから落としてきます.
npm i -S https://github.com/ngrx/schematics-builds
これで準備は整いました.
コード生成する
以下のコマンドでコード生成を行うことができます.
ng g type name -c @ngrx/schematics
今のところ,type
に指定できるのは,以下の7つです.
- action
- container
- effect
- entity
- feature
- reducer
- store
action
$ ng g action test --collection @ngrx/schematics
create src/app/test.actions.ts (222 bytes)
上記のコマンドを実行すると,以下のようなコードが吐き出されます.
import { Action } from '@ngrx/store';
export enum TestActionTypes {
TestAction = '[Test] Action'
}
export class Test implements Action {
readonly type = TestActionTypes.TestAction;
}
export type TestActions = Test;
以下の3つが生成されています.
- アクションを識別するための文字列のenum
- アクションクラス
- 共用体型
container
コンテナコンポーネントです.
$ ng g container test --state test --collection @ngrx/schematics
create src/app/test/test.component.css (0 bytes)
create src/app/test/test.component.html (23 bytes)
create src/app/test/test.component.ts (379 bytes)
create src/app/test/test.component.spec.ts (827 bytes)
update src/app/app.module.ts (390 bytes)
scssとhtmlは割愛します.
コンテナコンポーネントが生成されています.
Storeをコンストラクタで受け取るように設定されています.
import { Component, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
import * as fromStore from '../test';
@Component({
selector: 'app-test',
templateUrl: './test.component.html',
styleUrls: ['./test.component.css']
})
export class TestComponent implements OnInit {
constructor(private store: Store<fromStore.State>) { }
ngOnInit() {
}
}
モジュールに 生成したコンテナが追加されています.
--- a/src/app/app.module.ts
+++ b/src/app/app.module.ts
@@ -3,11 +3,13 @@ import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
+import { TestComponent } from './test/test.component';
@NgModule({
declarations: [
- AppComponent
+ AppComponent,
+ TestComponent
],
imports: [
BrowserModule
effect
Effectsクラスを生成します.
$ ng g effect test --feature test --collection @ngrx/schematics
create src/app/test.effects.ts (183 bytes)
create src/app/test.effects.spec.ts (588 bytes)
--feature
オプションを指定した場合は,アクションを受け取った場合の処理がデフォルトで生成されます. 指定しなかった場合は単にコンストラクタが宣言されているだけになります.
import { Injectable } from '@angular/core';
import { Actions, Effect } from '@ngrx/effects';
import { TestActions, TestActionTypes } from './test.actions';
@Injectable()
export class TestEffects {
@Effect()
effect$ = this.actions$.ofType(TestActionTypes.TestAction);
constructor(private actions$: Actions) {}
}
entity
@ngrx/entity
を利用するコードを生成します.
ある特定の種類のentityに対してCRUD操作を行えるようになります.
$ ng g entity test/test --collection @ngrx/schematics
create src/app/test/test.actions.ts (1677 bytes)
create src/app/test/test.model.ts (40 bytes)
create src/app/test/test.reducer.ts (1531 bytes)
create src/app/test/test.reducer.spec.ts (322 bytes)
アクションの定義です.
ここに定義されているアクションを発火すると,reducer側で EntityAdapter
を通してエンティティの操作が行われるようになっています.
import { Action } from '@ngrx/store';
import { Test } from './test.model';
export enum TestActionTypes {
LoadTests = '[Test] Load Tests',
AddTest = '[Test] Add Test',
AddTests = '[Test] Add Tests',
UpdateTest = '[Test] Update Test',
UpdateTests = '[Test] Update Tests',
DeleteTest = '[Test] Delete Test',
DeleteTests = '[Test] Delete Tests',
ClearTests = '[Test] Clear Tests'
}
export class LoadTests implements Action {
readonly type = TestActionTypes.LoadTests;
constructor(public payload: { tests: Test[] }) {}
}
export class AddTest implements Action {
readonly type = TestActionTypes.AddTest;
constructor(public payload: { test: Test }) {}
}
export class AddTests implements Action {
readonly type = TestActionTypes.AddTests;
constructor(public payload: { tests: Test[] }) {}
}
export class UpdateTest implements Action {
readonly type = TestActionTypes.UpdateTest;
constructor(public payload: { test: { id: string, changes: Test } }) {}
}
export class UpdateTests implements Action {
readonly type = TestActionTypes.UpdateTests;
constructor(public payload: { tests: { id: string, changes: Test }[] }) {}
}
export class DeleteTest implements Action {
readonly type = TestActionTypes.DeleteTest;
constructor(public payload: { id: string }) {}
}
export class DeleteTests implements Action {
readonly type = TestActionTypes.DeleteTests;
constructor(public payload: { ids: string[] }) {}
}
export class ClearTests implements Action {
readonly type = TestActionTypes.ClearTests;
}
export type TestActions =
LoadTests
| AddTest
| AddTests
| UpdateTest
| UpdateTests
| DeleteTest
| DeleteTests
| ClearTests;
EntityAdapter
の操作対象となる型の定義です.
export interface Test {
id: string;
}
reducerです.reducer関数の実装を見ると,上で定義したアクションをハンドルして,エンティティを操作しているのが分かるかと思います.
import { EntityState, EntityAdapter, createEntityAdapter } from '@ngrx/entity';
import { Test } from './test.model';
import { TestActions, TestActionTypes } from './test.actions';
export interface State extends EntityState<Test> {
// additional entities state properties
}
export const adapter: EntityAdapter<Test> = createEntityAdapter<Test>();
export const initialState: State = adapter.getInitialState({
// additional entity state properties
});
export function reducer(
state = initialState,
action: TestActions
): State {
switch (action.type) {
case TestActionTypes.AddTest: {
return adapter.addOne(action.payload.test, state);
}
case TestActionTypes.AddTests: {
return adapter.addMany(action.payload.tests, state);
}
case TestActionTypes.UpdateTest: {
return adapter.updateOne(action.payload.test, state);
}
case TestActionTypes.UpdateTests: {
return adapter.updateMany(action.payload.tests, state);
}
case TestActionTypes.DeleteTest: {
return adapter.removeOne(action.payload.id, state);
}
case TestActionTypes.DeleteTests: {
return adapter.removeMany(action.payload.ids, state);
}
case TestActionTypes.LoadTests: {
return adapter.addAll(action.payload.tests, state);
}
case TestActionTypes.ClearTests: {
return adapter.removeAll(state);
}
default: {
return state;
}
}
}
export const {
selectIds,
selectEntities,
selectAll,
selectTotal,
} = adapter.getSelectors();
feature
指定した名前とオプションで,
- action
- reducer
- effect
を生成します.
生成される内容については,それぞれを個別に生成した場合と同じなので割愛します.
reducer
状態の型と初期状態,reducer関数を生成します.
$ ng g reducer test --collection @ngrx/schematics
create src/app/test.reducer.ts (247 bytes)
create src/app/test.reducer.spec.ts (322 bytes)
import { Action } from '@ngrx/store';
export interface State {
}
export const initialState: State = {
};
export function reducer(state = initialState, action: Action): State {
switch (action.type) {
default:
return state;
}
}
store
StoreModule.forRoot
で指定できるような形式のActionReducerMap
と,
状態の型を定義したコードを生成します.
生成されたコードに,reducers
で生成したコードをimportして使う感じになるかと思います.
import {
ActionReducer,
ActionReducerMap,
createFeatureSelector,
createSelector,
MetaReducer
} from '@ngrx/store';
import { environment } from '../../environments/environment';
export interface State {
}
export const reducers: ActionReducerMap<State> = {
};
export const metaReducers: MetaReducer<State>[] = !environment.production ? [] : [];
まとめ
schematicsは,単にテンプレートをもとにコードを生成するだけのシンプルなツールですが,ボイラープレートが非常に多いngrxでは,必要不可欠だと思います.
また,コードの静的解析を用いて,より高度なコード生成を行う@ngrx/codegen
もあります.
そちらはまた別に記事を書きたいと思っています.