Node.jsを用いた簡単なアプリケーションの実装

はじめに

Node.jsを使った簡単なアプリケーションの作成方法を紹介します。

今回はExpress.jsという軽量なフレームワークを使用しますが、普段RailsやDjangoを使っている方だと自由すぎてどこから手を出せばよいかわからなくなってしまいがちです。

そのため実際にコードを書きながら、Express.jsを使って開発用サーバーの立ち上げテンプレートエンジンを使った動的なコンテンツの表示Formからのデータ追加の機能実装を行います。

対象読者

この記事は基本的なJavaScriptの文法を理解できている方を対象にしています。

何らかのフレームワーク(Rails, Django, Laravelなど)でWebアプリケーション開発を行ったことがあると更に理解がしやすいと思います。

目次

環境

  • macOS Catarina 10.15.3
  • Node.js v10.18.0
  • npm 6.13.7
  • express 4.16.3
  • ejs 2.6.1

express.jsとは

express.js(以下express)とはWebアプリケーションとモバイル・アプリケーション向けの一連の堅固な機能を提供する最小限で柔軟なNode.js Web アプリケーション・フレームワークです。

Express.js: the fast, unopinionated, minimalist web framework for node
https://github.com/expressjs

あくまで最低限の機能なので、PythonのDjangoに搭載されているような認証機能や管理画面などは有りません。
しかしその分自由に実装できます。

使用するにはインストールが必要です。

npm install --save express

expressを使用するとほんの数行のコードでアプリケーションサーバーを立ち上げることができます。

const express = require("express");

const app = express();

app.use((req, res, next) => {
  res.send('<h1>This is Express.js</h1>')
})

app.listen(3000);

この処理ではブラウザに<h1>This is Express.js</h1>を送信し、localhostの3000番ポートにサーバーを立てています。
http://localhost:3000/に表示されていれば無事アプリが起動しています。

Template Engineの導入と動的コンテンツの表示

先程はres.sendを使ってHTMLを直接送っていましたが、普通のWeb開発の際にはHTMLファイルを用意してファイルごとブラウザに表示します。

express.jsではTemplate Engineという動的にデータを受け取り、HTMLファイルに送信することが出来る機能が搭載されています。

Template Engineにはさまざまな種類がありますが、今回はHTMLに似ており導入コストの低いejsを使用します。

npm install --save ejs

app.jsを以下のように書き換えます。

// app.js
const path = require("path");

const express = require("express");

const app = express();
const router = express.Router();

// Template Engineの定義
app.set("view engine", "ejs");
// HTMLファイルのフォルダを指定
app.set("views", "views");

app.use(express.urlencoded({ extended: false }));

// css等の静的ファイルのファルダを指定
app.use(express.static(path.join(__dirname, "public")));

app.use(
  router.get("/", (req, res, next) => {
    // views/index.ejsをレンダリングし、データを渡す
    res.render("index", { title: "Book Reviews" });
  })
);

app.listen(3000);

指定したtemplateとcssも記述します。
ejsでは<% %>というタグを利用してレンダリング時に渡されたデータを受け取ることができます。

<!-- views/index.ejs -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Book Reviews</title>
    <link rel="stylesheet" href="/css/style.css">
</head>
<body>
      <!-- titleで渡されたデータを受け取る -->
    <h1><%= title %></h1>
    <h2>Add Book Infomation</h2>
</body>
</html>
/* public/css/style.css */
h1,
h2 {
  text-align: center;
}

Book Reviewsが表示されていれば問題なく実装できています。

first

ここからもう少し機能を追加していきます。

データの書き込みと読み込み

データの追加追加されたデータの読み込む機能を実装します。

HTMLにFormを作成しデータを追加できるようにして、追加されたデータをHTMLで表示します。

本来データはMySQLやMongoDBのようなデータベースを用意して保存することが普通ですが、今回はあくまで入門なのでローカルにjson形式でファイルを保存することにします。

const path = require("path");
const fs = require("fs");

const express = require("express");

const app = express();
const router = express.Router();

app.set("view engine", "ejs");
app.set("views", "views");

app.use(express.urlencoded({ extended: false }));
app.use(express.static(path.join(__dirname, "public")));

app.use(
  // データの読み込む処理
  // データの受け取りはGET
  router.get("/", (req, res, next) => {
    // カレントディレクトリのdata.jsonを読み込む
    fs.readFile(path.join(__dirname, "data.json"), (err, fileContent) => {
      books = JSON.parse(fileContent);
      res.render("index", { title: "Book Reviews", books: books });
    });
  }),
  // データの書き込み
  // データの送信はPOST
  router.post("/", (req, res, next) => {
    // フォームに書かれたデータを受け取る
    const title = req.body.title;
    const author = req.body.author;
        const books = { title: title, author: author }
    // カレントディレクトリにdata.jsonとして保存する
    fs.writeFile(path.join(__dirname, "data.json"), JSON.stringify(books), err => {
      if (err) {
        console.log(err);
      }
      // 送信後は'/'へリダイレクトする
      res.redirect("/");
    });
  })
);

app.listen(3000);
<!-- views/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Book Reviews</title>
    <link rel="stylesheet" href="/css/style.css">
</head>
<body>
    <h1><%= title %></h1>
    <div class="book-form">
        <h2>Add Book Infomation</h2>
        <form action="/" method="POST" autocomplete="off">
            <div class="form-control">
                <label for="title">Title</label>
                <input type="text" name="title">
            </div>
            <div class="form-control">
                <label for="text">Author</label>
                <input type="text" name="author">
            </div>
            <button class="btn" type="submit">Submit</button>
        </form>
    </div>
    <div class="book-list">
        <h2>Book List</h2>
        <article>
              <!-- booksで受け取ったデータをfor文で回す-->
              <!-- booksに複数のデータがある場合、データがすべて読み込むまで繰り返し表示する -->
            <% for (let book of books) { %>
                <h3><%= book.title %></h3>
                <p><%= book.author %> </p>
            <% } %> 
        </article>
    </div>
</body>
</html>

体裁を整えるためにcssも追加します。

h1,
h2 {
  text-align: center;
}

.book-form {
  width: 20rem;
  max-width: 90%;
  margin: auto;
  display: block;
}

.form-control {
  margin: 1rem 0;
}

.form-control label,
.form-control input,
.form-control textarea {
  display: block;
  width: 100%;
  margin-bottom: 0.25rem;
}

.form-control input,
.form-control textarea {
  border: 1px solid #a1a1a1;
  font: inherit;
  border-radius: 2px;
}

.form-control input:focus,
.form-control textarea:focus {
  outline-color: #00695c;
}

.btn {
  display: inline-block;
  padding: 0.25rem 1rem;
  text-decoration: none;
  font: inherit;
  border: 1px solid #00695c;
  color: #00695c;
  background: white;
  border-radius: 3px;
  cursor: pointer;
}

.btn:hover,
.btn:active {
  background-color: #00695c;
  color: white;
}

.book-list {
  text-align: center;
}

これでTemplateのFormを使ってデータを書き込みが出来るようになりました。

試しにtitleHTML/CSSモダンコーディングauthor吉田真麻と記載してみます。
するとdata.jsonに以下のように記載され、画面に表示されることが確認できると思います。

[{"title":"HTML/CSSモダンコーディング","author":"吉田真麻"}]
second

しかしこのままでは1つ問題があります。

バグの修正:データの追加

例えば、新しくFormへtitleNode.js中級者を目指すauthorこのすみと追加してみます。
すると画面には新しく追加したデータのみが追加され、data.jsonが新しいものに書き換わってしまっています。

[{"title":"Node.js中級者を目指す","author":"このすみ"}]

普通であれば古いデータを残したまま、新しいデータを追加していきたいです。
データの追加ができるように修正します。

Formに書き込んだデータを新しいデータとして追加するためには、書き込む前に一度データを読み込んだ後にデータを追加する処理を入れる必要があります。

const path = require("path");
const fs = require("fs");

const express = require("express");

const app = express();
const router = express.Router();

app.set("view engine", "ejs");
app.set("views", "views");

app.use(express.urlencoded({ extended: false }));
app.use(express.static(path.join(__dirname, "public")));

app.use(
  router.get("/", (req, res, next) => {
    fs.readFile(path.join(__dirname, "data.json"), (err, fileContent) => {
      books = JSON.parse(fileContent);
      res.render("index", { title: "Book Reviews" });
    });
  }),
  router.post("/", (req, res, next) => {
    const title = req.body.title;
    const author = req.body.author;
    // データがある場合は一度読み込み、受け取ったデータを上書きする
    fs.readFile(path.join(__dirname, "data.json"), (err, fileContent) => {
      if (!err) {
        let books = JSON.parse(fileContent);
        // 追加のデータを読み込んだデータに追加する
        books.push({ title: title, author: author });
        // データをファイルに書き込み
        fs.writeFile(path.join(__dirname, "data.json"), JSON.stringify(books), err => {
          if (err) {
            console.log(err);
          }
          res.redirect("/");
        });
      }
    });
  })
);

app.listen(4000);

先程は単にデータを書き込んでいましたが、ここではデータのある場合は一度データを読み込んでリストとして保持し、そこに新しいデータをpushで追加しています。

これでデータが2つになったので、for文で順に表示されます。

data

コラム:非同期通信とコールバックについて

最後にnodejsにおいて重要な処理について触れておきたいと思います。
先程のコードを少し変更してみます。

...
// ネストさせずに処理を書いた場合
router.post("/", (req, res, next) => {
    const title = req.body.title;
    const author = req.body.author;
      // ①
    fs.readFile(path.join(__dirname, "data.json"), (err, fileContent) => {
      if (!err) {
        let books = JSON.parse(fileContent);
        books.push({ title: title, author: author });
      }
    });
      // ここをネストしないで次の処理として書く
    // データの書き込み
      // ②
    fs.writeFile(path.join(__dirname, "data.json"), JSON.stringify(books), err => {
      if (err) {
        console.log(err);
      }
      res.redirect("/");
    });
  })
...

このようにすると以下のようなエラーが表示されます。

ReferenceError: bookArray is not defined
    at app.use.router.post (/Users/daichi/Project/application/book-reviews/app.js:33:68)
    ...

なぜこのようになったのか分かるでしょうか。

実はNodejsでは非同期処理が行われる場合があります。
非同期処理では、処理が終わる前にJavaScript側で内部的に処理を行わせて、その処理が終わることを待たずして次の処理が行われます。

①と②のファイルの書き込みと読み込みは非同期処理です。

// ①
fs.readFile(path.join(__dirname, "data.json"), (err, fileContent) => {
  if (!err) {
    let books = JSON.parse(fileContent);
    books.push({ title: title, author: author });
  }
});
// ②
fs.writeFile(path.join(__dirname, "data.json"), JSON.stringify(books), err => {
  if (err) {
    console.log(err);
  }
  res.redirect("/");
});

つまりdata.jsonの読み込む処理が終わる前に、次の処理のデータを書き込む処理が始まってしまいます。
その結果、booksがまだ定義されていないのでエラーになってしまうというわけです。

これを修正するには、①の処理が終わった後に②の処理が始まるようにする必要があります。
その方法として一番シンプルな方法はコール、つまり関数の中に関数を定義して処理を行うことです。
なのでネストさせている訳です。

ただし、何度もコールバックするとネストがどんどん深くなって可読性が低くなってしまいます。
いわゆるコールバック地獄と呼ばれるものです。

それを解消するには様々な方法がありますが(Generater, Promise等々)、今回は基本的な考え方だけ示したかったため割愛します。

このようにJavaScriptでは非同期通信を順番に呼び出すための工夫が必要になります。
他の言語にはあまりない特性ですが、node.jsでは知っておくと実装の際に役に立ちます。

まとめ

今回はExpress.jsを使って開発用サーバーの立ち上げ、テンプレートエンジンを使った動的なコンテンツの表示、Formからのデータ追加の機能を実装してみました。

盛りだくさんでしたが、自由にコードが書ける分、設計やどうすればバグが修正できるか考えられるので勉強になるかと思います。