Node.js
MongoDB
Heroku
Express
ejs

Node.js Express Ejs MongoDB でブログを作成、herokuにディプロイ

この記事について

まだITを習いだしたばかりの学生ですが、夏休みですることもないのでとりあえずの目標としてNodeを勉強してなにか形にしたいと思っていました。
ようやく一応もっともらしい見た目でディプロイすることができたので、自分の理解を確認するためにも手順を書いていきたいと思います。
これから同じようなものに挑戦する人の参考になればうれしいです。
海外の大学で勉強していますので、日本語でコーディングを説明するのに慣れていません。
ご容赦ください。。。

使用したもの

-node(10.4.0)
-express(^4.16.4)
-ejs(^2.6.1)
-mongoose(^5.4.1)

index.js

ブログのルートとなるindex.jsではまずview engineや静的ファイルの保存場所、あと今回使用したbootstrapを参照するためのパスも指定しておきました。

index.js
const express = require("express");
const path = require('path'); //pathはnodeに標準であるモジュールでパスの指定にとても便利です。
const app = express(); //express アプリケーションの作成
const port = process.env.PORT || 8080; //後のherokuではポートが一定しないのでこのようにしておきます

app.set("views", "./src/views"); //この/src/viewフォルダにejsファイルを保存していきます。
app.set("view engine", "ejs"); //view engine を ejs に設定
app.use(express.static(path.join(__dirname, '/public'))); //静的ファイルの保存場所を指定
app.use('/css', express.static(path.join(__dirname, '/node_modules/bootstrap/dist/css')));
app.use('/js', express.static(path.join(__dirname, '/node_modules/bootstrap/dist/js')));
app.use('/js', express.static(path.join(__dirname,'/node_modules/jquery/dist'))); //上記3つはbootstrap用

ルート

index.js
app.get('/', (req, res) => {
        res.render('index',{
            posts,
   }); 
});

app.getの第一引数はパスです。'/'にするとルートになります。
第二引数はリクエストハンドラーでコールバック関数の第一引数reqがクライエントからのリクエスト、resがサーバーからのレスポンスです。
res.renderでは'index'を最初に渡していますが、view engine をすでに指定しているのでviewフォルダ内のindex.ejsをクライエントに渡します。その他渡したい情報はオブジェクトにします。

index.ejs

ファイルすべてを記述すると長くなるのでexpressのrender()で値を渡すうえで重要な部分のみをのせます。
ejsはejsのためのjavaScriptを挿入するタグ<% %>がある以外大してHTMLと区別がないのでわかりやすいと思います。

index.ejs
<%for(let i = 0; i < posts.length; i++){ %>
          <!-- Blog Post -->
          <div class="card mb-4">

            <img class="card-img-top" src="/img/book.jpg" width="700" height="350" alt="Card image cap">
            <div class="card-body">
              <h2 class="card-title"><%=posts[i].title%></h2>
              <p class="card-text"><%-posts[i].postBody%></p>
              <a href="/post/<%=posts[i]._id%>" onclick="readmore()"class="btn btn-primary">Read More &rarr;</a>
            </div>
            <div class="card-footer text-muted">
              Posted by
              <a href="#"><%=posts[i].author%></a>
            </div>
          </div>
          <%}%>

ejsではこのようにforループなども挿入できますので、のちにmongoDBから引っ張ってくるブログポストの数だけブログポストのセクションを繰り返すようにしました。
注意としてejsではjavaScript部分はjavaScriptしか認識しませんので、HTMLを記述する部分へ来れば一度 %> のタグを閉じなければなりません。

postsへ渡される値は以下のようになっています。

index.js
const moment = require('moment');
const mongoose = require('mongoose');
mongoose.connect(process.env.HEROKU_DB_URI, {useNewUrlParser: true});
const db = mongoose.connection;
const Schema = mongoose.Schema;
const blogSchema = new Schema({//このスキーマ
    title: String,
    date: Date,
    author: String,
    postBody: String,
    category: String
});
db.on('error', console.error.bind(console, 'connection error:'));

スキーマはmongoDBを通じてやりとりするドキュメントの雛型みたいなものだと思っています。

MongoDBへの接続

MongoDBへの接続は上記index.jsにもある通りMongooseを使っています。理由としてはただMongooseのページのほうが僕にとってドキュメントがわかりやすかっただけですのでこだわったわけではありません。
herokuあとでherokuにディプロイするのでherokuのアドオンがあるmLabでのアカウント作成をしています。
Mongooseへの接続はmongoose.connect()を使いますが、そこに接続用のURIを渡すだけだとその仕様が廃止予定である旨の注意がコンソールに表示されます。
それを防ぐためには

{useNewUriParser: true}

を第二引数として渡します。

mongoose.connect(process.env.HEROKU_DB_URI, {useNewUrlParser: true});
const db = mongoose.connection;
const Schema = mongoose.Schema;
const blogSchema = new Schema({
    title: String,
    date: Date,
    author: String,
    postBody: String,
    category: String
});
db.on('error', console.error.bind(console, 'connection error:'));

let posts = db.model('posts', blogSchema, 'blogPosts')
            .find({}).sort('-date').exec();

RDBMSに慣れている人にはとっつきにくいかもしれませんが、MongoDBではドキュメントの集まりをCollectionと呼びます。
モデル名、スキーマ、collection名(該当がなければ新規作成)の三つの引数で定義されたモデルをもとにデータベースの操作を行います。
その後、空の検索find({})ですべてのドキュメントを選択、sort('-date')で日付の新しい順でソート、exec()でソートの実行をしています。
検索やソートの条件付けはほかにもいろいろな方法があります。

非同期化

このままexpressによるルートへのレンダリング

app.get('/', (req, res) => {
        res.render('index',{
            posts,
   }); 
});

を行おうとするとデータベースからの取得が間に合わずpostsがundefinedのままindex.ejsへ渡されてしまうので、エラーを吐き出します。
この解決のためにapp.getのコールバック関数を非同期にします。

index.js
app.get('/', (async (req, res, next) => {
    try{
        let posts = await db.model('posts', blogSchema, 'blogPosts')
            .find({}).sort('-date').exec();
        res.render('index',{
            posts
        });
    } catch (err) {
        next(err);
}
}));

これでエラーを吐かなくなります。

herokuへのディプロイ

herokuにnode index.jsを実行させるためにプロジェクトフォルダのルートにprocfileを作成します

Procfile
web: node index.js

次に、mLabの接続用の文字列をハードコードしているのはよくないので、herokuの環境変数に取り込むことで外から隠します。
あとはgit push heroku master ディプロイすれば完成です。

$heroku addons:create mongolab:sandbox

$heroku heroku config:set HEROKU.DB.URI = "mLabの接続用URI"

$git init

&git add . 

$git commit -m "First Commit"

$heroku create

$git push heroku master

$heroku logs --tail

$heroku open

最終的なindex.js

index.js
const express = require("express");
const mongoose = require('mongoose');
const path = require('path'); //pathはnodeに標準であるモジュールでパスの指定にとても便利です。
const app = express(); //express アプリケーションの作成
const port = process.env.PORT || 8080; //後のherokuではポートが一定しないのでこのようにしておきます

app.set("views", "./src/views"); //この/src/viewフォルダにejsファイルを保存していきます。
app.set("view engine", "ejs"); //view engine を ejs に設定
app.use(express.static(path.join(__dirname, '/public'))); //静的ファイルの保存場所を指定
app.use('/css', express.static(path.join(__dirname, '/node_modules/bootstrap/dist/css')));
app.use('/js', express.static(path.join(__dirname, '/node_modules/bootstrap/dist/js')));
app.use('/js', express.static(path.join(__dirname,'/node_modules/jquery/dist'))); //上記3つはbootstrap用

mongoose.connect(process.env.HEROKU_DB_URI, {useNewUrlParser: true});
const db = mongoose.connection;
const Schema = mongoose.Schema;
const blogSchema = new Schema({
    title: String,
    date: Date,
    author: String,
    postBody: String,
    category: String
});
db.on('error', console.error.bind(console, 'connection error:'));

app.get('/', (async (req, res, next) => {
    try{
        let posts = await db.model('posts', blogSchema, 'blogPosts')
            .find({}).sort('-date').exec();
        res.render('index',{
            posts
        });
    } catch (err) {
        next(err);
}
}));

app.listen(port);

あとがき

大学ではコンピュータサイエンス専攻でマイコンとか離散数学とかしかやっていなくてここまでできるのでものすごく時間がかかりました。
しかし不思議なもので完成してみるととても簡単なことだったように感じます。(事実簡単なことなのかもしれませんが)
とにかくこれからはじめるひとの助けになれたらうれしいです。
間違いや指摘があるとこれから始める人への思いが本末転倒になるのでどうぞお気軽にお願いします。