JavaScriptの勉強を兼ねて、Webサービスの作り方まとめです。
簡単なTodoリストを作ります。
AngularJS+NodeJS(ExpressJS)+MongoDB
Webサービスを作るには色々と手段はありますが、今回選んだのは上記の組み合わせ。
サーバからクライアントまで全部JavaScriptで書けるためです。
MongoDBのサイトでは、MEANスタックなどと呼ばれている模様です。
以下、meanstack-sampleと名付けて、動くようになるまでにやったことを順番に書いていきます。
Yeomanの設定
NodeJS(+ExpressJS)でRESTもどきを作る
まずはサーバ側が無いと動かないので、
Node.jsでRESTっぽく値を返してくれるものを準備します。
Node.jsのWebアプリケーションフレームワークであるExpressをインストールします。
詳しいガイドはこちら。
% npm install express
Node.js/Expressを使って以下をweb.jsとして作成します。
var express = require('express'),
http = require('http'),
path = require('path'),
db = require('./db.js'),
application_root = __dirname;
var app = express();
app.configure(function() {
app.set('port', process.env.PORT || 3000);
app.set('view engine', 'ejs');
// AngularJSのディレクトリを静的ファイルとして追加
app.use(express.static(path.join(application_root, "app")));
app.use(express.cookieParser());
app.use(express.bodyParser());
app.use(express.methodOverride());
app.use(app.router);
});
app.get('/todo', db.read); // GETの処理
app.post('/todo', db.create); // POSTの処理
app.delete('/todo/:id', db.delete); // DELETEの処理
app.put('/todo/:id', db.update); // PUTの処理
// サーバ起動
http.createServer(app).listen(app.get('port'), function(){
console.log('Express server listening on port ' + app.get('port'));
});
URLを定めて、HTTPのGET、POST、DELETE、PUTを使うことで
CRUDを実現します。
AngularJS側からHTTPメソッドを使うことで、簡単にデータ操作ができるようになります。
MongoDBのインストールとNodeJSとの接続
サーバ側の仕組みが作れたので、次はDBと接続します。
同じJavaScriptで、ということでMongoDBを選んでみました。
MongoDBはWikipediaによると、
オープンソースのドキュメント指向データベースである。C++言語で記述されており、開発とサポートはMongoDB Inc.によって行なわれている。MongoDBはRDBMSではなく、いわゆるNoSQLと呼ばれるデータベースに分類されるものである。
というもので、SELECT文等のSQLを使わず、JavaScriptのメソッドでデータ操作をします。
インストール等の準備は、ここを見ればできます。
Node.jsからMongoDBに接続するためには、色々なライブラリが存在しています。
ぐぐってみると、mongooseというものが有名なようですが、
今回はより単純なmongodbというライブラリを使ってみます。
これは、ターミナルからMongoDBを使う場合とほぼ同じ感じで使えるみたいです。
% npm install mongodb
でインストールして、web.js内で利用しているdb.jsで接続部分を作ります。
var mongo = require('mongodb');
var Db = mongo.Db,
BSON = mongo.BSONPure;
var mongoUri = 'mongodb://localhost/contentdb';
// CRUDのC
exports.create = function(req, res) {
var content = JSON.parse(req.body.mydata);
Db.connect(mongoUri, function(err, db) {
db.collection('todolist', function(err, collection) {
collection.insert(content, {safe: true}, function(err, result) {
res.send();
db.close();
});
});
});
};
// CRUDのR
exports.read = function(req, res) {
Db.connect(mongoUri, function(err, db) {
db.collection('todolist', function(err, collection) {
collection.find({}).toArray(function(err, items) {
res.send(items);
db.close();
});
});
});
};
// CRUDのU
exports.update = function(req, res) {
var content = JSON.parse(req.body.mydata);
var updatedata = {};
updatedata.data = content.data;
updatedata.checked = content.checked;
var id = req.params.id;
Db.connect(mongoUri, function(err, db) {
db.collection('todolist', function(err, collection) {
collection.update({'_id':new BSON.ObjectID(id)}, updatedata, {upsert:true}, function(err, result) {
res.send();
db.close();
});
});
});
};
// CRUDのD
exports.delete = function(req, res) {
var id = req.params.id;
console.log(id);
Db.connect(mongoUri, function(err, db) { // add Db.connect
db.collection('todolist', function(err, collection) {
collection.remove({'_id':new BSON.ObjectID(id)}, {safe:true}, function(err, result) {
res.send();
db.close();
});
});
});
};
基本はDBに接続、コレクションを選択、コレクションを操作、DBクローズの流れで処理をします。
毎回DBに接続せずにコネクションを保持するやりかたもあると思いますが、
Heroku上で動かそうとしたら上手く行かず、この形式にしました。
AngularJSとRESTの連携
ここまでくればサーバ側は完成しているので、後はクライアント側をAngularで作っていきます。
yeomanを使ってyo angular後の状態として編集をしていきます。
app/views/main.htmlと、app/scripts/controllers/main.jsを修正します。
画面は簡単なTODOリストです。(何故かHTML内の中括弧が表示できないので、全角にしています。)
<div class="hero-unit">
<input type="text" ng-model="newtodo"><input type="button" value="add todo" ng-click="createTodo()">
<ul>
<li ng-repeat="todo in todolist">
<input type="checkbox" ng-change="updateTodo(todo)" ng-model="todo.checked">
<span class="done-{{todo.checked}}">{{todo.data}}</span>
<input type="button" value="×" ng-click="deleteTodo(todo)">
</li>
</ul>
</div>
'use strict';
angular.module('angularApp').controller('MainCtrl', function ($scope, $http) {//, $templateCache) {
var url = '/todo';
$scope.todolist = [];
$scope.getTodo = function() {
$http.get(url).success(function(data) {
$scope.todolist = data;
});
};
$scope.createTodo = function() {
var todo = {};
todo.checked = false;
todo.data = this.newtodo;
var senddata = 'mydata=' + JSON.stringify(todo);
$http({
method: 'POST',
url: url,
data: senddata,
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
}).success(function(response) {
$scope.getTodo();
}).error(function(response) {
});
this.newtodo = "";
};
$scope.updateTodo = function(todo) {
var senddata = 'mydata=' + JSON.stringify(todo);
$http({
method: 'PUT',
url: url + '/' + todo._id,
data: senddata,
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
}).success(function(response) {
$scope.getTodo();
}).error(function(response) {
});
};
$scope.deleteTodo = function(todo) {
var senddata = 'mydata=' + JSON.stringify(todo);
console.log(senddata);
$http({
method: 'DELETE',
url: url + '/' + todo._id,
data: senddata,
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
}).success(function(response) {
$scope.getTodo();
}).error(function(response) {
});
};
$scope.getTodo();
});
ここまででひととおり動くようになったので、ここまでのソースをgithubに置きました。
動かすと、以下のような画面になります。
NodeJSでのOAuth認証
ここまで作成したものに認証機能を付けていきます。
修正箇所は主に以下の3点です。
- Node.jsのPassportライブラリを利用して、認証機能追加(web.js修正)
- データベースの項目にuidを追加(db.js修正)
- クライアント側のapp.jsを修正、login.html追加
web.js修正
Node.jsのPassportというライブラリを使うと、簡単に様々な認証機能を持たせることができます。今回はPassportの公式サイトを参考に、Twitter認証を追加します。
新たにauthという関数を用意して、認証が通っているかを確認した上で、認証の上でDB操作を行う用に変更します。
なお、consumerKeyや、consumerSecretは、TwitterのDeveloperのページから取得できます。
/* passportの設定 */
var passport = require('passport'),
TwitterStrategy = require('passport-twitter').Strategy;
// Passport: TwitterのOAuth設定
passport.use(new TwitterStrategy({
consumerKey: "your key",
consumerSecret: "your secret",
callbackURL: "/auth/twitter/callback"
}, function(token, tokenSecret, profile, done) {
// ユーザIDを設定
profile.uid = profile.provider + profile.id;
process.nextTick(function() {
return done(null, profile);
});
}));
// Serialized and deserialized methods when got from session
passport.serializeUser(function(user, done) {
done(null, user);
});
passport.deserializeUser(function(user, done) {
done(null, user);
});
// Define a middleware function to be used for every secured routes
// 認証が通っていないところは401を返す
var auth = function(req, res, next){
if (!req.isAuthenticated()) {
res.send(401);
} else {
next();
}
};
var app = express();
app.configure(function() {
...
// OAuth認証用
app.use(express.session({ secret: 'hogehoge', cookie: {maxAge: 1000 * 60 * 60 * 24 * 30} }));
app.use(passport.initialize()); // Add passport initialization
app.use(passport.session()); // Add passport initialization
app.use(app.router);
});
// route to log in
app.get("/auth/twitter", passport.authenticate('twitter'));
app.get("/auth/twitter/callback", passport.authenticate('twitter', {
successRedirect: '/',
falureRedirect: '/login'
}));
app.get('/todo', auth, db.read); // GETの処理。要認証(auth関数の後にこれまでの処理実施)
...
db.jsの修正
ユーザ毎にデータを持つため、MongoDBのCRUD操作の際にuidを追加します。
...
// CRUDのC
// セッション内のUIDに紐付けてデータを作成する
exports.create = function(req, res) {
var uid = req.user.uid;
var content = JSON.parse(req.body.mydata);
content.uid = uid;
Db.connect(mongoUri, function(err, db) {
db.collection('todolist', function(err, collection) {
collection.insert(content, {safe: true}, function(err, result) {
res.send();
db.close();
});
});
});
};
...
app.jsの修正(とlogin.html追加)
web.jsのauth関数と連動して、認証されていない際に帰ってくる401エラーを検知する機能を実現します。
HTTPレスポンスを確認して、401エラーが含まれていればログインページに飛ばす処理を作ります。
AngularJS公式の$httpページを参考に作りました。
が、使う前に「$q and deferred/promise APIs」の理解が必要と記載があり、そこが理解できていないため、
過不足があるかもしれません。
...
// []で囲わないと、gruntでminifyした際にエラーとなる
$httpProvider.responseInterceptors.push(['$q', '$location',function($q, $location) {
return function(promise) {
return promise.then(function(response) {
// Success: 成功時はそのまま返す
return response;
}, function(response) {
// Error: エラー時は401エラーならば/loginに遷移
if (response.status === 401) {
$location.url('/login');
}
return $q.reject(response);
}
);
};
}]);
...
<div class="container">
<div class="page-header">
login
</div>
<div>
<p><button class="btn-auth btn-twitter" onclick="location.href='/auth/twitter'">
Sign in with <b>Twitter</b></button></p>
</div>
</div>
まとめと参考
認証を追加したところまでのソースコードはgithubに置いてあります。見た目もしょぼく、セキュリティ面でも怪しいところはありますが、一通り動作するはずです。
あとは最初から入れてあるBootStrapを使って見た目を直すとか、Herokuにアップしてみるとか、
Facebookでの認証も付けてみるとか、色々応用ができると思います。