【Angular】Ngrx概要

 

Ngrx?

 

f:id:kanmi-program:20201126224833p:plain



 

用語集

Component

 

Action

  • 「何が起こったか」という情報を持ったオブジェクト

  • コンポーネントからのイベントをトリガーとしてActionオブジェクトをStoreにDispatchする

 

Reducer

  • ActionオブジェクトとStateを受け取りStateを更新する

  • 更新する場合はStateを新規のオブジェクトとして作成する

 

Store

  • Stateを保存する場所

 

Selector

  • Storeから特定のデータを取り出す

 

Effects

  • 外部APIとの通信などをComponentから切り分ける

  • Reduxからの拡張機能

 

Service

  • Angularのサービス

 

 

実装

インストール

Store

npm install @ngrx/store --save
ng add @ngrx/store@latest

 

Effects

  • 使う人だけ

npm install @ngrx/effects --save
ng add @ngrx/effects@latest

 

Devtool

  • 使う人だけ

  • productionビルド時はログ参照のみになる

npm install @ngrx/store-devtools --save
ng add @ngrx/store-devtools@latest

 

<参考>

https://ngrx.io/guide/store/install

https://ngrx.io/guide/store-devtools/install

https://ngrx.io/guide/effects/install

 

 

Storeの作成

  • Stateとして管理するものを定義

 

store-definition.ts

import { LoggedInReducer, UserIdReducer } from '../reducer/user-reducer';

/**
* Key定義
*/
export enum Storekey {
 isloggedIn = 'isloggedIn',
 userId = 'userId',
}

/**
* Store定義
*/
export const StoreDefinition = {
 isloggedIn: LoggedInReducer,
 userId: UserIdReducer,
};

/**
* 型定義
*/
export interface AppStore {
 isloggedIn: boolean;
 userId: string;
}

 

 

Actionの作成

  • Stateを参照するActionを定義

 

user-actions.ts

import { createAction, props } from '@ngrx/store';

/**
* ユーザーAction
*/
export const UserActions = {
 login: createAction('user/login'),
 logout: createAction('user/logout'),
 // Actionの引数としてuserIdを定義
 addUser: createAction('user/add', props<{ userId: string }>()),
 deleteUser: createAction('user/delete'),
};

 

Reducerの作成

  • Actionに紐づく処理を定義

 

user-reducers.ts

import { Action, createReducer, on } from '@ngrx/store';
import { UserActions } from '../action/user-actions';

/** ログイン状態 */
export const loggedIn: boolean = false;
/** ユーザーID */
export const userId: string = '';

const loggedInReducer = createReducer(
  loggedIn,
  on(UserActions.login, (state) => {
    // 変更後のstateを返却(新しいオブジェクトとして作成)
    return true;
  }),
  on(UserActions.logout, (state) => {
    return false;
  })
);

const userIdReducer = createReducer(
  userId,
  // Actionの引数としてuserIdを受け取る
  on(UserActions.addUser, (state, { userId }) => {
    return userId;
  }),
  on(UserActions.deleteUser, (state) => {
    return '';
  })
);

export function LoggedInReducer(state: boolean, action: Action) {
  return loggedInReducer(state, action);
}

export function UserIdReducer(state: string, action: Action) {
  return userIdReducer(state, action);
}

 

NgModuleに設定

  • インストール時のng addで自動で追加される

  • Store定義を手動で追加

 

app.module.ts

import { StoreModule } from '@ngrx/store';
import { StoreDefinition } from './state/store/store-definition';
import { StoreDevtoolsModule } from '@ngrx/store-devtools';

imports: [
    // Store定義を追加
    StoreModule.forRoot(StoreDefinition),
    // Devtoolの設定
    StoreDevtoolsModule.instrument({
      maxAge: 25,
      logOnly: environment.production,
    }),
]

 

Componentから呼び出す

  • 定義したActionをStoreにDispatchする

  • 入力した内容でStateを更新する

  • 遷移先のComponentで更新されたStateを取得する

 

遷移元component

import { Router } from '@angular/router';
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs';
import { UserActions } from 'src/app/state/action/user-actions';
import { AppStore, Storekey } from 'src/app/state/store/store-definition';

@Component({
  selector: 'app-test',
  templateUrl: './test.component.html',
  styleUrls: ['./test.component.scss'],
})
export class TestComponent implements OnInit {
  constructor(
    private fb: FormBuilder,
    private router: Router,
    private store: Store<AppStore>
  ) {}

  public sampleForm: FormGroup;
  ngOnInit() {
    this.initForm();
  }

  initForm() {
    this.sampleForm = this.fb.group({
      userId: ['', Validators.required],
      password: ['', Validators.required],
    });
  }

  login(value: any) {
    // ログインActionをdispatch
    this.store.dispatch(UserActions.login());
    // ユーザーID追加Actionをdispatch
    this.store.dispatch(UserActions.addUser({ userId: value.userId }));

    // 画面遷移
    this.router.navigate(['/state']);
  }
}

 

遷移先component

import { Component, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs';
import { AppStore, Storekey } from 'src/app/state/store/store-definition';

@Component({
  selector: 'app-state',
  templateUrl: './state.component.html',
  styleUrls: ['./state.component.scss'],
})
export class StateComponent implements OnInit {
  /** ログイン状態 */
  isloggedIn$: Observable<boolean>;
  /** ユーザーID */
  userId$: Observable<string>;

  constructor(private store: Store<AppStore>) {
    // storeから取得
    this.isloggedIn$ = store.select(Storekey.isloggedIn);
    this.userId$ = store.select(Storekey.userId);
  }

  ngOnInit(): void {}
}

 

遷移先html

<!-- 取得したstateを表示 -->
<p>ログイン状態:{{isloggedIn$ | async}}</p>
<p>ユーザーID:{{userId$ | async}}</p>

【概要】Reduxを始める

 

 

 

概要

  • UIの状態管理を行うライブラリ

    • 状態:ログイン済、未ログインとか

  • Fluxの概念を拡張して作成

  • 中規模~大規模アプリに向いている

 

基本構成

  • 基本は以下の3つ

    • State:状態

    • View:画面

    • Actions:イベント

  • データフロー

    • 初期状態のState(状態)に基づいてView(画面)が表示される

    • ViewからActions(ボタン押下等)が実行される

    • Actionの種類に基づいてStateが更新される

    • 更新されたStateに基づいてViewが更新される

f:id:kanmi-program:20201120223746p:plain



 

 

データフロー詳細

  1. ユーザー入力からActionを作成

  2. StoreへActionをdispatch(送信)

  3. dispatchされたActionとStateをReducerへ渡す

  4. Reducerが作成した新しいStateをStoreが保存

f:id:kanmi-program:20201120224717g:plain

 

 

Reduxの3原則

 

1. Single source of truth

  • アプリ内でStoreは1つのみ

  • Global Stateは単独のオブジェクトとしてStoreに保存

    • どこからでも同じものが取得できる

 

2. State is read-only

  • Stateを直接変更しない

  • Stateの変更はActionをStoreへdispatchする方法のみ

    • 状態の変更が一元化されることで競合が起きない

 

3. Mutations are written as pure functions

  • Reducerは純粋関数にする

    • 関数の実行に副作用無し

    • 関数の戻り値は入力パラメータ(State、Action)のみに依存(参照透過性)

    • 非同期処理やランダム値の計算はしない

  • 既存のStateオブジェクトを更新ではなく、新規にStateオブジェクトを作成する

 

 

用語集(Redux)

Action

  • 「状態を変更する」という情報を持ったオブジェクト

  • typeプロパティ(定数)を必ず持つ

  • typeプロパティの命名規則は「domain/eventName」

    • domain:アクションが属する機能またはカテゴリ

    • eventName:発生した特定のイベント

 

ActionCreator

  • Actionを作成するメソッド

  • Actionを作るのみ

 

Store

  • State(状態)を保管している場所

  • アプリ内で1つだけ存在

  • Storeの機能は以下

    • Stateの保持

    • Stateへのアクセス機能(getState)

    • Stateの更新機能(dispatch)

    • リスナーの登録機能(subscribe)

 

Dispatch

  • Stateを更新する機能

    • StoreにActionを送信し、新規のStateを作成してStoreのStateを更新

 

State

  • アプリの状態

 

Reducer

  • ActionとStateを受け取り、新しいStateを返すメソッド

  • 純粋関数として作る

 

 

 

 

【Angular】IndexedDBを使う

前提

  • AngularでIndexedDBにアクセス

  • ライブラリに Dexie.jsを使う

 

 

IndexedDB?

 

実装

ライブラリのインストール

npm install --save dexie

 

 

DB定義の作成

import Dexie from 'dexie';

export class RamenDB extends Dexie {
   
 // ストア定義
 menu: Dexie.Table<IMenu, string>;
 recipi: Dexie.Table<IRecipi, string>;

 /** DBバージョン */
 dbversion: number = 1;

 constructor() {
   // DB名
   super('RamenDB');

   // ストア定義
   this.version(this.dbversion).stores({
     menu: '++menuId, name, price',
     recipi: 'menuId, men, soup, ingredients',
  });

   // ストア名
   this.menu = this.table('menu');
   this.recipi = this.table('recipi');
}
}

export interface IMenu {
 name: string;
 price: number;
}

export interface IRecipi {
 menuId: number;
 men: string;
 soup: string;
 ingredients: Array<string>;
}

 

データアクセス用のサービスクラスを作成

import { Injectable } from '@angular/core';
import { from, Observable } from 'rxjs';

import { IMenu, RamenDB } from '../table/ramen-table';

@Injectable({
 providedIn: 'root',
})
export class RamenDBService {
 ramenTable = new RamenDB();

 constructor() {}

 // メニューテーブル

 /**
  * メニューを取得
  * @param menuId
  */
 public getMenu(menuId: number): Observable<IMenu> {
   return from(this.ramenTable.menu.get(menuId));
}

 /**
  * メニューを全件取得
  */
 public getAllMenu(): Observable<IMenu[]> {
   return from(this.ramenTable.menu.toArray());
}

 /**
  * メニューの追加
  * @param menu
  */
 public addMenu(menu: IMenu): Observable<number> {
   return from(this.ramenTable.menu.add(menu));
}

 /**
  * メニューの更新
  * @param menuId
  * @param menu
  */
 public updateMenu(menuId: number, menu: IMenu) {
   return from(this.ramenTable.menu.update(menuId, menu));
}

 /**
  * メニューの削除
  * @param menuId
  */
 public deleteMenu(menuId: number) {
   return from(this.ramenTable.menu.delete(menuId));
}
}

 

DBの内容を確認

  • DBの内容はデバッグツールの「Application」タブから確認できる

f:id:kanmi-program:20201116235201p:plain

 

 

 

 

プライバシーポリシー

個人情報の利用目的

当ブログでは、お問い合わせや記事へのコメントの際、名前やメールアドレス等の個人情報を入力いただく場合がございます。 取得した個人情報は、お問い合わせに対する回答や必要な情報を電子メールなどをでご連絡する場合に利用させていただくものであり、これらの目的以外では利用いたしません。

広告について

当ブログでは、第三者配信の広告サービス(Googleアドセンス)を利用しており、ユーザーの興味に応じた商品やサービスの広告を表示するため、クッキー(Cookie)を使用しております。 クッキーを使用することで当サイトはお客様のコンピュータを識別できるようになりますが、お客様個人を特定できるものではありません。

Cookieを無効にする方法やGoogleアドセンスに関する詳細は「広告 – ポリシーと規約 – Google」をご確認ください。

アクセス解析ツールについて

当ブログでは、Googleによるアクセス解析ツール「Googleアナリティクス」を利用しています。このGoogleアナリティクスはトラフィックデータの収集のためにクッキー(Cookie)を使用しております。トラフィックデータは匿名で収集されており、個人を特定するものではありません。

コメントについて

当ブログへのコメントを残す際に、IP アドレスを収集しています。 これはブログの標準機能としてサポートされている機能で、スパムや荒らしへの対応以外にこのIPアドレスを使用することはありません。 なお、全てのコメントは管理人が事前にその内容を確認し、承認した上での掲載となります。あらかじめご了承ください。

【RxJS】ユースケースから理解するRxJS

 

環境

f:id:kanmi-program:20201105234221p:plain

 

 

ユースケース

Observableを作りたい

  • new Observableで値を流していく

import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs';

@Component({
 selector: 'app-rxjs',
 templateUrl: './rxjs.component.html',
 styleUrls: ['./rxjs.component.scss'],
})
export class RxjsComponent implements OnInit {
 constructor() {}

 ngOnInit(): void {}

 /**
  * 注文処理
  */
 order() {
   const menu = new Observable((observable) => {
     try {
       observable.next('お冷');
       setTimeout(() => {
         observable.next('チャーハン');
         observable.next('醤油拉麺');
         setTimeout(() => {
           observable.complete();
        }, 5000);
      }, 3000);
    } catch (e) {
       observable.error(e);
    }
  });

   // 時間測定用
   let time = 0;
   const timer = setInterval(() => {
     time++;
  }, 1000);

   menu.subscribe(
     // 値が流れてきた時の処理
    (result) => {
       console.log(`[${time}秒] 提供:${result}`);
    },
     // エラーが発生した時の処理
    (error) => {
       console.error(error);
    },
     // 処理が完了した時の処理
    () => {
       console.log(`[${time}秒] お会計`);
       clearInterval(timer);
    }
  );
}
}
  • 実行結果

f:id:kanmi-program:20201105234401p:plain



 

 

 

PromiseをObservableに変換したい

  • AmplifyとかのライブラリはPromiseで実装されているのでObservableに統一したい

  • fromを使う

import { Component, OnInit } from '@angular/core';
import { from } from 'rxjs';
import { Storage } from 'aws-amplify';

@Component({
 selector: 'app-rxjs',
 templateUrl: './rxjs.component.html',
 styleUrls: ['./rxjs.component.scss'],
})
export class RxjsComponent implements OnInit {
 constructor() {}

 ngOnInit(): void {}

 /**
  * 注文処理
  */
 orderRamen() {
   from(Storage.list('', { level: 'protected' })).subscribe((result) => {
     console.log(result);
  });
}
}

 

 

複数の非同期通信の結果を待ち合わせて取得する

  • ZIPを使う

  • それぞれの非同期通信の実行結果を取得してログに出力するだけ

import { Component, OnInit } from '@angular/core';
import { from, zip } from 'rxjs';
import { map } from 'rxjs/operators';

@Component({
 selector: 'app-rxjs',
 templateUrl: './rxjs.component.html',
 styleUrls: ['./rxjs.component.scss'],
})
export class RxjsComponent implements OnInit {
 constructor() {}

 ngOnInit(): void {}

 /**
  * 注文処理
  */
 orderRamen() {
   // 注文を受けた順に格納(1人目「太麺、醤油」 2人目「細麺、塩」3人目「たまご麺、味噌」)
   let menOrder = from(['太麺', '細麺', 'たまご麺']);
   let soupOrder = from(['醤油', '塩', '味噌']);

   zip(menOrder, soupOrder)
    .pipe(
       map(([men, soup]) => {
         // 順番に結果を取得して提供
         console.log(`[麺]:${men}, [スープ]:${soup}`);
      })
    )
    .subscribe();
}
}
  • 実行結果

    • observableにそれぞれ値が流れてきてから処理を実施している

f:id:kanmi-program:20201105234401p:plain

 

 

非同期通信でエラーが発生した場合

import { Component, OnInit } from '@angular/core';
import { from, throwError, zip } from 'rxjs';
import { map } from 'rxjs/operators';

@Component({
 selector: 'app-rxjs',
 templateUrl: './rxjs.component.html',
 styleUrls: ['./rxjs.component.scss'],
})
export class RxjsComponent implements OnInit {
 constructor() {}

 ngOnInit(): void {}

 /**
  * 注文処理
  */
 orderRamen() {
   // 注文を受けた順に格納(1人目「太麺、醤油」 2人目「細麺、塩」3人目「たまご麺、味噌」)
   let menOrder = from(['太麺', '細麺', 'たまご麺']);
   let soupOrder = from(['醤油', '塩', '味噌']);
   let error = throwError("落としたException");

   zip(menOrder, soupOrder, error)
    .pipe(
       map(([men, soup]) => {
         // 順番に結果を取得して提供
         console.log(`[麺]:${men}, [スープ]:${soup}`);
      })
    )
    .subscribe(
      (result)=>{},
      (error)=>{
         console.log(error);
      }
    );
}
}
  • 実行結果

    • subscribe側でエラーを検知

f:id:kanmi-program:20201105234304p:plain

 

 

subscribe側に値を渡したい

  • map内でreturnを使えばobservable.nextに値が設定される

import { Component, OnInit } from '@angular/core';
import { from, zip } from 'rxjs';
import { map } from 'rxjs/operators';

@Component({
 selector: 'app-rxjs',
 templateUrl: './rxjs.component.html',
 styleUrls: ['./rxjs.component.scss'],
})
export class RxjsComponent implements OnInit {
 constructor() {}

 ngOnInit(): void {}

 /**
  * 注文処理
  */
 orderRamen() {
   // 注文を受けた順に格納(1人目「太麺、醤油」 2人目「細麺、塩」3人目「たまご麺、味噌」)
   let menOrder = from(['太麺', '細麺', 'たまご麺']);
   let soupOrder = from(['醤油', '塩', '味噌']);

   zip(menOrder, soupOrder)
    .pipe(
       map(([menSozai, soupSozai]) => {
         // 順番に結果を取得して提供
         return { men: menSozai, soup: soupSozai };
      })
    )
    .subscribe(
      (result) => {
         console.log(result);
      },
      (error) => {
         console.log(error);
      }
    );
}
}
  • 実行結果

f:id:kanmi-program:20201105234450p:plain



 

 

通化して処理の切り出し

import { Component, OnInit } from '@angular/core';
import { from, Observable, zip } from 'rxjs';
import { map } from 'rxjs/operators';

@Component({
 selector: 'app-rxjs',
 templateUrl: './rxjs.component.html',
 styleUrls: ['./rxjs.component.scss'],
})
export class RxjsComponent implements OnInit {
 constructor() {}

 ngOnInit(): void {}

 /**
  * 注文処理
  */
 orderRamen() {
   // 注文を受けた順に格納(1人目「太麺、醤油」 2人目「細麺、塩」3人目「たまご麺、味噌」)
   let menOrder = from(['太麺', '細麺', 'たまご麺']);
   let soupOrder = from(['醤油', '塩', '味噌']);

   this.createRamen(menOrder, soupOrder).subscribe((result) => {
     console.log(`[麺]:${result.men}, [スープ]:${result.soup}`);
  });
}

 /**
  * 作成処理
  * @param menOrder 麺の注文
  * @param soupOrder スープの注文
  */
 private createRamen(
   menOrder: Observable<string>,
   soupOrder: Observable<string>
): Observable<any> {
   return zip(menOrder, soupOrder).pipe(
     map(([menSozai, soupSozai]) => {
       // 順番に結果を取得して提供
       return { men: menSozai, soup: soupSozai };
    })
  );
}
}

 

 

非同期処理の実行結果を使って非同期処理を実行する

  • 以下のオペレーターを仕様によって使い分ける

    • concatMap

      • 値が流れてきた順番で実行

    • mergeMap

      • 準備ができたものから随時実行

    • switchMap

      • 流れてきた値の処理中に再度値が流れてきたら前の処理はキャンセル

 

例)Cognitoのトークンを取得してAPI Gatewayに通信する

import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { switchMap } from 'rxjs/operators';
import { RestGetInput } from '../aws/api/data/rest-get-input';

import { RestService } from '../aws/api/rest.service';
import { CognitoService } from '../aws/auth/cognito.service';

@Injectable({
 providedIn: 'root',
})
export class OrderService {
 private API_NAME = 'RAMEN';
 private API_PATH = '/order';

 constructor(
   private restService: RestService,
   private cognitoService: CognitoService
) {}

 /**
  * 注文情報取得
  * @param orderId
  */
 public getOrder(orderId: string): Observable<IOrderInfo> {
   let restGetInput = new RestGetInput();

   restGetInput.apiName = this.API_NAME;
   restGetInput.path = this.API_PATH;
   restGetInput.init = { queryStringParameters: orderId };

   return new Observable((observer) => {
     // Token取得
     this.cognitoService
      .getAuthToken()
      .pipe(
         switchMap((token) => {
           restGetInput.token = token;
           // GET通信
           return this.restService.get(restGetInput);
        })
      )
       // 通信処理の確認
      .subscribe(
        (result) => {
           // ステータスコードチェック
           const statusCode = result.statusCode;
           if (statusCode !== 200) {
             // エラー通知
             observer.error('取得処理に失敗');
             return;
          }

           const respnse = result.response;

           // 処理結果を通知
           observer.next({
             orderId: respnse.orderId,
             order: respnse.order,
          });
        },
        (error) => {
           // エラー通知
           observer.error('取得処理に失敗');
        }
      );
  });
}
}

/**
* 注文情報
*/
interface IOrderInfo {
 orderId: string;
 order: Array<string>;
}

【AWS】S3からダウンロードしたファイルのContent-Typeが変なんです

 

Content-Typeがbinary/octet-stream

現象

S3からダウンロードしたファイルのContent-Typeがbinary/octet-streamになってしまう。

 

原因

S3へのアップロード時にContent-Typeを指定していない。

APIでのアップロードなどで忘れがち。

 

対策

Amplifyの場合

// ファイルアップロード
Storage.put(key, fileObject, {
 level: fileLevel,
 contentType: ${contentType},
 progressCallback(progress) {},
});

【AWS】Amplify使ってAPI Gatewayにリクエスト送信したい

 

目的

  • AWS Amplifyを使用してAWS Gatewayに通信する

  • Amplifyコマンドは使わない。AWS SDK機能のみ使う

  • 実装はAngular

  • Angular CLIの環境構築は実施済とする

 

<参考>

Amplify Docs

https://docs.amplify.aws/start/getting-started/installation/q/integration/angular

 

環境

NO 項目 バージョン
1 Angular 10.0.5
2 Amplify 3.3.4
3 Typescript 3.9.5

 

実装

Angularプロジェクトを作成

ng new ${プロジェクト名}

 

Amplifyライブラリを追加

npm install --save aws-amplify @aws-amplify/ui-angular

 

AWSの設定ファイルを作成

フォーマットはAmplifyコマンドで生成されてるものに合わせました。

 

aws-export.jsを作成

${プロジェクトルート}/src/aws-exports.js

import Amplify from 'aws-amplify';

// cognitoやS3に接続する設定を追記する箇所
const awsmobile = {
};

export default awsmobile;

// API Gatewayの設定
Amplify.configure({
 API: {
     endpoints: [
        {
             name: '${API名}',
             endpoint: '${エンドポイント}'
        }
    ]
}
});

 

API名は↓を設定。

f:id:kanmi-program:20201030234841p:plain



エンドポイントは↓を設定。

f:id:kanmi-program:20201030234850p:plain



 

設定ファイル読み込み処理を実装

app.module.tsに実装しました。

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';

// Amplifyと設定ファイルをインポート
import Amplify from 'aws-amplify';
import config from '../aws-exports';

// 設定ファイルを読み込ませる
Amplify.configure(config)

@NgModule({
 declarations: [
   AppComponent
],
 imports: [
   BrowserModule,
   AppRoutingModule
],
 providers: [],
 bootstrap: [AppComponent]
})
export class AppModule { }

 

Shimの追加

polyfills.tsにglobalとprocessのShimを追記する。

/**
* This file includes polyfills needed by Angular and is loaded before the app.
* You can add your own extra polyfills to this file.
*
* This file is divided into 2 sections:
*   1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
*   2. Application imports. Files imported after ZoneJS that should be loaded before your main
*     file.
*
* The current setup is for so-called "evergreen" browsers; the last versions of browsers that
* automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
* Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
*
* Learn more in https://angular.io/guide/browser-support
*/

/***************************************************************************************************
* BROWSER POLYFILLS
*/

/** IE10 and IE11 requires the following for NgClass support on SVG elements */
// import 'classlist.js'; // Run `npm install --save classlist.js`.

/**
* Web Animations `@angular/platform-browser/animations`
* Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
* Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
*/
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.

/**
* By default, zone.js will patch all possible macroTask and DomEvents
* user can disable parts of macroTask/DomEvents patch by setting following flags
* because those flags need to be set before `zone.js` being loaded, and webpack
* will put import in the top of bundle, so user need to create a separate file
* in this directory (for example: zone-flags.ts), and put the following flags
* into that file, and then add the following code before importing zone.js.
* import './zone-flags';
*
* The flags allowed in zone-flags.ts are listed here.
*
* The following flags will work for all browsers.
*
* (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
* (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
* (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
*
* in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
* with the following flag, it will bypass `zone.js` patch for IE/Edge
*
* (window as any).__Zone_enable_cross_context_check = true;
*
*/

/***************************************************************************************************
* Zone JS is required by default for Angular itself.
*/
import 'zone.js/dist/zone';  // Included with Angular CLI.


/***************************************************************************************************
* APPLICATION IMPORTS
*/
// globalとprocessのShimを追記
(window as any).global = window;
(window as any).process = {
   env: { DEBUG: undefined },
};

 

リクエスト送信処理の実装

 

GET通信の場合

import { Component } from '@angular/core';
import { from } from 'rxjs';

// amplifyをインポート
import { API } from 'aws-amplify';

@Component({
 selector: 'app-root',
 templateUrl: './app.component.html',
 styleUrls: ['./app.component.scss'],
})
export class AppComponent {
 title = 'AmplifyTest';

 /**
  * API Gatewayに通信する
  */
 sendRequest() {
   const apiName = '${API名}';
   const path = '${APIパス}';
   const init = {
     // クエリパラメータを設定
     queryStringParameters: {
       orderId: '1234',
    },
     // ヘッダーを設定
     headers: {
       'Content-Type': 'application/json',
    },
  };

   // API実行
   // RXJSで扱いたいためPromiseをfromで変換
   from(API.get(apiName, path, init)).subscribe(
    (result) => {
       console.log(result);
    },
    (error) => {
       console.log(error);
    }
  );
}
}

※CORSでエラーが出る場合(1)を参照

 

POST通信の場合

import { Component } from '@angular/core';
import { from } from 'rxjs';

// amplifyをインポート
import { API } from 'aws-amplify';

@Component({
 selector: 'app-root',
 templateUrl: './app.component.html',
 styleUrls: ['./app.component.scss'],
})
export class AppComponent {
 title = 'AmplifyTest';

 /**
  * API Gatewayに通信する
  */
 sendPOSTRequest() {
   const apiName = 'TestLambda-API';
   const path = '/history';
   const init = {
     // ボディを追加
     body: {
       name: 'taro',
       order: '醤油マシマシ',
       date: '2020-10-11',
    },
     // ヘッダーを追加
     headers: {
       'Content-Type': 'application/json; charset=utf-8',
    },
  };

   // API実行
   // RXJSで扱いたいためPromiseをfromで変換
   from(API.post(apiName, path, init)).subscribe(
    (result) => {
       console.log(result);
    },
    (error) => {
       console.log(error);
    }
  );
}
}

※CORSでエラーが出る場合(1)を参照

 

 

その他

(1)CORSでエラーになる

①CORSを適用したいAPIに以下のパスを追加する。

No 項目
1 パス {proxy+}
2 メソッド OPTIONS

 

②追加したパスに適当なLambdaを統合する。

※ドキュメントにCORSのレスポンスが記載されているがAPI Gateway V2の場合無視されるので、

 左メニュー「開発」→「CORS」で設定する。

 

Lambda

※初期状態でもOK

import json

def lambda_handler(event, context):
   # TODO implement
   return {
       'statusCode': 200,
       'body': json.dumps('Hello from Lambda!')
  }

 

CORS

f:id:kanmi-program:20201030234856p:plain

 

<参考>

REST API リソースの CORS を有効にする

https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/how-to-cors.html

 

Angular HttpInterceptorを使いたい

AmplifyはHTTPクライアントにAxiosを使用しているためAngularのInterceptorが使用できない