2017/07/19
これらを達成するための最小構成プロジェクトの作り方を3回に分けて紹介します。
Angular CLIで作成したプロジェクトをベースに、
MongoDBに登録したメッセージを画面に一覧で表示するアプリを作成していきます。
メッセージを登録すると一覧に追加されていくようなアプリです。
今回のチュートリアル終了すると下記のようなプロジェクトの構成になります。
リポジトリも用意しているので詳細はそちらを参照してください。
.
├── dist ・・・(1) コンパイル資産出力先
│ └── server ・・・(1-1) コンパイルされたサーバ資産
│ ├── app.js
│ ├── app.js.map
│ ├── bin
│ │ ├── www.js
│ │ └── www.js.map
│ ├── config.js
│ ├── config.js.map
│ ├── models
│ │ ├── message.js
│ │ └── message.js.map
│ ├── public ・・・(1-2) コンパイルされたクライアント資産
│ │ ├── favicon.ico
│ │ ├── index.html
│ │ ├── inline.bundle.js
│ │ ├── inline.bundle.js.map
│ │ ├── main.bundle.js
│ │ ├── main.bundle.js.map
│ │ ├── polyfills.bundle.js
│ │ ├── polyfills.bundle.js.map
│ │ ├── styles.bundle.js
│ │ ├── styles.bundle.js.map
│ │ ├── vendor.bundle.js
│ │ └── vendor.bundle.js.map
│ └── routes
│ ├── message.js
│ └── message.js.map
├── node_modules
│ ├── ...
│ ...
│
├── e2e
│ ├── app.e2e-spec.ts
│ ├── app.po.ts
│ └── tsconfig.e2e.json
├── server ・・・(2) サーバ資産
│ ├── app.ts
│ ├── bin
│ │ └── www.ts
│ ├── config.ts
│ ├── models
│ │ └── message.ts
│ ├── routes
│ │ └── message.ts
│ └── tsconfig.server.json
├── src ・・・(3) クライアント資産
│ ├── app
│ │ ├── app.component.css
│ │ ├── app.component.html
│ │ ├── app.component.spec.ts
│ │ ├── app.component.ts
│ │ ├── app.module.ts
│ │ └── message
│ │ └── message.service.ts
│ ├── assets
│ ├── environments
│ │ ├── environment.prod.ts
│ │ └── environment.ts
│ ├── favicon.ico
│ ├── index.html
│ ├── main.ts
│ ├── polyfills.ts
│ ├── styles.css
│ ├── test.ts
│ ├── tsconfig.app.json
│ ├── tsconfig.spec.json
│ └── typings.d.ts
├── karma.conf.js
├── package-lock.json
├── package.json
├── protractor.conf.js
├── proxy.conf.json ・・・(4)
├── tsconfig.json
├── tslint.json
└── README.md
コンパイルした資産の出力先フォルダ
ここにサーバ側のコンパイルされたjsファイルが出力されます。
serverフォルダを設けているのは本資産とテスト資産を分離したかったからです。
その2. テスト編で説明しますが、サーバ側テスト用jsファイルはdist配下のserver_testフォルダに出力されるようにしています。
コンパイルされたクライアント資産。
サーバ側アプリの資産の一部としてコンパイルされるようにしています。
Expressのアプリでは静的資産をpublicフォルダに置くのが一般的なのでこうしました。
サーバ資産を格納するためのディレクトリ。
いろいろ悩みましたが、TypeScript資産をコンパイルすることとテストすることを考慮してこのような構成にしました。
フロントの実行資産とテスト資産を格納するためのディレクリ。
Angular CLIでプロジェクトを作成するとデフォルトで作成されます。
ビルドやアプリ起動はng
コマンドで実施します。
npm start
でフロント側とサーバ側を同時に起動した時に、クライアント側からサーバ側へのリクエストを送れるようにするためのプロキシ設定です。
Angular CLIをインストール
$ npm install -g @angular/cli
プロジェクトを生成、Angular CLIであらかじめ定義している依存ライブラリをインストール
$ ng new sample
$ cd sample
$ npm install
Angular CLIであらかじめ定義している依存ライブラリの他に必要なものををインストール
express
body-parser
mongoose
nodemon
npm-run-all
$ npm install --save express body-parser mongoose
$ npm install --save-dev @types/mongoose nodemon npm-run-all
Angular CLIで作ったプロジェクトの直下にserver
フォルダを作って、その中にサーバ側の処理を書いていきます。
MongoDBにアクセスするためのモデルを定義します。
DBアクセスにはmangoosを使います。
import * as mongoose from 'mongoose';
const Message = mongoose.model('messages', new mongoose.Schema({
message: {type: String}
}));
export { Message };
エンドポイントごとの処理を記述するルータを定義します。
メッセージの取得と登録にはserver/models/message.ts
を使います。
import * as http from 'http';
import { Router, Response } from 'express';
import { Message } from '../models/message';
const messageRouter: Router = Router();
// 全てのメッセージを取得する
messageRouter.get('/', (req, res, next) => {
Message.find(function(err, doc) {
if (err) {
return res.status(500).json({
title: 'エラーが発生しました。',
error: err.message
});
}
return res.status(200).json({messages: doc});
});
});
// メッセージを登録する
messageRouter.post('/', (req, res, next) => {
const message = new Message({
message: req.body.message
});
message.save((err, result) => {
if (err) {
return res.status(500).json({
title: 'エラーが発生しました。',
error: err.message
});
}
return res.status(200).json({
message: 'メッセージを登録しました。',
obj: result
});
});
});
export { messageRouter };
Expressで使用するルータと依存モジュールを定義するためのファイルを作成します。
メッセージAPIのエンドポイントは/api/messages
に設定し、
mongooseを使ってMongoDBへの接続設定をしています。
クライアント資産はビルドするとpublicフォルダ配下に出力されるようにしているので、
静的資産へのルーティングはpublicフォルダを指定しています。
import * as express from 'express';
import * as path from 'path';
import * as bodyParser from 'body-parser';
import * as mongoose from 'mongoose';
import { messageRouter } from './routes/message';
import { MONGO_URL } from './config';
class App {
public express: express.Application;
constructor() {
this.express = express();
this.middleware();
this.routes();
}
private middleware(): void {
this.express.use(bodyParser.json());
this.express.use(bodyParser.urlencoded({ extended: false }));
// 接続する MongoDB の設定
mongoose.Promise = global.Promise;
mongoose.connect(process.env.MONGO_URL || MONGO_URL, {
useMongoClient: true,
});
process.on('SIGINT', function() { mongoose.disconnect(); });
}
private routes(): void {
// 静的資産へのルーティング
this.express.use(express.static(path.join(__dirname, 'public')));
this.express.use('/api/messages', messageRouter);
// その他のリクエストはindexファイルにルーティング
this.express.get('*', (req, res) => {
res.sendFile(path.join(__dirname, 'public/index.html'));
});
}
}
export default new App().express;
Node.js でサーバを起動するための設定ファイルを作成します。
import * as http from 'http';
import { SERVER_PORT } from '../config';
import app from '../app';
// ポートの設定.
const port = normalizePort(process.env.PORT || SERVER_PORT);
app.set('port', port);
// HTTPサーバ生成.
const server = http.createServer(app);
server.listen(port, () => console.log(`API running on localhost:${port}`));
server.on('error', onError);
server.on('listening', onListening);
// ポートを正規化.
function normalizePort(val): number|string|boolean {
const normalizedPort: number = (typeof val === 'string')
? parseInt(val, 10)
: val;
if (isNaN(normalizedPort)) {
return val;
}
if (normalizedPort >= 0) {
return normalizedPort;
}
return false;
}
// エラーハンドラー.
function onError(error): void {
if (error.syscall !== 'listen') {
throw error;
}
const bind = typeof port === 'string'
? 'Pipe ' + port
: 'Port ' + port;
switch (error.code) {
case 'EACCES':
console.error(bind + ' requires elevated privileges');
process.exit(1);
break;
case 'EADDRINUSE':
console.error(bind + ' is already in use');
process.exit(1);
break;
default:
throw error;
}
}
// サーバ起動時のリスナー.
function onListening(): void {
const addr = server.address();
const bind = (typeof addr === 'string')
? `pipe ${addr}`
: `port ${addr.port}`;
}
サーバ側の設定ファイルを作成します。
ポートとMongoDBのURLを定義しています。
今回MongoDBはローカルにポート27017で立てる想定です。
export const SERVER_PORT = 3000;
export const MONGO_URL = 'mongodb://localhost:27017/test';
Angular CLIでプロジェクトを作成すると最低限のクライアント資産が生成されるので、
ここでは修正が必要なファイル、新規作成するファイルのみ紹介します。
サーバ側からメッセージを取得するためのサービスを新規作成します。
import { Injectable } from '@angular/core';
import { Http, Response } from '@angular/http';
import { Observable } from 'rxjs/Rx';
import 'rxjs/Rx';
@Injectable()
export class MessageService {
constructor(private http: Http) {}
getAll(): Observable<any> {
return this.http
.get('/api/messages')
.map((response: Response) => {
const result = response.json();
return result;
})
.catch((error: Response) => Observable.throw(error.json()));
}
regist(message: string): Observable<any> {
return this.http
.post('/api/messages', {message: message})
.map((response: Response) => {
const result = response.json();
return result;
})
.catch((error: Response) => Observable.throw(error.json()));
}
}
既存のファイルを修正して、messagesを保持するようにします。
MessageServiceを使ってメッセージを取得します。
import { Component, OnInit } from '@angular/core';
import { MessageService } from './message/message.service';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
providers: [ MessageService ]
})
export class AppComponent {
messages: Array<any>;
message: string;
constructor(private messageService: MessageService) {
this.getMessages();
}
getMessages(): void {
this.messageService
.getAll()
.subscribe((res: any) => {
this.messages = res.messages;
});
}
registerMessage(): void {
if (!this.message) {
return;
}
this.messageService
.register(this.message)
.subscribe((res: any) => {
this.message = '';
this.getMessages();
});
}
}
既存のファイルの修正して、メッセージ一覧と登録のUIに書き換えます。
<div>
<div>
<h1>メッセージ一覧</h1>
<button id="getMessagesButton" (click)="getMessages()">メッセージ一覧を最新化</button>
<ul id="messageList">
<li *ngFor="let item of messages">
{{item.message}}
</li>
</ul>
</div>
<div>
<h1>メッセージ登録</h1>
<input type="text" id="registerMessage" [(ngModel)]="message" placeholder="登録するメッセージを入力してください。">
<button type="submit" id="registerMessageButton" (click)='registerMessage()'>登録</button>
</div>
</div>
HttpModule、FormsModule、MessageServiceを追加します。
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { HttpModule } from '@angular/http';
import { FormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
import { MessageService } from './message/message.service';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
HttpModule,
FormsModule
],
providers: [MessageService],
bootstrap: [AppComponent]
})
export class AppModule { }
スクリプトを下記のように修正します。
(npm scriptsは便利ですが、コメントが記述できないのが残念だなーと思いました。)
"scripts": {
...
"start": "npm-run-all -s build:server -p start:*",
"start:client": "ng serve --aot=true --progress=false --proxy-config proxy.conf.json",
"start:server": "run-p watch:server boot:server",
"watch:server": "tsc -w -p ./server/tsconfig.server.json",
"boot:server": "nodemon ./dist/server/bin/www.js",
"build": "run-s build:server build:client",
"build:client": "ng build --output-path=./dist/server/public",
"build:server": "tsc -p ./server/tsconfig.server.json",
"buildRun": "run-s build boot:server",
...
},
/server/tsconfig.server.json
を使います。dist/server/public
)を指定しています。サーバ資産コンパイルときの設定ファイルを作成します。
outDir
で出力先をdist/serverに指定しています。
{
"extends": "../tsconfig.json",
"compilerOptions": {
"preserveConstEnums": true,
"outDir": "../dist/server",
"mapRoot": "../dist/server",
"module": "commonjs"
}
}
start
でクライアントとサーバの2つを起動した時に、クラ
イアントからサーバへのリクエストを送れるようにするためのプロキシ設定ファイルを作成します。
/api
始まるリクエストをサーバへのリクエストとみなしてプロキシ設定を行います。
{
"/api": {
"target": "http://localhost:3000",
"secure": false
}
}
具体的な方法について触れませんが、Dockerでもなんでもいいのでローカルにポート27017でMongoDBを立ち上げておいてください。DB、テーブルの作成などは不要です。
プロジェクト直下で下記コマンドを実行するとアプリが起動します。
$ npm start
起動したらhttp://localhost:4200
にアクセスしてみます。すると下記のようにメッセージ一覧画面が表示され、メッセージを登録すると適宜一覧に追加されていきます。
プロジェクト直下で下記コマンドを実行するとアプリがビルドされdistフォルダ配下に出力されます。
$ npm run build
http://localhost:3000
でアクセスできます。
$ npm run buildRun
今回はAngular CLIベースのプロジェクトをベースにしてMEANスタックの最小構成プロジェクトを構築する方法を紹介しました。プロジェクトを起動、ビルドすることはできるようになったので、次回「その2. テスト編」ではテストコードの作成とテスト実施環境の構築について紹介しようと思います。