B-Teck!

お仕事からゲームまで幅広く

【Angular】Angular + Akitaで単方向アーキテクチャ + Single State Streamパターンを実装するメモ

この記事の目的

の2つの記事を踏まえて、AngularのアプリケーションでAkitaを導入し、単一のStateに依存するComponentを作成する流れのメモ。
流れに沿って実装すれば以下画像のデータフローの状態になるが、各ブロックに細かい説明は書いていない。

実装を直接見たい場合は Sandbox-Frontend/Angular-Architecture-Sample at master · beatdjam/Sandbox-Frontend · GitHub を参照。

環境構築

ブログ用サンプル 環境構築 · beatdjam/Sandbox-Frontend@015f59e · GitHub

Angular CLIインストール

$ npm install -g @angular/cli

Akita CLIインストール

$ npm install @datorama/akita-cli -g

プロジェクトの初期設定

プロジェクト作成

$ ng new <name>

Akita導入

$ npm install @datorama/akita

iniのバージョンが古くて怒られるけどコードジェネレータでの利用のみということで特に対応されていないっぽい
ini 1.3.5 - dependency vulnerability in npm audit · Issue #698 · datorama/akita · GitHub

package.jsonに下記を追記するとAkita CLIで作成されるファイルがAngularプロジェクト用になる(TypeScript、Angularのアノテーションがつくなど)

"akitaCli": {
  "customFolderName": "true",
  "template": "angular",
  "basePath": "./src/app"
},

(任意)akita-ngdevtoolsインストール

$ npm i @datorama/akita-ngdevtools --save-dev

Redux DevTools - Chrome ウェブストア でStoreの状態を確認できるようにする

app.module.tsのimportsに AkitaNgDevtools.forRoot() の記述を追加する

imports: [
    environment.production ? [] : AkitaNgDevtools.forRoot(),
    BrowserModule,
    BrowserAnimationsModule
],

実装

ユーザー一覧を表示する

ブログ用サンプル 画面表示実装 · beatdjam/Sandbox-Frontend@8c77299 · GitHub

CLIでcomponent作成

$ ng g component

app.component.htmlから作成したcomponentを呼び出す

<app-user-list></app-user-list>

app.module.tsのimportsに HttpClientModuleを追加

imports: [
  environment.production ? [] : AkitaNgDevtools.forRoot(),
  BrowserModule,
  AppRoutingModule,
  HttpClientModule,
]

レスポンスを扱うModelを作成

export interface User {
  id: string;
  first_name: string;
  last_name: string;
  email: string;
  avatar: string;
}

ユーザー一覧APIのリクエスト部分を作成

$ ng g service

Reqres - A hosted REST-API ready to respond to your AJAX requests を使ってAPI取得部分を作成

import { Injectable } from '@angular/core';
import {HttpClient} from "@angular/common/http";
import {User} from "../model/user";
import {map, Observable} from "rxjs";

const apiHost = 'https://reqres.in/api';

interface ApiResponse<T> {
  data: T;
}

@Injectable({
  providedIn: 'root'
})
export class UsersService {
  constructor(private http: HttpClient) {
  }

  get users$(): Observable<User[]> {
    return this.http
      .get<ApiResponse<User[]>>(`${apiHost}/users`)
      .pipe(map(resp => resp.data))
  }
}

ComponentからAPIを呼び出す部分を作成

import { Component, OnInit } from '@angular/core';
import {UsersService} from "../../repository/users.service";
@Component({
  selector: 'app-user-list',
  templateUrl: './user-list.component.html',
  styleUrls: ['./user-list.component.scss']
})
export class UserListComponent implements OnInit {
  users$ = this.service.users$;
  constructor(private service: UsersService) { }

  ngOnInit(): void {
  }
}

取得したデータを画面に表示するテンプレートを作成

<ul>
  <li *ngFor="let user of users$ | async">
    #{{user.id}} {{user.first_name}} {{user.last_name}}
  </li>
</ul>

処理の分離と単方向データフローの適用

ブログ用サンプル Store経由の単方向データフローにする · beatdjam/Sandbox-Frontend@122880a · GitHub

Store, Queryの作成

$ akita

Akita CLIで作る場合のオプションに出てくるHttp Entity Serviceは今回利用していない。

import{Injectable}from '@angular/core';
import{EntityState, EntityStore, StoreConfig}from '@datorama/akita';

export class UserListItem{
    constructor(
        public id: string,
        public first_name: string,
        public last_name: string
    ) {}
}

export interface UserListState extends EntityState<UserListItem> {}

@Injectable({providedIn: 'root'})
@StoreConfig({
  name: 'UserList',
  idKey: 'id'
})
export class UserListStore extends EntityStore<UserListState> {
    constructor() {
        super();
    }
}
import{Injectable}from '@angular/core';
import{QueryEntity}from '@datorama/akita';
import{UserListStore, UserListState}from './user-list.store';

@Injectable({providedIn: 'root'})
export class UserListQuery extends QueryEntity<UserListState> {

constructor(protected override store: UserListStore) {
    super(store);
    }
}

Usecase作成

$ ng g service

Serviceと同じように使うのでserviceで作成する

import { Injectable } from '@angular/core';
import {UsersService} from "../../repository/users.service";
import {UserListItem, UserListStore} from "./user-list.store";
import {tap} from "rxjs";

@Injectable({
  providedIn: 'root'
})
export class UserListUsecaseService {
  constructor(private service: UsersService, private store: UserListStore) { }

  fetchUsers() {
    this.store.setLoading(true);
    this.service.getAllUsers().pipe(
      tap(res => {
          const users = res.map(user => {
            return new UserListItem(user.id, user.first_name,user.last_name);
          });
          this.store.add(users);
      })
    ).subscribe(() => this.store.setLoading(false));
  }
}

ComponentからQueryの購読

import { Component, OnInit } from '@angular/core';
import {UserListUsecaseService} from "./user-list-usecase.service";
import {UserListQuery} from "./user-list.query";
@Component({
  selector: 'app-user-list',
  templateUrl: './user-list.component.html',
  styleUrls: ['./user-list.component.scss']
})
export class UserListComponent implements OnInit {
  users$ = this.query.selectAll();
  constructor(private usecase: UserListUsecaseService, private query: UserListQuery) { }

  ngOnInit(): void {
    this.usecase.fetchUsers();
  }
}

Single State Streamパターンを適用する

ブログ用サンプル Single State Streamパターン適用 · beatdjam/Sandbox-Frontend@c9739c4 · GitHub

Single State Streamの説明は下記を参照
第2章 Effective RxJS - コンポーネントにおけるObservableの購読

要約すると、同じObservableにAsyncパイプを適用したり、いくつものObservableを同期的に処理する必要があるとき、それらを単一のStreamに合成して扱うことでテンプレートで扱う状態を単純化できるというもの。

Stateを合成する

import { Component, OnInit } from '@angular/core';
import {UserListUsecaseService} from "./user-list-usecase.service";
import {UserListQuery} from "./user-list.query";
import {map, Observable} from "rxjs";
import {combineQueries} from "@datorama/akita";
import {UserListItem} from "./user-list.store";
@Component({
  selector: 'app-user-list',
  templateUrl: './user-list.component.html',
  styleUrls: ['./user-list.component.scss']
})
export class UserListComponent implements OnInit {
  readonly state$: Observable<UserStateViewModel>;
  constructor(private usecase: UserListUsecaseService, private query: UserListQuery) {
    this.state$ = combineQueries([this.query.selectAll()]).pipe(
      map(([users]) => new UserStateViewModel(users))
    );
  }

  ngOnInit(): void {
    this.usecase.fetchUsers();
  }
}


class UserStateViewModel {
  constructor(public users: UserListItem[]) {
  }
}

合成したStateをテンプレートから利用する

<ng-container *ngIf="state$ | async as state">
  <ul>
    <li *ngFor="let user of state.users">
      #{{user.id}} {{user.first_name}} {{user.last_name}}
    </li>
  </ul>
</ng-container>

【振り返り】2022年3月

前月の振り返り
【振り返り】2022年2月 - B-Teck!

学習

今月やったこと

所感

今月は体調を崩し気味だったり、色々と忙しかったのもあり、本を全然読めませんでした。
読みきれないちょっと開いただけの本はたくさんあるんですが…。今月は読み切りたいですね。
というか元気になっていたい。

ブログにも書きましたが、毎日草を生やすのをやり始めて、無事一年を達成しました。
月イチの振り返りブログとともに、自分の学習を駆動させる上で良い取り組みになったなあと思ってます。
ネタも尽きていないので当面は引き続きやっていきたいところ。

引き続きキャリアについては悩み続けているんですが、ひとまずEM的なロールを目指してみようかなと思い始めました。
めちゃくちゃマネジメントがやりたい!というわけでもないんですが、実際やるやらないに関わらず、これまでマネジメントやリーダー経験のない自分にとって、意識して行動してみるだけでも得るものがありそうという目論見です。 というわけでそれ系の本とかイベント参加とかをちょこちょこやっていく予定です。

技術的な話だと、やっとScalaとだいぶ仲良くなってきた感覚になりました。
まだ距離は感じるんですが、それでもかなり馴染んできたなと。
次の方針的には当面は改めて仕事で扱ってるAngularとか、フロントエンドのアーキテクチャを掘り下げていこうかなと思ってます。
フロントはキャリアの中でも書いてた期間が短くてまだまだわからないことも多いんですが、できることを増やしていきたい。

セールだったのでモンハンライズを買ってしまったんですが、ワールドをやってからかなり経ってたのもあり、いちいち最近のモンハンっぽい要素に驚いてしまいます。
とりあえず養殖で上位までは上がったので、サンブレイクまでにある程度戦える状態を目指します。

【雑記】毎日コミットを(ほぼ)1年間継続した

f:id:beatdjam:20220330234000p:plain

過去の記事

経緯

  • 仕事外で勉強するときまとまった時間を取るのが難しかった
  • まとめて時間を作るのではなく、毎日少しずつ時間を取っていくスタイルにした

レギュレーション

  • 草が生える行動であればOK
    • リポジトリを作る
    • Issueを立てる
    • コミット
  • 最低限毎日数分なにかする
    • 用事がある日は寝る前とかにやっておく

この活動でできたこと

感想

  • まとまって時間を取ろうとしていたときよりも勉強に使う時間が増えた
    f:id:beatdjam:20220330235904p:plain
    毎日コミットをはじめてから
    f:id:beatdjam:20220330235937p:plain
    毎日コミットをはじめるまえ
  • 意志力だけではプライベートの時間に動くのは難しい
    • 仕組み化と低いハードルが大事
    • 最初の軌道に乗るまでが大変
  • プライベートと仕事のGithubアカウントが分かれているので、頑張って手を動かさないと草が生えない
    • 個人でやったことが視覚化できるのでこれは良かった
  • 写経ずっとやってると辛いときは別のことしたほうがよい
  • 最低1コミットまでハードルを下げればネタは意外とある