WebDesign Dackel

Vagrant+node.js+express4+MongoDBで簡単なWebアプリを構築

Vagrant+node.js+express4+MongoDBで簡単なWebアプリを構築

Hatena0
Google+0
Pocket0
Feedly0

これまで僕はサーバサイドのJavaScriptをまともに書いたことがありませんでした。普段はPHP+MySQLで作ることがほとんどだからです。
しかし、フロントを実装する際にgulpを使ったり、npmでちょっとしたライブラリを公開している内に、サーバサイドもJavaScriptで書いてみたいなー!となりました。

既に沢山のブログなどで紹介されているような内容かもしれませんが、環境を作るために毎回ググりまくることになりそうだったので、自分の作業ログとして一通りまとめていきたいと思います。

この記事で目指す内容

  • シンプルなブログアプリを構築
  • express4での基本的なCRUDに慣れたい
  • Vagrant内で環境を整えたい
  • ファイルの編集はホスト側から行いたい

Vagrant内で、express.js+MongoDBを使って、カテゴリやタグすら無い、めちゃめちゃシンプルなブログアプリを作っていきます。
基本的なCRUDに慣れて、ルーティングがなんとなく理解出来ればRESTfulAPIの構築なんかもサクッと作れそうです。

express.jsのバージョンは4.xを使っていきます。他でも結構言われていることですが、ネットで情報を探すと3.x4.xの情報が混在しています。僕みたいな初心者にはそれが結構辛かったです。。

そういった意味でもブログに書いておくことで、自分の中で流れを整理しておきたいと思った次第です。

作業環境

  • OS X Yosemite 10.10.05
  • Vagrant 1.7.4
  • VirtualBox 4.3.30

Vagrant, VirtualBoxの導入

Vagrantがまだ入っていない場合は、下記の記事を参考に導入してみると良さそうです!

以降、Vagrantは既に導入済みの前提で進めていきます。

まずはVagrantで環境構築

ターミナルから適当な作業ディレクトリを作成し移動しておきます。

$ mkdir sample-app && cd $_

boxの追加・初期化

A list of base boxes for Vagrant – Vagrantbox.es から、好きなboxファイルを取得します。
今回は、CentOS 6.5 x86_64を選んでみました。

下記のコマンドでは、該当boxcentos65という名前で追加しています。

$ vagrant box add centos65  https://github.com/2creatives/vagrant-centos/releases/download/v6.5.3/centos65-x86_64-20140116.box

ネット環境にもよりますが、これには結構時間がかかるので気長に待ちます。


インストールが終わったら、下記コマンドを入力して追加されていることを確認してみます。

$ vagrant box list
# 色々出てくる
centos65        (virtualbox, 0)

ちゃんとcentos65が追加されています。

プラグインのインストールとVagrantの設定

boxの準備が出来たので、Vagrantの初期化と便利なプラグインのインストールを実行します。

$ vagrant init centos65
$ vagrant plugin install vagrant-hostsupdater

vagrant-hostsupdaterhostsファイルの書き換えを勝手にやってくれるかしこいやつです。

次に生成されたVagrantfileを次の様に変更します。

Vagrant.configure(2) do |config|
  config.vm.box = "centos65"
  config.vm.hostname = "sample-app.local" # `http://sample-app.local` でアクセス出来るように
  config.vm.network "private_network", ip: "192.168.33.10"
  config.vm.synced_folder "./html", "/var/www/html"
end

コメントアウトが沢山あるおかげで最初は長く見えるのですが、今回必要あるものだけ残すとこのくらい短くなりました。

synced_folderの設定では、ホスト(作業しているPC側)のhtmlディレクトリと、ゲスト(Vagrant内)の/var/www/htmlを同期するような設定になります。そうすることで、普段使っているエディタ(AtomSublimeTextなど)でのファイル編集が行えます。(便利ですね!)

同期先である./htmlがまだ無かったので、ディレクトリを作成しVagrantを起動します。

$ mkdir html
$ vagrant up

途中、hostsファイルの書き換えでパスワードを聞かれましたが、パスワードを入力してエンターを押せばそのまま進みます。
無事に起動できたら、Vagrant内にSSHでアクセス出来るようになっています。

$ vagrant ssh
Last login: 日時が出る from 10.0.2.2
$ pwd
/home/vagrant

参考サイト

nodebrewを使ってnode.jsのインストール

まずはnodebrewから

Vagrantの設定が終わったので、主役であるnode.jsのインストールをしていきます。
node.jsのバージョン管理を楽にしたいので、nodebrewを使ったインストールで進めます。

以下の作業はVagrant内で行います。基本的に公式に書いてある通りに進めれば問題ありませんでした。

まずは、nodebrewのインストールです。

$ curl -L git.io/nodebrew | perl - setup

PATHの設定と反映を行います。

$ echo 'export PATH=$HOME/.nodebrew/current/bin:$PATH' >> ~/.bashrc
$ source ~/.bashrc

ちゃんとインストール出来たか確認します。

$ nodebrew help

ずらーとヘルプが出てきたらOKです。

node.jsをインストール

今どんなバージョンのnode.jsが使えるか確認してみます。

$ nodebrew ls-remote
v0.0.1    v0.0.2    v0.0.3    v0.0.4    v0.0.5    v0.0.6    
v0.1.0    v0.1.1    v0.1.2    v0.1.3    v0.1.4    v0.1.5    v0.1.6    v0.1.7
....

沢山バージョンが出てきました。好きなバージョンを選んでインストールします。(今回はv0.12.7を使ってみました)

$ nodebrew install-binary v0.12.7

Install successfulと出たらインストールは終わりです。
しかし、このままではまだ使える状態ではないため、以下のコマンドを実行します。

$ nodebrew use v0.12.7

ここでやっとnode.jsが使用できる状態になりました。

$ node -v
v0.12.7

nginxをインストール

nginxでリバースプロキシさせてnode.jsを動かしたいので、nginxのインストールと設定を行います。

まずはインストール。

$ sudo yum install -y nginx

Complete!が表示されたら、インストールは完了です。
あとはnginxの起動と自動起動の設定を実行します。

$ sudo service nginx start
$ sudo chkconfig nginx on

バーチャルホストを設定

次に、http://sample-app.localをブラウザで叩いた時に、これから作るアプリを動かしたいのでバーチャルホストの設定を行います。

設定ファイルを開きます。

$ sudo vi /etc/nginx/conf.d/virtual.conf

開いたファイルを以下のように変更します。

server {
  listen 80;
  server_name sample-app.local;
  location / {
    proxy_pass http://127.0.0.1:3000;
  }
}

設定を変更したら、nginxを再起動して設定を反映します。

$ sudo service nginx restart

MongoDBのインストール

今回初めて使うのですが、JavaScriptとの親和性が高くnode.jsを使ったアプリではよく使われるデータベースのようです。

インストールを行う前に、リポジトリの追加をする必要があるので、まずはそちらから。

$ sudo vi /etc/yum.repos.d/10gen.repo

10gen.repoには下記を記載します。

[10gen]
name=10gen Repository
baseurl=http://downloads-distro.mongodb.org/repo/redhat/os/x86_64
gpgcheck=0
enabled=1

リポジトリの追加が終わったら、早速インストールの実行です。(これも時間がかかります)

$ sudo yum install -y mongo-10gen mongo-10gen-server

Complete!と表示されたらインストールは完了です。
あとは起動と自動起動の設定です。

$ sudo service mongod start
$ sudo chkconfig mongod on

参考サイト

特に「さくらVPSを〜」の記事はめちゃめちゃ参考になりました!(nodebrewMongoDBらへんは、ほとんどそのまんまです…)

ここまでの作業をbox化しておき、次回からの開発をスムーズに

ちょっと寄り道です。本筋には関係無いので、必要無い方は読み飛ばしてしまって問題ありません。

ここまで、nodebrewnode.jsMongoDBのインストールを行いました。同じような構成でWebアプリを作る時に、また一からやり直すのは少し面倒です。
そこで、この状態をboxファイルとして残しておきたいと思います。

まずはVagrantから抜けて、Vagrantを停止します。

$ exit;
logout
$ vagrant halt

次にpackageコマンドの実行を行います。

$ vagrant package

これで、package.boxが生成されました。このboxファイルをVagrantへ登録します。

$ vagrant box add node-mongodb package.box

上記コマンドのnode-mongodbboxの登録名です。
これで、次回別のアプリとして作業を開始する時に、以下のようにして始めることができます。

$ vagrant init node-mongodb

Vagrantあんまり使ったことなかったですが、めっちゃ便利ですねー!

でも、これだけだと全部自動!っていう訳ではないため、Vagrantfileの書き換え、htmlディレクトリの作成まではやっておく必要があります。

expressの環境構築

やっとサーバ周りの準備が終わりました。
早速expresssを使ってアプリを書いていきたいのですが、今回は一から書くのでは無く、雛形のコード生成してそれをベースに進めていきます。

express-generatorで雛形の生成

まずは、作業を行うディレクトリに移動します。(Vagrantsync_folderで設定したパス)

$ cd /var/www/html

ここで、雛形生成用のモジュールをインストールします。

$ npm i express-generator

以下のコマンドで、現在いるディレクトリに雛形を生成します。
テンプレートエンジンがデフォルトでJadeですが、今回はEJSを使いたいと思います。

$ ./node_modules/.bin/express -e -f .

これで、下記のようなファイル群が生成されました。

.
├── app.js
├── bin
│   └── www
├── package.json
├── public
│   ├── images
│   ├── javascripts
│   └── stylesheets
│       └── style.css
├── routes
│   ├── index.js
│   └── users.js
└── views
    ├── error.ejs
    └── index.ejs

express-generatorはもう必要ないので削除して、必要な各種モジュールをインストールします。

$ npm un express-generator
$ npm i

インストールが終わったら、以下を実行してhttp://sample-app.localを開いてみます。

$ npm start

expressのwelcome画面

ちゃんと動いてそうです!

コードの編集後、プロセスを自動で再起動させる

app.jsとか、routes関連のコードを編集するとプロセスの再起動が必要になります。
ただ、編集したらコマンド打って再起動、編集したら…っていうのは面倒です。

そのため、nodemonsupervisorといったモジュールを使うのが一般的みたいです。
nodemonだと、Vagrantでホストのファイルを編集しても反映されなかったので、今回はsupervisorを使ってみます。

以下、supervisorをインストールします。

$ npm i supervisor

package.jsonscriptsを編集します。

{
  "name": "html",
  "version": "0.0.0",
  "private": true,
  "scripts": {
    "start": "supervisor -i views,public ./bin/www"
  },
  "dependencies": {
    "body-parser": "~1.13.2",
    "cookie-parser": "~1.3.5",
    "debug": "~2.2.0",
    "ejs": "~2.3.3",
    "express": "~4.13.1",
    "morgan": "~1.6.1",
    "serve-favicon": "~2.3.0"
  }
}

node ./bin/wwwから、supervisor -i views,public ./bin/wwwに書き換えただけです。
ビューに関するファイルの変更では再起動の必要はないようなので、-iオプションで対象外としてみました。

これで、プロセスの再起動を自動化できましたー!

参考サイト

ブログアプリの実装

やっと開発に入れます…!! 必要なモジュールをインストールしたら、実際にコードを書いていきます。

必要なモジュールのインストール

もしサーバを起動したままの場合は、Ctl+Cで止めておきます。

MongoDBO/R Mapperっぽく扱えるmongooseCSRF対策にcsurfなど、必要なモジュールを予めインストールしておきます。

$ npm i -S mongoose csurf express-session method-override connect-flash

インストールしたモジュールは以下のとおりです。

それぞれインストールが終わったら、またサーバを起動しておきます。

$ npm start

あとはホスト側のFinderなどで、ファイルの追加・編集など行っていきます。
楽ちんでいいですね。

app.jsの編集

以下の編集を行います。

  • データベースの接続
  • CSRF対策
  • method-overrideの設定

app.jsはこんな感じになりました。(一部省略しています)

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 methodOverride = require("method-override");
var session = require("express-session");
var csurf = require("csurf");
var flash = require("connect-flash");
var mongoose = require("mongoose");

var routes = require('./routes/index');
var users = require('./routes/users');

var app = express();

// データベースを接続
mongoose.connect("mongodb://localhost/blog");

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');

// 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')));

// HTTP METHOD を上書き
// https://github.com/expressjs/method-override#custom-logic
app.use(methodOverride(function(req, res){
  if( req.body && typeof req.body === "object" && "_method" in req.body ){
    var method = req.body._method;
    delete req.body._method;
    return method;
  }
}));

// SessionとCSRF、flashメッセージの設定
app.use(session({
  secret: "keyboard cat",
  resave: false,
  saveUninitialized: false,
}));
app.use(csurf());
app.use(flash());


app.use('/', routes);
app.use('/users', users);

// ...省略

module.exports = app;

モデルの作成

ブログ記事用のモデルを作成します。

インストールディレクトリに、modelsディレクトリを作ってその中にPost.jsを作ります。
Post.jsの中身は以下のとおり。

var mongoose = require("mongoose");
var Schema = mongoose.Schema;

var PostSchema = new Schema({
  title: {type: String, required: "タイトルは必須です"},
  contents: {type: String, default: ""},
  created: {type: Date, default: Date.now},
  modified: {type: Date, default: Date.now}
});


module.exports = mongoose.model("Post", PostSchema);

記事のタイトル(title)を必須、記事の内容を(contents)のデフォルト値を空文字に。
あとは作成・更新日時だけのシンプルな構造にしてみました。

MongoDBはスキーマレスらしいですが、mongooseを使うことで型の指定や、必須、バリデーションにデフォルト値の設定など、色々なことが出来るみたいです。

コントローラー(ルーティング)を設定

URLViewを紐付け、Postモデルを使ったデータ操作を実装します。

expressの基本的なルーティングは公式のドキュメントを見るのが間違いないと思います。

./routes/index.jsを以下の様に変更します。

var express = require('express');
var router = express.Router();
var Post = require("../models/Post");


// 記事一覧 (./views/index.ejsを表示)
router.get("/", function(req, res, next) {
  Post.find({}, function(err, posts){
    res.render("index", {
      title: "SampleApp",
      csrf: req.csrfToken(),
      posts: posts
    });
  });
});


// 記事の追加 (./views/new.ejsを表示)
router.get("/new", function(req, res, next) {
  res.render("new", {
    title: "記事の追加 | SampleApp",
    csrf: req.csrfToken(),
    errors: req.flash("errors").shift() //必ず配列が帰ってくるので、一番目の値を取得
  });
});

router.post("/create", function(req, res, next) {
  var post = new Post();
  post.title = req.body.title;
  post.contents = req.body.contents;
  post.save(function(err){
    // エラーがあれば、メッセージを残して追加画面に
    if( err ){
      req.flash("errors", err.errors);
      res.redirect("/new");

    // エラーが無ければ一覧に
    }else{
      res.redirect("/");
    }
  });
});


// 記事の編集 (./views/edit.ejsを表示)
router.get("/edit/:id", function(req, res, next){
  // 適当なIDを指定して、該当する記事が見つからない場合は処理をスキップします
  Post.findById(req.params.id, function(err, post){
    if( err ) return next();
    res.render("edit", {
      title: "記事の編集 | SampleApp",
      csrf: req.csrfToken(),
      post: post,
      errors: req.flash("errors").shift()
    });
  });
});

router.put("/update", function(req, res, next){
  Post.findById(req.body._id, function(err, post){
    if( err ) return next();
    post.title = req.body.title;
    post.contents = req.body.contents;
    post.save(function(err){
      if( err ){
        req.flash("errors", err.errors);
        res.redirect("/edit/" + req.body._id);
      }else{
        res.redirect("/");
      }
    });
  });
});


// 記事の削除
router.delete("/destroy", function(req, res, next){
  Post.findById(req.body._id, function(err, post){
    if( err ) return next();
    post.remove();
    res.redirect("/");
  });
});


module.exports = router;

コメントにも書いていますが、簡単な入力値の検証と、エラーメッセージを渡すところまでやってみました。
ルーティングと対応するViewもコメントに書いています。

今回は./routes/index.jsに記事に関するルーティングを設定してしまいましたが、多分./routes/posts.jsを作ってその中で設定していく方が良いかなと思います。

ビューを作成

最後は画面表示のためのViewを作ります。

ヘッダ・フッタは共通化していきたいので、まずはそこから。

./views/の中にpartialsディレクトリを作ります。
更にその中に

  • header.ejs
  • footer.ejs

を作ります。

それぞれの中身は次のような感じです。

header.ejs

<!DOCTYPE html>
<html>
  <head>
    <title><%= title %></title>
    <link rel='stylesheet' href='/stylesheets/style.css' />
  </head>
  <body>

footer.ejs

</body>
</html>

共通部分はこんな感じで良しとします。
では次に、ページの中身を作っていきます。

記事一覧 (./views/index.ejs)

記事があるようならpostsの中に配列が渡ってくるので、それをforEachで回して表示します。
削除する際は、一応確認のダイアログを出してみました。

あと、削除を実行する際にPOSTを使ったフォームの送信を行うので、CSRF対策用の変数を埋め込んでおきましょう。

<% include partials/header %>

<h1><%= title %></h1>
<ul>
  <% posts.forEach(function(post){ %>
    <li>
      <form action="/destroy" method="post">
        <input type="hidden" name="_method" value="delete">
        <input type="hidden" name="_csrf" value="<%= csrf %>">
        <input type="hidden" name="_id" value="<%= post._id %>">
        <a href="/edit/<%= post._id %>"><%= post.title %></a>
        <span>Created: <%= post.created.toDateString() %></span>
        <button type="submit" onclick="return confirm('削除してもよろしいでしょうか?');">削除</button>
      </form>
    </li>
  <% }) %>
</ul>

<p><a href="/new">記事の追加</a></p>

<% include partials/footer %>

上記では、<input type="hidden" name="_method" value="delete">で、HTTP METHODを上書きして、DELETEとして解釈してもらえるように設定しています。

記事作成フォーム (./views/new.ejs)

<% include partials/header %>

<h1><%= title %></h1>

<form action="/create" method="post">
  <input type="hidden" name="_csrf" value="<%= csrf %>">
  <p>
    <input type="text" name="title" value="" size="60">
    <% if( errors && errors.title ){ %>
      <strong><%= errors.title.message %></strong>
    <% } %>
  </p>
  <p><textarea name="contents" cols="60" rows="12"></textarea></p>
  <p><button type="submit">作成</button></p>
</form>
<p><a href="/">一覧に戻る</a></p>

<% include partials/footer %>

エラーの表示と、CSRF以外は普通のフォームです。

記事編集フォーム (./views/edit.ejs)

記事の作成とほとんど同じですが、postの中に編集する記事データが入ってきます。それを使って初期値の設定を行っている点が異なります。

それと、削除の時と同じ要領で_methodの指定をputにしています。

<% include partials/header %>

<h1><%= title %></h1>

<form action="/update" method="post">
  <input type="hidden" name="_method" value="put">
  <input type="hidden" name="_csrf" value="<%= csrf %>">
  <input type="hidden" name="_id" value="<%= post._id %>">
  <p>
    <input type="text" name="title" value="<%= post.title %>" size="60">
    <% if( errors && errors.title ){ %>
      <strong><%= errors.title.message %></strong>
    <% } %>
  </p>
  <p><textarea name="contents" cols="60" rows="12"><%= post.contents %></textarea></p>
  <p><button type="submit">作成</button></p>
</form>
<p><a href="/">一覧に戻る</a></p>

<% include partials/footer %>

記事の_idを渡しているところも新規作成と違いますね!

viewsディレクトリの中身

ここまでで、./views/の中身はこんな感じになりました。

views
├── edit.ejs
├── error.ejs
├── index.ejs
├── new.ejs
└── partials
    ├── footer.ejs
    └── header.ejs

実際に動かしてみる

これで一通り動作するようになりました。

http://sample-app.localをブラウザで開いて、動作を確認していきます。

記事の追加

記事作成の動作確認

ちゃんと記事が追加されました。
空のタイトルで投稿しようとするとエラーを表示しています。

記事の編集

記事編集の動作確認

内容が更新されていて、編集時も入力値の検証も出来てます。

記事の削除

記事削除の動作確認

削除ボタンの選択でダイアログが表示されて、「OK」を押した時だけ削除されています。

参考サイト

まとめ

環境の構築から、実際に簡単なアプリを作るところまで一つの記事にしてしまったため長くなってしまいました。。
詳細な部分で説明不足な部分は、参考にさせていただいたサイトでご確認いただけたらと思います!

今回は、冒頭で書いたようにタグやカテゴリ管理すら無い、凄くシンプルなブログアプリでした。ただ、なんとなく流れはつかめた?気がするので、これをベースにちょこちょこ手を入れていってみたいと思います。

これからの課題など

  • expressで動かすコードをES6CoffeeScriptで書きたい
  • gulpでフロントのビルド、バックエンドの自動起動とかしたい
  • 本番環境で動かす時にきをつけるところなんかを知りたい
  • テストどうやって書くの?
  • RESTfulJSON返すAPIを書きたい
  • Reactのサーバサイドレンダリングとかしてみたい

始めたてで分からないことばかりです。。
一つ一つ出来るように勉強していかないとですね。