概要
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 ファイル作成
以下のとおりに中身を作成する
{
"extends": "../tsconfig"
}
インターフェースを作成
独自クラスを以下の通りに作成
/** サンプル クラス */
export class Sample {
/** ID */
id: number;
/** 氏名 */
name: string;
/** 年齢 */
age: number;
}
API インターフェースを以下の通りに作成
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 時に、../
の多用を防ぐためにエイリアスを設定しておく。
{
"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 通信を実際に担う部分を、以下のとおりに作成。
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>;
}
}
}
}
サンプルサービス作成
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 通信を行うために必要なモジュールをインポート。
これをしないとロジック作ってもエラーで動かない。
:
import { HttpClientModule } from '@angular/common/http'; // 追加
:
@NgModule({
:
imports: [
:
HttpClientModule, // 追加
],
:
})
export class AppModule { }
コンポーネント修正
TS ファイルを以下のとおりに追加。
(サービス呼び出し処理追加、および sampleList
の型を修正)
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 を以下のとおりに修正(検索ボタン追加)
<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>
この後の手順
サーバサイド作らないと動作確認できないので、サーバサイドを作る。