概要
Angular6で「俺式 KOAN スタック」で Web アプリ構築するための個人的な備忘録記事。
概要については「こちら」を参照。
事前に「ほぼ全手順(2)」が完了していることが前提。
今回の内容は「画面のボタンから API を呼べるようにする」コーディングを行う。
フロントエンド対応
Web ブラウザで表示するボタンを作り、そこから複数の内部 API が呼び出せるようにする。
1.ホーム画面用 コンポーネント生成
client/app の配下に 新たにホーム画面となるコンポーネントを作成する。ただし、先にホーム画面用モジュールを作成し、そこにコンポーネントをぶら下げる方式にする。
通常、コンポーネントは、AppModule の declarations
に対して追加するが、SPA ではなく複数画面にするつもりなので、別モジュールにした方が後でメンテしやすい。
モジュール作成
# client/app に移動
cd client/app
# modules ディレクトリを作成し、そこに移動
mkdir modules
cd modules
# home モジュール作成
ng generate m home
home というディレクトリが自動生成され、その中に home.module.ts
, home.module.spec.ts
ができあがる。
コンポーネント作成
# コンポーネント作成 (-m オプションでモジュール指定)
ng generate c home -m home
home ディレクトリ内に home.component.html
, home.component.scss
, home.component.ts
, home.component.spec.ts
ができあがる。
home.module.ts
には、コンポーネントが自動インポートされる。
2.ホーム画面 コンポーネント修正
ボタン押下処理を追加
...
export class HomeComponent implements OnInit {
constructor() { }
ngOnInit() {
}
// メソッド追加
async testGet() {
console.log('HomeComponent #testGet 呼出');
}
// メソッド追加
async testPost() {
console.log('HomeComponent #testPost 呼出');
}
}
ボタン要素を追加
<p>
home works!
</p>
<!-- ボタンを追加 -->
<div>
<button (click)="testGet()">GET TEST</button>
</div>
<div>
<button (click)="testPost()">POST TEST</button>
</div>
3.画面遷移できるようにする
AppModule にホーム画面モジュール追加
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { HomeModule } from './modules/home/home.module'; // 追加
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
AppRoutingModule,
HomeModule // 追加
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
画面ルーティング情報にホーム画面コンポーネント追加
path の文字列がURLの末尾に指定されたら、作成したコンポーネントが表示されるように修正。
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { HomeComponent } from './modules/home/home.component'; // 追加
// 修正: const routes: Routes = []; を以下のように変更する
const routes: Routes = [
{
path: '',
redirectTo: 'home',
pathMatch: 'full'
},
{
path: 'home',
component: HomeComponent
}
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
App コンポーネント修正
後で表示確認する時に、既存の記載が邪魔なので、必要なもの以外は決しておく。
<!-- 以下のルーティング記載だけ残して、あとは消す -->
<router-outlet></router-outlet>
4.いったん確認
ローカル環境でアプリ起動
npm run local
Web ブラウザでアクセス
http://localhost:3000
にアクセスし、以下のとおりボタンが表示されていればOK.
この時、ブラウザに搭載の「開発者ツール」などでコンソール表示しておく。

殺風景ですが、無事ホーム画面が出ました。。。
URL は自動的に /home
にリダイレクトされています。
画面のボタンを押してみる
まず、画面の「GET TEST」ボタンを押した結果(ブラウザ コンソール)

続けて、画面の「POST TEST」ボタンを押した結果(ブラウザ コンソール)

こんな風に処理に埋め込んだコンソールが呼ばれていればOK.
5.HTTP 通信サービス生成
画面のボタンを押したら、HTTP 通信でサーバサイドにリクエストするサービスを追加する。
サービス作成
モジュール、コンポーネントをコマンド生成した時と同じ階層から、以下のコマンドを実行
# home ディレクトリに移動
cd home
# サービス生成
ng generate s home
home ディレクトリ配下に home.service.ts
, home.service.spec.ts
ができあがる。
モジュール修正
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { HttpClientModule } from '@angular/common/http'; // 追加
import { HomeComponent } from './home.component';
import { HomeService } from './home.service'; // 追加
@NgModule({
imports: [
CommonModule,
HttpClientModule // 追加
],
declarations: [HomeComponent],
providers: [HomeService] // 追加
})
export class HomeModule { }
サービスに HTTP 通信処理を追加
home.service.ts
を以下のとおりに書き換える。
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
@Injectable()
export class HomeService {
constructor(protected http: HttpClient) { }
async getTest<Req>(request: Req): Promise<any> {
let response: any;
const url = '/api/sample';
try {
// GET リクエスト生成
let httpParams: HttpParams = new HttpParams();
const keys = Object.keys(request);
for (const key of keys) {
httpParams = httpParams.append(key, request[key]);
}
response = await this.http.get<any>(url, {
headers: new HttpHeaders({ 'Content-Type': 'application/json' }),
params: httpParams
}).toPromise();
} catch (error) {
response = {
val: 'GET リクエストでエラーが発生しました。',
error: error
};
}
return response;
}
async postTest<Req>(request: Req): Promise<any> {
let response: any;
const url = '/api/sample';
try {
response = await this.http.post<any>(url, request, {
headers: new HttpHeaders({ 'Content-Type': 'application/json' }),
}).toPromise();
} catch (error) {
response = {
val: 'POST リクエストでエラーが発生しました。',
error: error
};
}
return response;
}
}
コンポーネント修正
サービスの HTTP 処理を呼び出すように修正する
home.component.ts
を以下のとおりに書き換える。
import { Component, OnInit } from '@angular/core';
import { HomeService } from './home.service';
@Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.scss']
})
export class HomeComponent implements OnInit {
constructor(private service: HomeService) { }
ngOnInit() {
}
async testGet() {
console.log('HomeComponent #testGet 呼出');
const request = {testKey: 'aaa', otherKey: 'bbb'};
const response: any = await this.service.getTest(request);
console.log('[response]\n', response);
}
async testPost() {
console.log('HomeComponent #testPost 呼出');
const request = {testKey: 'ccc', otherKey: 'ddd'};
const response: any = await this.service.getTest(request);
console.log('[response]\n', response);
}
}
バックエンド作らないと動作確認ができないので、一旦これで、画面側の HTTP 通信ロジックは完了。
バックエンド対応
複数の内部 API を、画面のボタンに合わせて呼び出せるようにする。
1.バックエンドのコントローラを作成
server ディレクトリ配下に controllers というディレクトリを作成し、
その配下に以下のとおりに sample.controller.ts というファイルを作る。
/** サンプル コントローラ */
export class SampleContoroller {
public async test(request: any): Promise<any> {
// リクエスト内容にコメントを追加しただけの JSON レスポンス
return {
statusCode: 200,
body: { result: `Your Request is\n${JSON.stringify(request)}` }
};
}
}
2.API のパスを管理するクラスを作成
server ディレクトリ配下に managers というディレクトリを作成し、
その配下に以下のとおりに api-path.manager.ts というファイルを作る。
import { SampleContoroller } from '../controllers/sample.controller';
/** API パス インターフェース */
interface IApiInfo {
/** HTTP メソッド */
method: string;
/** API パス */
path: string;
/** 実行処理 */
process: (request: any) => Promise<any>;
}
/**
* API パス 管理クラス
*
* HTTP メソッド、API パス、バックエンド コントローラの関係を管理する)
*/
export class ApiPathManager {
public static readonly apis: IApiInfo[] = [
{ method: 'GET', path: '/api/sample', process: new SampleContoroller().test },
{ method: 'POST', path: '/api/sample', process: new SampleContoroller().test },
];
}
3.API のルーティングを管理するクラスを作成
server/managers ディレクトリ配下に、以下のとおりに middleware-router.manager.ts というファイルを作る。
import * as KoaRouter from 'koa-router';
import { ApiPathManager } from './api-path.manager';
/**
* ミドルウェア ルータ 管理クラス
*
* ミドルウェアのルーティングを設定し, APIを提供する
*/
export class MiddlewareRouterManager {
public routes(): KoaRouter.IMiddleware {
const koaRouter = new KoaRouter();
// 各 API に対応したミドルウェアを提供
for (const api of ApiPathManager.apis) {
if (api.method === 'GET') {
koaRouter.get(api.path, this.controller(api.process));
} else if (api.method === 'POST') {
koaRouter.post(api.path, this.controller(api.process));
} else if (api.method === 'PUT') {
koaRouter.put(api.path, this.controller(api.process));
} else {
koaRouter.delete(api.path, this.controller(api.process));
}
}
return koaRouter.routes();
}
/** コントローラ呼出 */
private controller(controll: (request: any) => Promise<any>) {
return async function (ctx: KoaRouter.IRouterContext) {
// GET, DELETE リクエストの時は、クエリから要求データを生成. それ以外の場合は、ボディから要求データを生成
const ctxRequest = ctx.request;
const request = (ctxRequest.method === 'GET' || ctxRequest.method === 'DELETE')
? ctxRequest.query : ctxRequest.body;
// コントローラの結果を返却
const response = await controll(request);
ctx.status = response.statusCode;
ctx.body = response.body;
};
}
}
先ほど作った api-path.manager.ts の情報を利用して、サービスから投げられる HTTP メソッドと URL を判定し、紐づく各コントローラに分岐するようなロジック。
GET や POST の HTTP メソッドによって、リクエストの取得元が異なる。
4.web-server.ts を修正
今作った middleware-router.manager.ts をインポートし、そこからルーティングされたバックエンドを、 app.use()
を使ってアプリに提供するように変更する。
また、このファイルでは koa-router を使わなくなったので、インポートから消す。
import * as Koa from 'koa';
import * as koaStatic from 'koa-static';
// import * as KoaRouter from 'koa-router'; // この行を削除
import * as bodyParser from 'koa-bodyparser';
import * as config from 'config';
import { join } from 'path';
import { MiddlewareRouterManager } from './managers/middleware-router.manager'; // 追加
...
app.use(bodyParser({
// application/x-javascript 型のリクエストを JSON として処理できるようにする
extendTypes: {
json: ['application/x-javascript']
}
}));
// ***** ↓↓ 削除 ↓↓ *****
// サーバサイド側 ミドルウェア ルーティングを定義
// const koaRouter = new KoaRouter();
// koaRouter.get('/api/*', (ctx) => {
// ctx.status = 200;
// ctx.body = { test: 'API が呼ばれました' };
// });
// ***** ↑↑ 削除 ↑↑ *****
// ***** ↓↓ 追加 ↓↓ *****
// サーバサイド側 ミドルウェアの提供
app.use(new MiddlewareRouterManager().routes());
// ***** ↑↑ 追加 ↑↑ *****
5.確認
アプリ起動
npm run local
アプリ起動したら、Web ブラウザから http://localhost:3000
にアクセス。
「開発者ツール」を開きコンソール表示しておく。

ブラウザから API アクセス
画面の「GET TEST」ボタンを押した結果(ブラウザ コンソール)
GET リクエストのサービスが正常に呼ばれ、サンプルコントローラを通過してレスポンスが返却された。

続けて、画面の「POST TEST」ボタンを押した結果(ブラウザ コンソール)
POST リクエストのサービスが正常に呼ばれ、サンプルコントローラを通過してレスポンスが返却された。

結果
これで、API を画面から呼び出せるようになった。
/api/
に続く、別の API を追加する場合、処理を行うバックエンドを用意した後、server/managers/api-path.manager.ts
にコントローラをインポートして、パスと HTTP メソッドを指定するだけで良い。
必要あれば、パスやメソッドを定数にしておくと良い。
この後やること
ロギング機構を作る