More than 1 year has passed since last update.

手作りMEAN Stack

業務アプリをAngularJS+PHP+MySQLで組んでいたのですが、JavaScriptとPHPの文法の微妙な違いのおかげでケアレスミスが多く出てきてしまいました。

そこで、フロントエンドもバックエンドもJavaScriptで統一できる、流行りのMEAN Stack(MongoDB+Express+AngularJS+Node.js)を試してみようと考えた次第です。

MEAN Stackのひな形を作るツールもいろいろあるのですが、今回は手作りで作成してみます。
WindowsとUbuntuの導入方法を挙げますが、他のOSでもほとんど同じ感じでいけると思います。

M: MongoDBのインストール・設定

言わずと知れたNoSQL界の雄です。

Windowsの場合

http://www.mongodb.org/ こちらからダウンロードしてインストールします。今回は C:\MongoDB にインストールしました。

設定

標準ではDB格納フォルダが /data/db に作られますが、ちょっと分かりにくいので C:\MongoDB\data に格納することにします。予め C:\MongoDB\data フォルダを作成しておきます。

サービスとしてインストールする場合は、ログを格納する場所も必要なので、 C:\MongoDB\log フォルダも合わせて作成しました。

サービスとしてインストールする

mongod --install --dbpath=C:\MongoDB\data --logpath=C:\MongoDB\log\mongo.log --logappend

--logpath はファイル名まで指定する必要があるので注意。

Ubuntuの場合

http://docs.mongodb.org/master/tutorial/install-mongodb-on-ubuntu/
こちらを参考にリポジトリを追加してインストールします。root権限で

apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 7F0CEB10
echo "deb http://repo.mongodb.org/apt/ubuntu "$(lsb_release -sc)"/mongodb-org/3.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-3.0.list
apt update
apt install mongodb-org

データは /var/lib/mongodb 、ログは /var/log/mongodb に入ります。

Mongoシェル

インストールが終わったら、シェル(mongoコマンド)を起動していろいろ試してみます。

Mongoシェルと戯れる
MongoDBのshellを使い倒す

等の記事を参考にしました。JavaScriptがそのまま使えるのでいろんなことが出来そうです。

GUIツール

GUIでデータベースを操作できるツールもいろいろあるようですが、中でもRobomongoRockMongoが使いやすいと思いました。

N: Node.jsのインストール

サーバーサイドJavaScriptです。

Windowsの場合

公式サイト http://nodejs.org/ から簡単にインストールできます。特に設定はしませんでした。

Ubuntuの場合

PPAからリポジトリを追加してインストールします。

add-apt-repository -y ppa:chris-lea/node.js
apt update
apt install nodejs

E: Expressのインストールと設定

Node.js用のウェブアプリケーションフレームワークです。Node.js単体でも頑張ればなんとかなりますが、こちらを導入したほうが圧倒的に楽にアプリを作成できます。

ExpressはNode.jsのパッケージマネージャであるnpmでインストールします。今回のアプリ用に適当なフォルダを作成し、その中で以下のコマンドを実行します。

npm install express

node_modules フォルダ以下にインストールされます。

または、グローバルインストールしてそちらにシンボリックリンクを張っても良いと思います。

npm install -g express
npm link express

最近のWindowsではちゃんとシンボリックリンクを張ってくれます。

body-parserのインストール

リクエストボディの解釈をするために追加モジュールで body-parser をインストールします。

npm install body-parser

node-mongodb-nativeドライバのインストール

npm install mongodb

Node.jsからMongoDBをアクセスするための基本的なモジュールです。Mongooseが有名ですが、今回はよりネイティブなこちらを選択しました。

A: AngularJS

AngularJSはCDNから引っ張ってくるので特にインストールする必要はありません。

CRUDアプリの作成

初期データの投入

Mongoシェルを起動すると、

connecting to: test

と表示されてデータベースtestに接続しているようなので、それをそのまま使います。以下のコマンドを実行します。

db.users.insert([
  {name: "勅使河原", age: 19},
  {name: "長宗我部", age: 20},
  {name: "小比類巻", age: 21}
])

dbは現在接続しているデータベースtest、usersはコレクション(RDBで言うところのテーブルのようなもの)名です。コレクションが存在しないときは自動的に作成されます。JSONと似た形式でドキュメント(RDBで言うところのレコードのようなもの)を挿入できます。

投入されたデータは以下のコマンドで確認できます。

db.users.find()

自動的にObjectId(RDBで言うところのプライマリキー)が付与されています。

ソースコード

作成が面倒な方は、まとめてこちらに置いてあります。
https://github.com/naga3/mean-basic

ファイル・フォルダ構成

アプリのフォルダ
├app.js →エントリポイント
├node_modules/ →npmで導入したファイル群
└api/
 └users →usersコレクションを読み書きするAPIアドレス
└front/ →フロントエンド
  ├index.html
  ├list.html
  └edit.html

作る必要のあるものは、app.jsファイルとfront/フォルダとそれ以下のファイルです。

バックエンド

app.js
var express = require('express');
var bodyParser = require('body-parser');
var mongodb = require('mongodb');

var app = express();
var users;

app.use(express.static('front'));
app.use(bodyParser.json());
app.listen(3000);

mongodb.MongoClient.connect("mongodb://localhost:27017/test", function(err, database) {
  users = database.collection("users");
});

// 一覧取得
app.get("/api/users", function(req, res) {
  users.find().toArray(function(err, items) {
    res.send(items);
  });
});

// 個人取得
app.get("/api/users/:_id", function(req, res) {
  users.findOne({_id: mongodb.ObjectID(req.params._id)}, function(err, item) {
    res.send(item);
  });
});

// 追加・更新
app.post("/api/users", function(req, res) {
  var user = req.body;
  if (user._id) user._id = mongodb.ObjectID(user._id);
  users.save(user, function() {
    res.send("insert or update");
  });
});

// 削除
app.delete("/api/users/:_id", function(req, res) {
  users.remove({_id: mongodb.ObjectID(req.params._id)}, function() {
    res.send("delete");
  });
});

フロントエンド

index

front/index.html
<!doctype html>
<html lang="ja" ng-app="app">
<meta charset="utf-8">
<title>ユーザー管理</title>
<div ng-view></div>
<script src="//code.angularjs.org/1.3.15/angular.min.js"></script>
<script src="//code.angularjs.org/1.3.15/angular-resource.min.js"></script>
<script src="//code.angularjs.org/1.3.15/angular-route.min.js"></script>
<script>
  var app = angular.module('app', ['ngResource', 'ngRoute']);

  app.config(function($routeProvider) {
    $routeProvider.when('/users', {
      templateUrl: 'list.html', controller: 'ListCtrl'
    }).when('/users/:_id', {
      templateUrl: 'edit.html', controller: 'EditCtrl'
    }).otherwise({
      redirectTo: '/users'
    });
  });

  app.factory('User', function($resource) {
    return $resource('/api/users/:_id');
  });

  app.controller('ListCtrl', function($scope, $route, User) {
    $scope.users = User.query();
    $scope.delete = function(_id) {
      User.delete({_id: _id}, function() {
        $route.reload();
      });
    };
  });

  app.controller('EditCtrl', function($scope, $routeParams, $location, User) {
    if ($routeParams._id != 'new') $scope.user = User.get({_id: $routeParams._id});
    $scope.edit = function() {
      User.save($scope.user, function() {
        $location.url('/');
      });
    };
  });
</script>
</html>

一覧

front/list.html
<a ng-href="#/users/new">新規</a>
<table border="1">
  <tr><th>&nbsp;</th><th>氏名</th><th>年齢</th></tr>
  <tr ng-repeat="user in users">
        <td>
          <a ng-href="#/users/{{user._id}}">編集</a>
          <button ng-click="delete(user._id)">削除</button>
        </td>
        <td>{{user.name}}</td>
        <td>{{user.age}}</td>
  </tr>
</table>

追加・編集

front/edit.html
氏名 <input ng-model="user.name"><br>
年齢 <input ng-model="user.age"><br>
<button ng-click="edit()">登録</button>
<a ng-href="/#/users">戻る</a>

実行方法

アプリのトップフォルダで

node app

でapp.jsを実行し、ブラウザで

http://localhost:3000/

にアクセスします。

一覧画面

list.png

編集画面

edit.png

app.js 解説

var app = express();

expressアプリケーションのインスタンスを作成します。

app.use(express.static('front'));
app.use(bodyParser.json());

expressの追加機能であるミドルウェアを読み込んでいます。

staticミドルウェアは、指定したフォルダ内のファイルを、静的ファイルとして公開できます。

bodyParserミドルウェアは、POSTメソッドで送られてきたリクエストボディを解析し、JSONデータは自動的にオブジェクトにしてくれます。

app.listen(3000);

3000番ポートで接続を待ち受けます。

mongodb.MongoClient.connect("mongodb://localhost:27017/test", function(err, database) {

localhostのMongoDBのデータベース「test」に接続します。コールバック引数のdatabaseにデータベースオブジェクトが返ってきます。

MongoDBのデフォルトポート番号は27017です。

  users = database.collection("users");

変数usersにコレクションusersのオブジェクトが入ります。

GET /api/users

app.get("/api/users", function(req, res) {
  users.find().toArray(function(err, items) {
    res.send(items);
  });
});

コレクションの一覧を配列で返します。 users.find() でコレクション全体を取得し、 toArray で配列にして出力しています。 $resourcequery() に対応する部分です。

GET /api/users/:_id

app.get("/api/users/:_id", function(req, res) {
  users.findOne({_id: mongodb.ObjectID(req.params._id)}, function(err, item) {
    res.send(item);
  });
});

ドキュメントひとつを返します。指定したObjectIdのドキュメントを users.findOne() で取得し出力しています。ObjectIdは文字列型では無いので変換しています。 $resourceget() に対応する部分です。

POST /api/users

app.post("/api/users", function(req, res) {
  var user = req.body;
  if (user._id) user._id = mongodb.ObjectID(user._id);
  users.save(user, function() {
    res.send("insert or update");
  });
});

リクエストボディに入ってくるJSONデータによってドキュメントを挿入・更新します。saveメソッドはidが存在しなければinsert、存在すればupdateになります。idは文字列型→ObjectId型に変換しています。

DELETE /api/users/:_id

app.delete("/api/users/:_id", function(req, res) {
  users.remove({_id: mongodb.ObjectID(req.params._id)}, function() {
    res.send("delete");
  });
});

指定したObjectIdのドキュメントを削除します。

index.html 解説

<div ng-view></div>

ngRouteの機能により、テンプレートのHTMLファイルがここに埋め込まれます。

<script src="//code.angularjs.org/1.3.15/angular.min.js"></script>
<script src="//code.angularjs.org/1.3.15/angular-resource.min.js"></script>
<script src="//code.angularjs.org/1.3.15/angular-route.min.js"></script>

CDNよりAngularJSと、REST APIを扱うためのngResource、ルーティングするためのngRouteを読み込みます。

  app.config(function($routeProvider) {
    $routeProvider.when('/users', {
      templateUrl: 'list.html', controller: 'ListCtrl'
    }).when('/users/:_id', {
      templateUrl: 'edit.html', controller: 'EditCtrl'
    }).otherwise({
      redirectTo: '/users'
    });
  });

ルーティングの設定です。

/#/users にアクセスされたときに list.html を読み込んで展開します。

/#/users/_id (_idは各ドキュメントのObjectId)にアクセスされたときに edit.html を読み込んで展開します。

その他のときは /#/users にリダイレクトします。

  app.factory('User', function($resource) {
    return $resource('/api/users/:_id');
  });

app.jsで定義したREST APIを使うためのサービス User を定義しています。

  app.controller('ListCtrl', function($scope, $route, User) {
    $scope.users = User.query();
    $scope.delete = function(_id) {
      User.delete({_id: _id}, function() {
        $route.reload();
      });
    };
  });

一覧画面のコントローラです。 User.query()GET /api/users が呼び出されコレクションusersの一覧が取得されます。
削除リンクが押されたときは DELETE /api/users/:_id が呼び出され、ドキュメントを削除して画面を更新します。

  app.controller('EditCtrl', function($scope, $routeParams, $location, User) {
    if ($routeParams._id != 'new') $scope.user = User.get({_id: $routeParams._id});
    $scope.edit = function() {
      User.save($scope.user, function() {
        $location.url('/');
      });
    };
  });

編集画面のコントローラです。URLが /#/users/new のときは新規、その他のときは編集になります。
登録ボタンが押されたときは POST /api/users が呼び出され、ドキュメントの挿入・更新を行います。

終わりに

慣れていない技術の組み合わせなので、無駄や間違っているところがあると思います。指摘して頂けたら嬉しいです。

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.