こんにちは、平奥です!
これは、 TECHSCORE Advent Calendar 2016 TECHSCORE BLOG の22日目の記事です。
弊社サービス Synergy! は、 AngularJS で実装されています。 AngularJS はご存知の方も多いとは思いますが、後継バージョンの Angular2 が登場しました。 Angular2 は AngularJS の問題点が整理されました。その結果、使いやすく、そしてパフォーマンスもものすごく改善されています。
しかし問題点もあります。 AngularJS との互換性がありません。
そのため Angular2 を動作させるには今まで使用していた AngularJS の環境を使うことはできず、新たに動作させる環境を用意しないといけません。
Plnkr などを使えば簡単に Angular2 の検証はできるのですが、せっかくなら Angular2 の MEAN スタックを用意してバックエンドやデータベースも用意していろいろ遊んでみようと思い、Angular2 で MEAN スタックを作ってみました。
そこで学んだことを、要点をまとめて書いてみようと思います。
そもそも MEAN スタックとは
MEAN スタックは Web アプリケーションの開発を簡素化し、高速化するフルスタックの JavaScript のフレームワークです。 MongoDB 、Express 、 AngularJS 、Node.js を組み合わせることによって JavaScript でクライアントからサーバまで開発することができる環境です。
それぞれのソフトウェアの簡単な説明は以下となります。
- AngularJS は JavaScript で書かれた、シングルページアプリケーション( SPA )の開発が可能なフロントエンドのフレームワークです。
- Node.js は Chrome の V8 JavaScript エンジンで動作するサーバーサイドの JavaScript 環境である。軽量で効率的に動作する非同期型のイベント駆動モデルを採用しています。
- MongoDB はデータを非定型のデータ構造の集合体として JSON 形式のデータで蓄える、 NoSQL 型ドキュメント指向データベース管理システムです。
- Express は Node.js 上で動作する Web アプリケーションのフレームワークです。MEAN スタックで使用される場合は Node.js 上で動作する MVC フレームワークとして AngularJS からリクエストを受け取り、レスポンスを返すような役割を担っています。
今回はタイトルにある通り、AngularJS を使うのではなく、Angular2 を使用する MEAN スタックを作成したいと思います。
Anglar2 の MEAN スタックのひな形
ひな形として以下のようなディレクトリ構成にしています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|-- app.js |-- assets | `-- app | |-- app.component.html | |-- app.component.service.ts | |-- app.component.ts | |-- app.module.ts | |-- main.ts | `-- polyfills.ts |-- bin | `-- www |-- models | `-- message.js |-- node_modules |-- package.json |-- public |-- routes | |-- app.js | `-- message.js |-- tsconfig.json |-- views | `-- index.hbs |-- webpack.config.js |
各ディレクトリの説明
- assets: フロントエンド側の Angular2 で使われるファイルを保存するディレクトリです。
- bin: Node.js でサーバを起動するためのファイルを保存するディレクトリです。
- models: 今回は None.js から MongoDB にアクセスするために mongoose を使います。mongoose はデータをモデル化してスキーマベースでロジックを組み立てることができます。このディレクトリにはそのスキーマを定義したファイルを保存します。
- public: 今回は Angular2 のモジュールをモジュールバンドラーである webpack を使ってまとめてこのディレクトリに保存するようにしています。
- routes: Express で提供されているルーティング機能を使って、ルーティングを実装したファイルを保存します。
- views: この MEAN スタックで使用する HTML ファイルを保存します。
サンプルアプリについて
今回は、MEAN スタックを理解できるように、メッセージを DB に登録して、登録情報を一覧表示するサンプルアプリを作成します。
まず MongoDB と mongoose をインストールします。
オフィシャルサイトから実行環境に適したバージョンをインストールしてください。
MongoDB オフィシャルサイト: https://www.mongodb.com/
mongoose オフィシャルサイト: http://mongoosejs.com/
インストールができたら、次はファイルの編集をおこなっていきます。
開発環境を構築するためのファイル
package.json
MEAN スタックで必要なモジュールは npm を使用して取得するようにしています。そのため package.json に必要なモジュールを記載して npm で取得できるようにしています。
ソースは以下となります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
{ "name": "techscore-nodejs-angular2", "version": "1.0.0", "private": true, "scripts": { "start": "node ./bin/www", "build": "del-cli public/js/app && webpack --config webpack.config.dev.js --progress --profile --watch" }, "dependencies": { "@angular/common": "2.0.1", "@angular/compiler": "2.0.1", "@angular/compiler-cli": "0.6.3", "@angular/core": "2.0.1", "@angular/forms": "2.0.1", "@angular/http": "2.0.1", "@angular/platform-browser": "2.0.1", "@angular/platform-browser-dynamic": "2.0.1", "@angular/platform-server": "2.0.1", "@angular/router": "3.0.1", "@angular/upgrade": "2.0.1", "body-parser": "~1.15.2", "cookie-parser": "~1.4.3", "core-js": "^2.4.1", "debug": "~2.2.0", "express": "~4.14.0", "hbs": "~3.1.0", "mongoose": "^4.7.1", "mongoose-unique-validator": "^1.0.3", "morgan": "~1.6.1", "reflect-metadata": "^0.1.3", "rxjs": "5.0.0-beta.12", "serve-favicon": "~2.3.0", "zone.js": "^0.6.23" }, "devDependencies": { "@types/core-js": "^0.9.34", "@types/node": "^6.0.45", "angular2-router-loader": "^0.3.2", "angular2-template-loader": "^0.5.0", "awesome-typescript-loader": "^2.2.4", "del-cli": "^0.2.0", "html-loader": "^0.4.4", "raw-loader": "^0.5.1", "typescript": "^2.0.3", "webpack": "2.1.0-beta.21", "webpack-merge": "^0.14.1" } } |
今回はモジュールバンドラである webpack を使って、フロントのソースをまとめています。scripts プロパティには、開発環境のサーバを起動する start と Angular2 の TypeScript のソースをトランスパイルして、ファイルをまとめるための build スクリプトを6~7行目に定義しています。
webpack.config.js
webpack の設定では部分的に抜粋して取り上げます。
まず、Entryですが、起点となるモジュールを webpack に設定します。ここでは、Angular2 の起動処理が記載されている main.ts を指定しています。
また、webpack が処理した結果を出力するための設定である、output も設定しています。ここで出力した JavaScript のファイルをアプリケーション起動時に参照するようにしています。
1 2 3 |
entry: { 'app': './assets/app/main.ts' }, |
1 2 3 4 5 |
output: { path: './public/js/app', publicPath: "/js/app/", filename: 'bundle.js' } |
他にも TypeScript をトランスパイルするための設定ファイルである、tsconfig.json などもありますが、特に重要ではないので、ここでは割愛させていただきます。
バックエンド側を構成するファイル
app.js
Node.js のメインのファイルです。ここではバックエンドで使用する各アプリケーションの設定が行われています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
var express = require('express'); var path = require('path'); var favicon = require('serve-favicon'); var logger = require('morgan'); var cookieParser = require('cookie-parser'); var bodyParser = require('body-parser'); var mongoose = require('mongoose'); var appRoutes = require('./routes/app'); var messageRoutes = require('./routes/message'); var app = express(); mongoose.connect('mongodb://localhost:27017/techscore'); // view engine setup app.set('views', path.join(__dirname, 'views')); app.set('view engine', 'hbs'); // uncomment after placing your favicon in /public //app.use(favicon(path.join(__dirname, 'public', 'favicon.ico'))); app.use(logger('dev')); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({extended: false})); app.use(cookieParser()); app.use(express.static(path.join(__dirname, 'public'))); app.use(function (req, res, next) { res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept'); res.setHeader('Access-Control-Allow-Methods', 'POST, GET, PATCH, DELETE, OPTIONS'); next(); }); app.use('/', appRoutes); app.use('/message', messageRoutes); // catch 404 and forward to error handler app.use(function (req, res, next) { return res.render('index'); }); module.exports = app; |
7行目、13行目が mongoose の設定です。ここでは、接続する MongoDB の設定を行っています。
ではIPアドレス:ポート番号/ DB 名を指定しています。
それ以外はほとんど Express の設定です。
ヘッダの設定や view の設定などを行っています。
9~10行目、34行目~35行目はルーティングの設定を行っています。
www ファイル
このファイルは Node.js でサーバを起動するための設定などが記載されています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 |
#!/usr/bin/env node /** * Module dependencies. */ var app = require('../app'); var debug = require('debug')('node-rest:server'); var http = require('http'); /** * Get port from environment and store in Express. */ var port = normalizePort(process.env.PORT || '3000'); app.set('port', port); /** * Create HTTP server. */ var server = http.createServer(app); /** * Listen on provided port, on all network interfaces. */ server.listen(port); server.on('error', onError); server.on('listening', onListening); /** * Normalize a port into a number, string, or false. */ function normalizePort(val) { var port = parseInt(val, 10); if (isNaN(port)) { // named pipe return val; } if (port >= 0) { // port number return port; } return false; } /** * Event listener for HTTP server "error" event. */ function onError(error) { if (error.syscall !== 'listen') { throw error; } var bind = typeof port === 'string' ? 'Pipe ' + port : 'Port ' + port; // handle specific listen errors with friendly messages 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; } } /** * Event listener for HTTP server "listening" event. */ function onListening() { var addr = server.address(); var bind = typeof addr === 'string' ? 'pipe ' + addr : 'port ' + addr.port; debug('Listening on ' + bind); } |
22行目でサーバを起動しています。
15~16行目でポートを指定しています。
views/index.hbs
このファイルは、バックエンド側からレスポンスとして返される HTML ファイルです。
この HTML ファイルでは Angular2 の初期に表示する内容が実装されています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<!DOCTYPE html> <html> <head> <base href="/"> <title>Angular 2 Messenger</title> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous"> <link rel='stylesheet' href='/stylesheets/style.css'/> </head> <body> <my-app>Loading...</my-app> <script src="/js/app/bundle.js"></script> </body> </html> |
11行目で Angular2 の Components を指定しています。
また13行目で webpack で作成された JavaScript のファイルを取り込んでいます。
routes/app.js
routes ディレクトリは Express の Routing 機能を実現するファイルです。ここでは REST API で実装することができます。
routes/app.js はルートのディレクトリを指定した場合に実行される処理です。
この処理が実行されると、views/index.hbs をレスポンスで返します。これにより、フロント側の Angular2 が実行されることになります。
1 2 3 4 5 6 7 8 |
var express = require('express'); var router = express.Router(); router.get('/', function (req, res, next) { res.render('index'); }); module.exports = router; |
routes/message.js
このファイルでは、フロント側のメッセージ取得要求( get )とメッセージ設定要求( post )の処理を実装しています。
それぞれ MongoDB からデータを取得、設定しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
var express = require('express'); var router = express.Router(); var Message = require('../models/message'); router.get('/', function (req, res, next) { Message.find(function(err, doc) { if (err) { return res.send('Error!'); } else { return res.status(200).json({messages: doc}); } }); }); router.post('/', function (req, res, next) { var message = new Message({ message: req.body.message }); message.save(function (err, result) { if (err) { return res.status(500).json({ title: 'An error occurred', error: err }); } return res.status(200).json({ message: 'Saved message', obj: result }); }); }); module.exports = router; |
6行目や16行目で実装されている Message オブジェクトが mongoose の機能を実装したオブジェクトになります。
このクラスは models ディレクトリで定義されています。
models/message.js
このファイルでは、mongoose の機能を使用して、スキーマ定義を作成しています。このスキーマを使って、MongoDB に対して処理を要求します。
1 2 3 4 5 6 7 8 |
var mongoose = require('mongoose'); var Schema = mongoose.Schema; var schema = new Schema({ message: {type: String} }); module.exports = mongoose.model('messages', schema); |
フロントエンド側を構成するファイル
assets/app/main.ts
Angular2 を起動するための処理が実装されているファイルです。このファイルは webpack の Entry に記載されており、一番初めに起動します。
1 2 3 4 5 6 7 |
import './polyfills'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { AppModule } from "./app.module"; platformBrowserDynamic().bootstrapModule(AppModule); |
assets/app/app.module.ts
ディレクティブやサービスなどをひとまとめにできる NgModule を実装したファイルです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { HttpModule } from '@angular/http'; import { AppComponent } from "./app.component"; import {componentService} from "./app.component.service"; @NgModule({ declarations: [ AppComponent ], imports: [BrowserModule, HttpModule], bootstrap: [AppComponent], providers: [componentService] }) export class AppModule {} |
以下に各プロパティの説明を記載します。
・import : 各モジュールで定義されているディレクティブなどを登録するプロパティです。
・bootstrap : エントリポイントになるコンポーネントを指定します。
・provider : 使用するサービスを登録するプロパティです。
assets/app/app.component.html
my-app コンポーネントで使用される HTML が記述されたファイルです。
ここでは、メッセージを登録する UI と取得する UI を実装しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<h1> Messages </h1> <ul> <li *ngFor="let item of messages"> {{item.message}} </li> </ul> <div class="config"> <input type="button" (click)="onSend()" value="Get Message"> </div> <h1> Set Message </h1> <div class="config"> <form action="/Message" method="post"> <input type="input" name="message"> <button type="submit">Set Message</button> </form> </div> |
assets/app/app.component.ts
このファイルは my-app コンポーネントの設定が記載されされているファイルです。
コンポーネントの名称を設定する selector や使用する HTML ファイルが指定されています。
また AppComponent クラスも定義され、コンポーネントの振る舞いが実装されています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
import { Component, OnInit } from '@angular/core'; import {componentService} from './app.component.service'; @Component({ selector: 'my-app', templateUrl: './app.component.html', providers: [componentService] }) export class AppComponent { messages: Array<Object>; constructor(private componentService: componentService) {} onSend() { this.componentService.getMessage() .subscribe((resData: any) => { this.messages = resData.messages; } ); } |
assets/app/app.component.service.ts
このファイルは my-app コンポーネントで使用するサービスを実装しています。
このサービスでは、メッセージを取得する getMessage メソッドが実装されています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
import { Observable } from 'rxjs/Rx'; import { Http, Response } from '@angular/http'; import { Injectable } from '@angular/core'; import 'rxjs/Rx'; @Injectable() export class componentService { constructor(private http: Http) {} getMessage() { return this.http.get('http://172.17.123.126:3000/message') .map((response: Response) => { const result = response.json(); return result; }) .catch((error: Response) => Observable.throw(error.json())); } } |
MongoDB の準備
このアプリケーションが動作するように MongoDB を準備します。
といっても用意するのはデータベースだけです。
まずクライアントを立ち上げます。
それからデータベースを作成します。今回は techscore という名称のデータベースを作成します。
動かしてみる
以上でソースコードの説明は完了です。では実際に動作させてみましょう!
ブラウザでサーバが起動している IP アドレスとポート 3000 番を指定してアクセスしてみます。
それでは「Set Message」のテキストボックスに任意のテキストを設定して、「Set Message」ボタンを押下してください。
今の実装だと redirect ができないので、以下のような画面が出てきますが、URL から /Message というパスを削除して、リロードしてください。
そうするとまた初期の画面が表示されます。
今度は「Get Message」ボタンを押下してください。
そうすると、先ほど登録したメッセージが表示されます。
まとめ
いかがでしたでしょうか。今回は MEAN スタックを Angular2 が使えるようにカスタマイズしてみました。実際に置き換えてみると各アプリケーションの役割がしっかり分かれているので、ディレクトリ構成をしっかりと検討してやれば、それほど苦労することなく置き換えることができました。
ですので、たとえば MongoDB を他の DB に置き換えることや、Node.js ではなく、Vert.x に置き換えることとかも予想よりも簡単にできるのではないかなと感じました。
みなさまもいろいろ試してみてください!