Corredor

ウェブ、プログラミングの勉強メモ。

1つのコマンドで複数の Node.js サーバを起動する

$ node start-servers.js みたいな感じで、1つのコマンドを叩くだけで、複数のポートを使用した Node.js サーバを起動できないかなぁ〜と思っていた。

つまり、例えば、1つのコマンドを叩くだけで、

  • http://localhost:3000/ でメインの Web アプリを見られるようにし、
  • http://localhost:8888/ で管理ツールの Web アプリを見られるようにし、
  • https://localhost:8443/ で HTTPS サーバも見られるようにしたい

ということだ。

これまではサーバを起動する Node.js ファイルを複数作っていたので、$ node server1.js & node server2.js みたいに並列実行すればいいのか…?とか思っていたのだけど、Node.js サーバは1つの JS ファイルから複数 listen() (= 起動) できることに気が付き、以下のようにやってみた。

const http  = require('http');
const https = require('https');
const fs    = require('fs');

// 一気に繋げて書いた場合
const server1 = http.createServer((req, res) => {
  console.log('Port 3000');
  res.writeHead(200);
  res.end('Hello World : 3000');
}).listen(3000);

// 分けて書いた場合
const server2 = http.createServer();
server2.on('request', (req, res) => {
  console.log('Port 8888');
  res.writeHead(200);
  res.end('Hello World : 8888');
});
server2.listen(8888);

// HTTPS サーバも混ぜてみる
const server3 = https.createServer({
  key : fs.readFileSync('key.pem'),
  cert: fs.readFileSync('cert.pem')
}, (req, res) => {
  console.log('HTTPS Port 8443');
  res.writeHead(200);
  res.end('Hello World : 8443 (HTTPS)');
}).listen(8443);

console.log('サーバ起動 :')
console.log('  http://localhost:3000/');
console.log('  http://localhost:8888/');
console.log('  https://localhost:8443/');

server1server2 は書き方が違うだけで挙動は同じ。server3 は前回紹介したオレオレ証明書を使用した HTTPS サーバ。HTTP サーバと HTTPS サーバの共存も問題なし。

複数サーバを1ファイルで起動したので、$ node start-servers.js みたいな形で1コマンドで実行できる。閉じる時も Ctrl + C で3つとも落ちる。コレで良い感じ。

Node.js でオレオレ証明書を利用した簡易 HTTPS サーバを立てる

Real World HTTP ―歴史とコードに学ぶインターネットとウェブ技術

Real World HTTP ―歴史とコードに学ぶインターネットとウェブ技術


※2018年1月29日の記事の改善版 (というかソチラの記事が下書き版だったのに間違えてアップしていた) です。

neos21.hatenablog.com


テストのために自己署名証明書を使用するサーバを立てることになった。

そこで、MacOS に標準で入っている openssl コマンドを利用して自己署名証明書を用意し、Node.js で簡易的な HTTPS サーバを立ててみる。

オレオレ証明書を作る

まずは自己署名証明書を作る。自己署名証明書とは要するに「認証局のお墨付きがない証明書」のことで、データが改竄されていないことの証明には使えない。今回はあくまで開発用に作るが、本来オレオレ証明書は使うべきでないシロモノなので、お忘れなきよう。

MacOS は最初から openssl コマンドが使用できたが、Linux などで openssl コマンドがない場合は別途インストールする。

# yum を使っている場合は以下で
$ yum install openssl

# apt の場合は以下で
$ apt-get install openssl

Windows の場合は「Win32 OpenSSL」というツールをインストールすれば良いようだ (未検証)。

openssl コマンドを使って、秘密鍵 → 公開鍵 → デジタル証明書、の順に作る。

# 秘密鍵を作る → key.pem ができる
$ openssl genrsa -out key.pem 1024

# 公開鍵を作る : key.pem を指定する。csr.pem ができる
$ openssl req -new -key key.pem -out csr.pem
  # 対話式で国だのなんだの聞かれるので、適当に入力したり、空欄で飛ばしたり

# デジタル証明書 : key.pem と csr.pem を指定する。cert.pem ができる
$ openssl x509 -req -in csr.pem -signkey key.pem -out cert.pem

これで key.pemcsr.pemcert.pem という3つのファイルができる。

Node.js で簡易 HTTPS サーバを立てる

次に、Node.js に組み込みの https モジュールを使って、簡易 HTTPS サーバを作る。今回は Node.js v6.11.0 にて検証。

作業ディレクトリに my-server.js (名前は任意) という空ファイルを作成し、同じディレクトリに key.pemcert.pem を格納しておく (csr.pem は今回使わない)。

my-server.js を開き、以下のように実装する。

const fs    = require('fs');
const https = require('https');

// 秘密鍵とデジタル証明書ファイルを指定する
const options = { 
  key : fs.readFileSync('key.pem'),
  cert: fs.readFileSync('cert.pem')
};

// サーバを起動する
https.createServer(options, (req, res) => {
  console.log('リクエストを受信');
  // レスポンスの設定
  res.writeHead(200);
  res.end('Hello World');
}).listen(8443);  // ポートを指定して待受スタート

console.log('自己署名証明書サーバを起動');

https.createServer() の第2引数が、リクエストを受け取った時に実行される関数となる。今回は HTTP ステータスコード 200 を返し、画面に Hello World とだけ表示させることにする。ポートは .listen(8443) 部分で指定しているとおり、8443 となる。

my-server.js が実装できたら、Node.js でファイルを実行する。すると HTTPS サーバが起動する。

$ node my-server.js

自己署名証明書サーバを起動

ターミナルはこのまま待機状態になるので放置。

ブラウザを開き、https://localhost:8443/ にアクセスすると、自己署名証明書を使用している旨の警告が表示されるかと思う。警告を無視してページを開くようにすると、画面に「Hello World」と表示されるはずだ。この際、ターミナルには「リクエストを受信」のログが出ているはず。

終わり

コレにて完了。今回はとりあえず https://localhost:8443/ にアクセスするとオレオレ証明書を利用したページが開ける、というだけの簡素なサーバを立ててみた。ベースは http モジュールと同様なようなので、カスタマイズも可能だ。

Angular In Memory Web API を使ってモックサーバを立てる

Angularデベロッパーズガイド  高速かつ堅牢に動作するフロントエンドフレームワーク

Angularデベロッパーズガイド 高速かつ堅牢に動作するフロントエンドフレームワーク

  • 作者: 宇野陽太,奥野賢太郎,金井健一,林優一,吉田徹生,稲富駿,丸山弘詩
  • 出版社/メーカー: インプレス
  • 発売日: 2017/12/15
  • メディア: 単行本(ソフトカバー)
  • この商品を含むブログを見る

AngularによるモダンWeb開発 実践編~実際の開発で必要な知識を凝縮~

AngularによるモダンWeb開発 実践編~実際の開発で必要な知識を凝縮~

Angular アプリの開発中、Web API サーバをモック化する時は、angular-in-memory-web-api を使うと簡単にモック API サーバが用意できる。

少々つまづいたところがあったので、実装の仕方を確認していこう。

プロジェクトを用意する

まずは Angular CLI の ng new コマンドで Angular プロジェクトの雛形を作ろう。今回は @angular/cli@1.6.4 を使用して、Angular5 系の雛形を作成した。

$ ng new api-example

雛形ができたら、Angular In Memory Web API をインストールする。

$ npm install angular-in-memory-web-api --save

コレでひとまず、$ npm start でサーバが起動するか確認しておこう。

HTTP 通信する画面を作る

今回は AppComponent にて、POST 送信を行う簡単なプログラムを用意する。今回は HttpClient で実装したが、以前の Http モジュールを利用しても問題ない。

import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';

import { environment } from '../environments/environment';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  /** 通信結果を表示するための項目 */
  public result: any;

  /**
   * コンストラクタ
   *
   * @param http HttpClient
   */
  constructor(private http: HttpClient) {}

  /**
   * GET 通信する
   *
   * @param path ドメイン以下のパス
   */
  doGet(path: string): void {
    // サーバ URL : 環境変数によって実際のサーバ URL にするか、モックサーバを示す文字列にするか切り替える
    const serverUrl = environment.production ? 'http://example.com/' : 'mock-server/';
    // サーバ URL とパスを結合して URL を生成し GET 通信する
    this.http.get(serverUrl + path)
      .toPromise()
      .then((response) => {
        this.result = response;
      })
      .catch((error) => {
        this.result = error;
      });
  }
}

HTML 側は以下のとおり。

<h1>Angular In Memory Web API Example</h1>
<ul>
  <li><button type="button" (click)="doGet('users')">ユーザ情報を取得する</button></li>
</ul>
<div *ngIf="result">
  <h2>結果</h2>
  <p>{{ result | json }}</p>
</div>

今回は簡単にするため、サービスクラスも作っていないし、HTML 中に doGet('users') と URL 文字列の一部をもたせたりしていて、かなりお行儀の悪いコードだが、お許しいただきたい。

ポイントは、HttpClient で通信するサーバ URL を開発中だけ http://example.com/ ではなく mock-server/ となるよう切り替えているところ。コレにより、「ユーザ情報を取得する」ボタンを押下すると、開発中は mock-server/users という URL に HTTP 通信を試みるワケである。

コンポーネントができたら、AppModuleimportsHttpClientModule を追加しておく。コレでひとまず HTTP 通信を行おうとする画面ができあがる。

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

import { AppComponent } from './app.component';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    HttpClientModule  // ← 追加
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Web API モックを構成するサービスクラスを作成する

次に、Web API のモックとして、URL やレスポンスデータを定義するサービスクラスを作成する。

$ npm run ng generate service mock-web-api

このコマンドで src/app/mock-web-api.service.ts を作成したら、以下のように実装する。

import { Injectable } from '@angular/core';

import { InMemoryDbService } from 'angular-in-memory-web-api';

@Injectable()
export class MockWebApiService implements InMemoryDbService {
  /** モックデータ : 標準的な Web API の URL と対応させるため、データは配列で定義し、各要素は id プロパティが必須 */
  private api: any = {
    // ユーザ情報とか
    users: [
      { id: 1, name: 'Marty' },
      { id: 2, name: 'Jennifer' }
    ]
  };
  
  /**
   * InMemoryDbService から継承 : モックデータを作成する
   *
   * @return モックデータ
   */
  public createDb(): any {
    return this.api;
  }
}

ポイントは InMemoryDbService を継承している点だ。createDb() もオーバーライドしているメソッドで、このメソッドで返却したオブジェクトが Web API の URL と自動的に対応するようになる。

つまり、この設定だけで以下の URL でそれぞれデータをアクセスできるようになる、ということだ。

  • mock-server/usersthis.api.users の配列データ
  • mock-server/users/1this.api.users の配列のうち、id プロパティが 1 のデータ

この「オブジェクトと URL の自動マッピング」については、あとで詳しく詳細する。

サービスクラスができたら、コレをモックサーバとして動作させるために、AppModule に以下のように追加する。

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

import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api';

import { AppComponent } from './app.component';
import { MockWebApiService } from './mock-web-api.service';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    HttpClientModule,
    HttpClientInMemoryWebApiModule.forRoot(MockWebApiService)  // ← 追加
  ],
  providers: [
    MockWebApiService  // ← 追加
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

コレで完了。

開発中は mock-server/users にアクセスすることになり、MockWebApiService にて定義した users のデータが返却されるようになる。

「ユーザ情報を取得する」ボタン

  ↓ 以下が取得・画面表示される

[ { "id": 1, "name": "Marty" }, { "id": 2, "name": "Jennifer" } ]

ID を指定して特定データのみ取得する

さて、標準的な RESTful API の URL 設計に基づけば、/users はデータを一覧形式で取得でき、/users/2 のように ID を指定すれば、個別の ID が取れるはずだ。

Angular In Memory Web API はこうした RESTful API の URL を自動的に判別してデータを返してくれるので、いきなり /users/2 にアクセスして Jennifer のデータのみを取得することができる。

実際にやってみよう。HTML に以下の1行を追加する。

<h1>Angular In Memory Web API Example</h1>
<ul>
  <li><button type="button" (click)="doGet('users')">ユーザ情報を取得する</button></li>
  <li><button type="button" (click)="doGet('users/2')">ID : 2 のユーザ情報を取得する</button></li>  <!-- ←追加 -->
</ul>
<div *ngIf="result">
  <h2>結果</h2>
  <p>{{ result | json }}</p>
</div>

コレだけで、「ID : 2 のユーザ情報を取得する」ボタンを押すと、以下のようなデータが取得できる。

{ "id": 2, "name": "Jennifer" }

その他にも、クエリパラメータなども受け付けてくれるようだ。詳しくは以下の「HTTP request handling」を参照。

コレは便利だ。しかし問題が…。

モックデータオブジェクト・URL に制約ができる

createDb() で返却したオブジェクトが自動的に URL とマッピングされるのは便利だが、この機能のためにオブジェクトの内容に以下のような制約が発生する。

  • this.api のトップレベルのキー名が URL となる
    • ここでは users というキー名がそのまま /users というパスになる
  • 各キーは配列でデータを持たなくてはならない
    • users: { name: 'Marty' } というようにいきなりオブジェクトを1つ返却する作りにはできない
    • 後述するカスタムハンドリングでレスポンスデータは調整可能だが、オブジェクトの型をチェックしているので、定義時点では配列化が必須。
  • 配列内の各データは URL で指定できる ID となる id プロパティを持たなくてはならない

そして、このようなオブジェクトの制約により、http://example.com/book/genres のように2階層以上くだるパスが扱えないという問題が発生している。標準的な RESTful API の URL 設計に基づいていればこのような URL になることは珍しいかもしれないが、それにしてもモックサーバ用ライブラリの都合で URL が決まってしまうのはつらい。

  • api オブジェクトのキー名を 'book/genres' と指定してもダメで、Collection 'book' not found" というエラーになってしまう。
  • book: { genres: { ... } } のように入れ子のオブジェクトにしたら…?と思ったが、コレだと book が配列でないためにエラーになる。

2階層以上のパスを解釈させる方法

では、どうやって /book/genres のような2階層以上のパスを解釈させるか、というと、InMemoryDbService が持つ parseRequestUrl() というメソッドをオーバーライドし、リクエスト URL を内部的に1階層のパスに変換することで対処できる。

実際にやってみる。MockWebApiService に以下のように bookGenres データと parseRequestUrl() メソッドを追加する。

import { InMemoryDbService, ParsedRequestUrl, RequestInfoUtilities } from 'angular-in-memory-web-api';

@Injectable()
export class MockWebApiService implements InMemoryDbService {
  /** モックデータ : 標準的な Web API の URL と対応させるため、データは配列で定義し、各要素は id プロパティが必須 */
  private api: any = {
    // ユーザ情報とか
    users: [ /* …中略… */ ],
    // 本のジャンル名 : '/book/genres' でアクセスされた時に対応させるデータ
    bookGenres: [
      { id: 1, genre: 'Sience Fiction' },
      { id: 2, genre: 'Drama' },
      { id: 3, genre: 'History' }
    ],
  };
  // …中略…
  
  /**
   * InMemoryDbService から継承 : リクエスト URL を変換する
   *
   * @param url リクエスト URL
   * @param utils リソース情報のユーティリティ
   * @return 変換された URL 情報
   */
  public parseRequestUrl(url: string, utils: RequestInfoUtilities): ParsedRequestUrl {
    // 'book/genres' という URL を 'bookGenres' に変換する
    const replacedUrl = url.replace('book/genres', 'bookGenres');
    // リクエスト情報を変換する
    return utils.parseRequestUrl(replacedUrl);
  }

parseRequestUrl() の第1引数の URL 文字列を replace() で変換し、第2引数の RequestInfoUtilities を使って ParsedRequestUrl 型に変えてやれば良い。

ココでの URL 置換処理は全ての通信に影響するので、慎重に変換したい。例えば以下のような汎用的な置換処理でも同等の結果は得られるが、こうすると今度は /users/2 といった URL が /users2 のようになってしまうため、あまり使えない。

const replacedUrl = url
  .replace('mock-server/', '')  // 先頭の 'mock-server/' を除去する : これによりトップレベルのパスは置換対象外にする
  .replace(/\/./g, (match) => {
    // '/a' 部分を取得し、'A' と大文字化する : これにより 'book/genres' は最終的に 'bookGenres' と置換される
    return match.replace(/\//g, '').toUpperCase();
  })
  .replace(/^/, 'mock-server/');  // 先頭に 'mock-server/' を再度付与する

面倒だが、必要なパスだけ置換するようにした方が良いかも。

実際にデータを取得してみる

それでは実際にデータを取得してみよう。コンポーネントの HTML に以下を追加する。

<li><button type="button" (click)="doGet('book/genres')">本のジャンル情報を取得する</button></li>

これで mock-server/book/genres への HTTP 通信が発生するが、parseRequestUrl() が URL を /mock-server/bookGenres に変換するため、api に宣言された bookGenres オブジェクトが取得され、以下の結果が得られる。

[ { "id": 1, "genre": "Sience Fiction" }, { "id": 2, "genre": "Drama" }, { "id": 3, "genre": "History" } ]

思ったように動いてくれた。

レスポンスデータをカスタマイズする

今度は全く別の例。

次は、/blood-type というパスにアクセスした時に、配列形式ではなく単一のオブジェクトを返却するようにしたい。

Angular In Memory Web API の標準仕様では、前述の /users のように、配列形式でしかデータを取得できないはずだ。しかし、responseInterceptor() というメソッドをオーバーライドすると、特定のパスへのアクセス時にレスポンスデータをカスタマイズできるのだ。

実際に実装してみる。MockWebApiService に以下のように blood-type モックデータと responseInterceptor() メソッドを追加する。

import { Injectable } from '@angular/core';

import { InMemoryDbService, ParsedRequestUrl, RequestInfoUtilities } from 'angular-in-memory-web-api';

@Injectable()
export class MockWebApiService implements InMemoryDbService {
  /** モックデータ : 標準的な Web API の URL と対応させるため、データは配列で定義し、各要素は id プロパティが必須 */
  private api: any = {
    users: [ /* 中略 */ ],
    bookGenres: [ /* 中略 */ ],
    // 血液型情報 : '/blood-type' で内部のオブジェクト部分のみ返却させる
    'blood-type': [
      {
        id: 0,
        // この部分を返却する
        data: {
          A: 'A型',
          B: 'B型',
          AB: 'AB型',
          O: 'O型'
        }
      }
    ]
  };
  
  // …中略…
  
  
  /**
   * InMemoryDbService から継承 : データを返却する処理
   *
   * @param responseOptions レスポンスデータを含むオブジェクト
   * @param requestInfo リクエスト情報
   */
  public responseInterceptor(responseOptions: any, requestInfo: any): any {
    // '/blood-type' へのアクセス時は内部のオブジェクトデータを返却する
    if(requestInfo.collectionName === 'blood-type') {
      responseOptions.body = this.api['blood-type'][0].data;
    }
    
    return responseOptions;
  }

responseInterceptor()responseOptions.body にお好みのデータを代入し直してやれば、レスポンスデータを変更できる。ココでは、キー blood-type の配列0番目の要素が持つ data プロパティの値、つまり連想配列オブジェクトを返却するようにした。

なお、ココで参照している requestInfo.collectionName は、parseRequestUrl() メソッドで変換した後の URL となる。つまり、先程の書籍一覧のレスポンスデータを操作したい場合は、'book/genres' ではなく 'bookGenres' であるかどうかでチェックしないといけない。

HTML 側にこのパスを叩くボタンを追加して、実際に動作させてみよう。

<li><button type="button" (click)="doGet('blood-type')">血液型情報を取得する</button></li>

これで、以下のような結果が得られるはずだ。

{ "A": "A型", "B": "B型", "AB": "AB型", "O": "O型" }

まとめ

  • angular-in-memory-web-api というパッケージを使うと、Angular4 以降のアプリでモック API を実現できる。
  • InMemoryDbService を継承したクラスを用意し、createDb() メソッドをオーバーライドすることで、URL と返却データを定義する。
  • parseRequestUrl() をオーバーライドして URL 文字列を変換すれば、2階層以上のパスでもアクセスできる。
  • responseInterceptor() をオーバーライドすると、特定のパスでレスポンスデータをカスタマイズできる。

かなり機能が多く、まだまだ紹介しきれていないノウハウも多々ある。設定の癖も強いので、一つずつ調べながら実装しよう。