Angular 'Tour of Heroes' でMEANスタック


始めに

前回の記事MEANスタックの一歩前。Angular と Express の連携の続きです。
Angular の公式チュートリアル、'Tour of Heroes'をMEANスタック化します。

前回はAngular の公式からダウンロードした'Tour of Heroes'を若干修正して実際のサーバにアクセスするようにしました。
その後Expressサーバーを構築しました。
ですがデータベースは使用しておらず、サーバー内に配列としてヒーローのリストを持つのみとしていました。

今回はWindows環境ですが、MongoDBをセットアップしてMEANの「M」の部分を完成させたいと思います。

MongoDB のインストール

まずはMongoDBのダウンロードを行います。MongoDB公式で、「Available Downloads」に必要な環境情報を登録して「Download」をクリック。インストールパッケージをダウンロードします。私が使用したパッケージファイル名は「mongodb-win32-x86_64-2012plus-4.2.7-signed.msi」でした。

ダウンロードしたら、一応ウイルスチェックを行いましょう。
公式サイトでウイルスを検出してしまうことは経験上ありませんが、念のため。

続けてインストールを進めていきます。
今回は、すべてデフォルト設定を受け入れるだけなので難しい部分はありませんでした。

では早速、インストールを進めていきます。
まずはインストーラをダブルクリックで起動。

Nextで次に進みます。


ライセンスに同意する必要があります。


Complete を選択します。


問題が無ければデフォルト設定のまま、Nextをクリックします。


GUI環境であるCompassを使用するため、チェックしたままNextをクリックします。


Install をクリックしてインストールを開始します。


インストール作業の途中でMongoDBのGUIツールであるCompasが起動しました。

完了です。
自動で起動したCompasの画面です。

MongoDBにデータベースを登録

'Tour of Heroes'で使用するデータベースをMongoDBに登録します。
先ほどのCompasの画面中央にある緑のConnectボタンをクリックします。

画面上部の「CREATE DATABASE」をクリックします。


データベースの名前の登録を求められるので、「Database Name」と「Collection Name」に"heroes"と入力し、「CREATE DATABASE」をクリックします。


heroes のデータベースが追加されているのが分かります。
「Heroes」をクリックします。


データベースのコレクションとしても、先ほど登録したheroes が登録されているのが分かります。
さらにheroesをクリックします。

すると、データが何もないことが分かります。
ここに新しいデータを登録していきます。

このタイミングでデータを登録しなくても、この後で作成する
MEANスタックの完成形から1項目ずつ登録する事で、'Tour of Heroes'の
データを再現する事もできます。


「ADD DATA」をクリックし、ドロップダウンの「Insert Document」をクリックします。

この様にJSON形式でデータを登録します。
ちなみに、数値以外の項目はすべて「"」でくくる必要があります。
チョットだけはまりポイントでした。コピペ用にテキストも張っておきます。

[
{ "id": 11, "name": "Dr Nice" },
{ "id": 12, "name": "Narco" },
{ "id": 13, "name": "Bombasto" },
{ "id": 14, "name": "Celeritas" },
{ "id": 15, "name": "Magneta" },
{ "id": 16, "name": "RubberMan" },
{ "id": 17, "name": "Dynama" },
{ "id": 18, "name": "Dr IQ" },
{ "id": 19, "name": "Magma" },
{ "id": 20, "name": "Tornado" }
]

この様に、データが登録されました。

Express のMongoDB 対応

まずは Node でMongoDBにアクセスするため mongoose のモジュールを追加します。
コマンドプロンプトでプロジェクトのディレクトリに移動し、

D:\angular\toh-srv>npm install mongoose

として、mongooseをサーバープログラムに追加します。

次にソースファイルを修正していきます。
MongoDB に登録したデータベース対応するため、モデルを定義します。
前回の記事MEANスタックの一歩前。Angular と Express の連携のソースファイルに
model.js を追加します。

toh-srv\model.js
const mongoose = require('mongoose');

mongoose.connect("mongodb://localhost/heroes");

const HeroesModelSchema = new mongoose.Schema({
  id: Number,
  name: String
});

module.exports = mongoose.model('heroes', HeroesModelSchema);

この時、

mongoose.connect("mongodb://localhost/heroes");

の heroes は、MongoDBにデータベースを登録で登録した「Database Name」に対応しています。

続けて、サーバーのメインプログラムを修正していきます。

index.js
const express = require('express'); // expressモジュールを読み込む
const bodyParser = require('body-parser');  // body-parserモジュールを読み込む 
const multer = require('multer'); // multerモジュールを読み込む

var HeroesModel = require('./model');

const app = express(); // expressアプリを生成する
app.use(bodyParser.urlencoded({ extended: false }))
app.use(bodyParser.json())

// heroes リストデータ
/*const heroes  = [
    { id: 11, name: 'Dr Nice' },
    { id: 12, name: 'Narco' },
    { id: 13, name: 'Bombasto' },
    { id: 14, name: 'Celeritas' },
    { id: 15, name: 'Magneta' },
    { id: 16, name: 'RubberMan' },
    { id: 17, name: 'Dynama' },
    { id: 18, name: 'Dr IQ' },
    { id: 19, name: 'Magma' },
    { id: 20, name: 'Tornado' }
  ];
//*/
// 新しいヒーローIDを作成.
function genId(){
    return new Promise(function (resolve, reject){
        HeroesModel.find().sort({id:-1}).limit(1).exec({}, function(err, heroes){
            if(err) return handleError(err);
            if(heroes.length>0) resolve(heroes[0].id + 1);
            else resolve(11);
        });
    });
}

// ヒーローのリストを取得
app.get('/heroes', (req, res) => {
    console.log('@@ get heroes');

    if(req.query.name)
    {
        // req.query.name を含むヒーローのリストを作成して返す.
        const query_name = req.query.name;

        HeroesModel.find( { "name" : new RegExp( ".*"+query_name+".*") } ).select('id name').exec({}, function(err, heroes){
            if(err) return handleError(err);
            res.json(heroes);
        });
    }else{
        HeroesModel.find().select('id name').exec({}, function(err, heroes){
            if(err) return handleError(err);
            res.json(heroes);
        });
    }
});

// idを指定してヒーローを取得
app.get('/heroes/:id', (req, res) => {
    const query_id = req.params.id;
    console.log('@@ get heroes id:' + query_id);

    HeroesModel.find( { "id" : query_id } ).select('id name').exec({}, function(err, heroes){
        if(err) return handleError(err);
        if(heroes.length<=0)res.sendStatus(404);
        res.json(heroes[0]);
    });
});

// 新しいヒーローを登録
app.post('/heroes', (req, res) => {
    const query_name = req.body.name;
    console.log('@@post heroes :' + query_name);

    genId().then(function(newId){
        console.log(newId);
        var heroes = new HeroesModel({
            id: newId,
            name: query_name
        });
        heroes.save(function(err){
            if(err) return handleError(err);
            // 追加した項目をクライアントに返す
            res.json(heroes);
        });
    }).catch(function (error) {
        // 非同期処理失敗。呼ばれない
        console.log('catch err :' );
        console.log(error);
    });
});

// ヒーロー削除
app.delete('/heroes/:id', (req, res) => {
    const query_id = req.params.id;
    console.log('@@delete heroes id:' + query_id);

    HeroesModel.remove( { "id" : query_id }, function(err){
        if(err) return handleError(err);
        // ステータスコード200:OKを送信
        res.sendStatus(200);
    });
});

// ヒーロー名修正
app.put('/heroes', (req, res) => {
    // URLの:idと同じIDを持つ項目を検索
    const query_id = req.body.id;
    const query_name = req.body.name;
    console.log('@@put heroes / req.body:' + query_id);
    console.log('@@put heroes / req.body:' + query_name);

    HeroesModel.update( { "id" : query_id }, {$set: { name: query_name}} ).select('id name').exec({}, function(err){
        if(err) return handleError(err);
        // ステータスコード200:OKを送信
        res.sendStatus(200);
    });
});

// ポート3000でサーバを立てる
app.listen(3000, () => console.log('Listening on port 3000'));

先ほど定義したモデルをインポートしています。
代わりに heroes リストデータ をコメントアウトしています。
この部分は削除して構いません。

新しいヒーローIDを作成する genId 関数と併せて、
get、post、delete、put、それぞれの部分でMongoDBに対応したコードに置き換わっています。
特に post で genId を使用する部分では少し苦労しました。
Promise についての理解がなかったため MDNのサイトExpress Web フレームワーク (Node.js/JavaScript)で基本から学び直しました。

テスト

さて、これで完了です。
あとはサーバーとクライアントを双方起動させれば'Tour of Heroes'の元の状態を再現出来ていると思います。

終わりに

一応 MEANスタックと呼べる状態には出来たと思います。
しかし、本格的なWebアプリを構築しようとするとまだまだやるべき事があるように思います。
ここから何かしらの改造をできるかどうか・・・モチベーション次第かと(^^ゞ

リンク

MDN web docs。 Express だけではなく、Web系の開発のイロハが盛りだくさんでした。
Express Web フレームワーク (Node.js/JavaScript)