Angular2
angular-cli
ngrx

@ngrx/schematicsを触ってみる

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)

上記のコマンドを実行すると,以下のようなコードが吐き出されます.

src/app/test.actions.ts
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をコンストラクタで受け取るように設定されています.

src/app/test.component.ts
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 オプションを指定した場合は,アクションを受け取った場合の処理がデフォルトで生成されます. 指定しなかった場合は単にコンストラクタが宣言されているだけになります.

src/app/effects.ts
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を通してエンティティの操作が行われるようになっています.

src/app/test/test.action.ts
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の操作対象となる型の定義です.

src/app/test/test.model.ts
export interface Test {
  id: string;
}

reducerです.reducer関数の実装を見ると,上で定義したアクションをハンドルして,エンティティを操作しているのが分かるかと思います.

src/app/test/test.reducer.ts
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)
src/app/test.reducer.ts
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して使う感じになるかと思います.

src/app/reducers/index.ts
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もあります.
そちらはまた別に記事を書きたいと思っています.