手作り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でデータベースを操作できるツールもいろいろあるようですが、中でもRobomongoとRockMongoが使いやすいと思いました。
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/フォルダとそれ以下のファイルです。
バックエンド
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
<!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>
一覧
<a ng-href="#/users/new">新規</a>
<table border="1">
<tr><th> </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>
追加・編集
氏名 <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/
にアクセスします。
一覧画面
編集画面
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
で配列にして出力しています。 $resource
の query()
に対応する部分です。
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は文字列型では無いので変換しています。 $resource
の get()
に対応する部分です。
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
が呼び出され、ドキュメントの挿入・更新を行います。
終わりに
慣れていない技術の組み合わせなので、無駄や間違っているところがあると思います。指摘して頂けたら嬉しいです。