2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Angular7 で俺式 MEAN スタック Webアプリを構築する、ほぼ全手順(2)

Last updated at Posted at 2019-02-09

概要

Angular7 で俺式 MEAN スタックを作るための備忘録。
今回は「クライアント側のクラサバ通信対応」を行う。

前提

2019年1月1日時点の情報です。また、以下の環境になっている前提です。

  • Angular CLI: 7.0.6
  • Node.js: 10.15.0
  • npm: 6.4.1

また、「Angular7 で俺式 MEAN スタック Webアプリを構築する、ほぼ全手順(1)」が完了していること。

クラサバ共用部の作成

クライアントとサーバでデータ形式を一致させるために、インターフェース統一用の定義ファイルを格納する領域を作る。ついでに共用する定数などがあれば、それもこちらに用意することにする。

ディレクトリを作成

以下の通りに、新規ディレクトリを src/app 配下に作成しておく

[ルートディレクトリ]
  :
  ├─ common # 新規作成
  │   ├─ apis
  │   │  └─ sample 
  │   │      └─ sample.api.ts
  │   ├─ entities
  │   │  └─ sample 
  :   │      └─ sample.entity.ts
  :   └─ tsconfig.json

tsconfig ファイル作成

以下のとおりに中身を作成する

common/tsconfig.json
{
  "extends": "../tsconfig"
}

インターフェースを作成

独自クラスを以下の通りに作成

common/entities/sample/sample.entity.ts
/** サンプル クラス */
export class Sample {
  /** ID */
  id: number;
  /** 氏名 */
  name: string;
  /** 年齢 */
  age: number;
}

API インターフェースを以下の通りに作成

common/apis/sample/sample.api.ts
import { Sample } from '../../entities/sample/sample.entity';

export const SAMPLE_API_PATH = '/api/sample';

/** サンプル パスパラメータ */
export interface ISamplePathParams {
  /** ID */
  id: number;
}

/** サンプル リクエスト */
export interface ISampleRequest {
  /** 氏名(サンプル) */
  name: string;
  /** 年齢(サンプル) */
  age: number;
}

/** サンプル レスポンス */
export interface ISampleResponse {
  users: Sample[];
}

クライアント側を作成

サーバ API を呼び出すサービスを作っていく。

ディレクトリを作成

以下の通りに、新規ディレクトリを src/app 配下に作成しておく

[ルートディレクトリ]
  ├─ browser
  │   └─ app # 既存
  :      :
  :      └─ services # 新規作成: サービス用
             └─ sample # 新規作成: サンプル個別サービス用

既存の tsconfig ファイル修正

browser/tsconfig.app.json の中身を以下の通りにする。

common ディレクトリの各ファイルを import 時に、../ の多用を防ぐためにエイリアスを設定しておく。

browser/tsconfig.app.json
{
  "extends": "../tsconfig.json",
  "compilerOptions": {
    "baseUrl": "./", // 追加
    "outDir": "../out-tsc/app",
    "types": [],
    // 追加
    "paths": {
      "common/*": ["../common/*"]
    },
  },
  "exclude": [
    "test.ts",
    "**/*.spec.ts"
  ]
}

サービスクラスのファイルを生成

# 移動
cd browser/app/services

# HTTP 基底サービス生成
ng generate s http

# さらに移動
cd sample

# 画面用の専用サービス生成
ng generate s sample

終わったら、 cd ../../../.. で元のディレクトリ位置に戻っておく。

HTTP 基底サービス作成

HTTP 通信を実際に担う部分を、以下のとおりに作成。

browser/app/services/http.service.ts
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders, HttpParams, HttpResponse, HttpErrorResponse } from '@angular/common/http';

@Injectable({
  providedIn: 'root'
})
export class HttpService {

  constructor(protected http: HttpClient) { }

  public async get<Req, Res>(apiPath: string, request?: Req): Promise<HttpResponse<Res>> {
    return await this.send<Req, Res>('GET', apiPath, request);
  }

  public async post<Req, Res>(apiPath: string, request?: Req): Promise<HttpResponse<Res>> {
    return await this.send<Req, Res>('POST', apiPath, request);
  }

  public async put<Req, Res>(apiPath: string, request?: Req): Promise<HttpResponse<Res>> {
    return await this.send<Req, Res>('PUT', apiPath, request);
  }

  public async delete<Req, Res>(apiPath: string, request?: Req): Promise<HttpResponse<Res>> {
    return await this.send<Req, Res>('DELETE', apiPath, request);
  }

  /** HTTP通信 実行 */
  private async send<Req, Res>(method: 'GET'|'POST'|'PUT'|'DELETE', path: string, request?: Req): Promise<HttpResponse<Res>> {
    try {
      const url = window.location.origin + path;
      console.log('[HttpService - url] ', url);

      // GET, PUT の場合 クエリパラメータ生成 (exppress の query にセットされる)
      let httpParams: HttpParams = new HttpParams();
      if ((method === 'GET' || method === 'DELETE') && request) {
        for (const requestKey of Object.keys(request)) {
          if (request[requestKey]) {
            httpParams = httpParams.append(requestKey, request[requestKey]);
          }
        }
      }

      const response = await this.http.request<Res>(method, url, {
        headers: new HttpHeaders({ 'Content-Type': 'application/json' }),
        responseType: 'json',
        params: httpParams,
        body: request
      }).toPromise();
      console.log('[HttpService - response] ', response);

      return { ok: true, status: 200, body: response } as HttpResponse<Res>;
    } catch (error) {
      console.log('[HttpService - error] ', error);

      if (error instanceof HttpErrorResponse) {
        return { ok: false, status: error.status, body: undefined } as HttpResponse<Res>;
      } else {
        return { ok: false, body: undefined } as HttpResponse<Res>;
      }
    }

  }

}

サンプルサービス作成

browser/app/services/sample/sample.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';

import { SAMPLE_API_PATH, ISampleRequest, ISampleResponse } from 'common/apis/sample/sample.api';
import { Sample } from 'common/entities/sample/sample.entity';
import { HttpService } from '../http.service';

@Injectable({
  providedIn: 'root'
})
export class SampleService extends HttpService {

  constructor(protected http: HttpClient) { super(http); }

  public async testGet(request: ISampleRequest, id?: number): Promise<{result: boolean, users: Sample[]}> {
    let apiPath = SAMPLE_API_PATH;
    if (id) { apiPath += `/${id}`; }

    const response = await this.get<ISampleRequest, ISampleResponse>(apiPath, request);

    return { result: response.ok, users: response.body.users };
  }

  public async testPost(request: ISampleRequest): Promise<{result: boolean}> {
    const apiPath = SAMPLE_API_PATH;

    const response = await this.post<ISampleRequest, ISampleResponse>(apiPath, request);

    return { result: response.ok };
  }

  public async testPut(request: ISampleRequest, id: number): Promise<{result: boolean}> {
    const apiPath = SAMPLE_API_PATH + `/${id}`;

    const response = await this.put<ISampleRequest, ISampleResponse>(apiPath, request);

    return { result: response.ok };
  }

  public async testDelete(id: number): Promise<{result: boolean}> {
    const apiPath = SAMPLE_API_PATH + `/${id}`;

    const response = await this.delete<any, ISampleResponse>(apiPath, undefined);

    return { result: response.ok };
  }
}

モジュールに HTTP 通信用モジュールを追加

HTTP 通信を行うために必要なモジュールをインポート。
これをしないとロジック作ってもエラーで動かない。

src/app/app.module.ts
:
import { HttpClientModule } from '@angular/common/http'; // 追加
:

@NgModule({
  :
  imports: [
    :
    HttpClientModule, // 追加
  ],
:
})
export class AppModule { }

コンポーネント修正

TS ファイルを以下のとおりに追加。
(サービス呼び出し処理追加、および sampleList の型を修正)

browser/app/components/home/home.component.ts
import { Component, OnInit } from '@angular/core';

import { Sample } from 'common/entities/sample/sample.entity';
import { SampleService } from '../../services/sample/sample.service';

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

  id: number;
  name: string;
  age: number;

  // エラーメッセージ
  message: string;

  sampleList: Sample[] = [];

  constructor(private service: SampleService) { }

  ngOnInit() {
  }

  test() {
    // エラーメッセージ初期化
    this.message = '';

    // 入力判定 および 表示処理
    if (this.id && this.name && this.age) {
      this.sampleList.push({ id: this.id, name: this.name, age: this.age });
    } else {
      this.message = '未入力の項目があります。必ず全て入力してください';
    }
  }

  // 表示リストをリセット
  resetList() {
    this.sampleList = [];
  }

  async searchUsers() {
    // エラーメッセージ初期化
    this.message = '';

    // 通信実施
    const response = await this.service.testGet({ name: this.name, age: this.age }, this.id);

    if (response.result) {
      // 通信が成功した場合
      this.sampleList = response.users;
    } else {
      // 処理が失敗した場合
      this.message = 'エラーが発生しました';
    }
  }

  async createUser() {
    // エラーメッセージ初期化
    this.message = '';

    // 入力判定 および 作成処理
    if (this.name && this.age) {
      // 通信実施
      const response = await this.service.testPost({ name: this.name, age: this.age });

      if (response.result) {
        // 通信が成功した場合、一覧再取得
        await this.searchUsers();
      } else {
        // 処理が失敗した場合
        this.message = 'エラーが発生しました';
      }
    } else {
      this.message = '「ID」以外の項目は、全て入力してください';
    }
  }

  async updateUser() {
    // エラーメッセージ初期化
    this.message = '';

    // 入力判定 および 更新処理
    if (this.id) {
      // 通信実施
      const response = await this.service.testPut({ name: this.name, age: this.age }, this.id);

      if (response.result) {
        // 通信が成功した場合、一覧再取得
        await this.searchUsers();
      } else {
        // 処理が失敗した場合
        this.message = 'エラーが発生しました';
      }
    } else {
      this.message = '「ID」の項目は、必ず入力してください';
    }
  }

  async deleteUser() {
    // エラーメッセージ初期化
    this.message = '';

    // 入力判定 および 更新処理
    if (this.id) {
      // 通信実施
      const response = await this.service.testDelete(this.id);

      if (response.result) {
        // 通信が成功した場合、一覧再取得
        await this.searchUsers();
      } else {
        // 処理が失敗した場合
        this.message = 'エラーが発生しました';
      }
    } else {
      this.message = '削除する「ID」を入力してください';
    }
  }

}

HTML を以下のとおりに修正(検索ボタン追加)

browser/app/components/home/home.component.html
<div class="home">
  <h2>ホーム画面</h2>

  <!-- <a [routerLink]="'/home2'">go to home2</a> -->

  <!-- エラーメッセージ -->
  <p *ngIf="message" class="error-message">{{ message }}</p>

  <!-- 入力エリア -->
  <div class="condition">
    <label>ユーザID</label>
    <input type="number" [(ngModel)]="id">

    <label>名前</label>
    <input type="text" [(ngModel)]="name">

    <label>年齢</label>
    <input type="number" [(ngModel)]="age">

    <button (click)="test()">表示追加</button>
    <button (click)="searchUsers()">検索</button>
    <button (click)="createUser()">登録</button>
    <button (click)="updateUser()">更新</button>
    <button (click)="deleteUser()">削除</button>
    <button (click)="resetList()">リセット</button>
  </div>

  <!-- 一覧表示エリア -->
  <table>
    <thead>
      <tr>
        <th>ID</th><th>名前</th><th>年齢</th>
      </tr>
    </thead>
    <tbody>
      <tr *ngFor="let element of sampleList">
        <td>{{ element.id }}</td><td>{{ element.name }}</td><td>{{ element.age }}歳</td>
      </tr>
    </tbody>
  </table>
</div>

この後の手順

サーバサイド作らないと動作確認できないので、サーバサイドを作る。

👉 次の開発手順はこちら

2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?