Node.js Web アプリケーションを Docker で構築してみる

## はじめに
以下の本を使って学習したので、その覚書です。
初学者には、とても分かりやすかったです。
<a href="https://www.amazon.co.jp/JavaScript%E3%82%A8%E3%83%B3%E3%82%B8%E3%83%8B%E3%82%A2%E3%81%AE%E3%81%9F%E3%82%81%E3%81%AENode-js%E5%85%A5%E9%96%80-%E5%A4%A9%E7%94%B0%E5%A3%AB%E9%83%8E-ebook/dp/B01N2Y16BQ?__mk_ja_JP=%E3%82%AB%E3%82%BF%E3%82%AB%E3%83%8A&crid=L29WW63FIQQX&keywords=node.js&qid=1657983789&s=books&sprefix=node.js%2Cstripbooks%2C223&sr=1-15&linkCode=li2&tag=cocolonifty00-22&linkId=0b63be6a46b7d926dc4f8a3638328820&language=ja_JP&ref_=as_li_ss_il" target="_blank"><img border="0" src="//ws-fe.amazon-adsystem.com/widgets/q?_encoding=UTF8&ASIN=B01N2Y16BQ&Format=_SL160_&ID=AsinImage&MarketPlace=JP&ServiceVersion=20070822&WS=1&tag=cocolonifty00-22&language=ja_JP" ></a><img src="https://ir-jp.amazon-adsystem.com/e/ir?t=cocolonifty00-22&language=ja_JP&l=li2&o=9&a=B01N2Y16BQ" width="1" height="1" border="0" alt="" style="border:none !important; margin:0px !important;" />

学習環境としてDockerを使っていますので設定ファイルの記録として残していますが、Docker化が目的ではないので詳細な説明は割愛します。

## まずは基本的な使い方
```:構成
.
├── Dockerfile
├── docker-compose.yml
└── index.js
```

```Dockerfile:Dockerfile
FROM node:latest
WORKDIR /project-name
```
```yaml:docker-compose.yml
version: "3"
services:
  web:
    build: .
    volumes:
      - .:/project-name
    command: node index.js
    ports:             #-p ポートフォワーディング
      - 8000:1234
    tty: true          #-t ttyを割り当てます。
    stdin_open: true   #-i STDINを開きます。
```
`write`メソッドはhtmlコンテンツを書き出す、`end`はhtmlコンテンツ出力を完了するものです。
```js:index.js
var http = require('http');      // httpサーバなどを扱う

var server = http.createServer(function(req, res) {
    res.writeHead(200, {'Content-Type': 'text/plain'});
    res.write('Hello World');
    res.end();
});
server.listen(1234);
console.log('サーバを起動しました');
```
ファイルが格納されているディレクトリに移動し、docker-composeを実行します。
```bash
$ docker-compose up --build -d
```
ブラウザでlocalhost:8000へアクセスします。
![スクリーンショット 2022-07-17 0.23.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/380300/10526690-e7f0-a934-303a-f083ed155494.png)

index.jsの数行の記述だけで、Webサーバとアプリが構築できました。

## テンプレートファイルを使用する
node.jsにwriteメソッドでHTMLを指定するのが現実的ではないためhtmlファイルを使います。

```:構成
.
├── Dockerfile          　//　変更なし
├── docker-compose.yml 　//　変更なし
├── index.js
└── temp.html
```
```temp.html
<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
	<title>sample</title>
</head>
<body>
	<h1>title</h1>
	<p>hogehoge</p>
</body>
</html>
```
`readFile`は、第一引数から対象ファイルパス、エンコード、読み込み完了時のコールバック関数です。
```js:index.js
var http = require('http');      // httpサーバなどを扱う
var fs = require('fs');          // ファイルを扱う

var server = http.createServer(function(req, res) {
    fs.readFile('./temp.html', 'utf-8', function(err, data) {
        res.writeHead(200, {'Content-Type': 'text/html'});
        res.write(data);
        res.end();
    });
});
server.listen(1234);
console.log('サーバを起動しました');
```
起動済みのコンテナを一度落としてから再度docker-composeを実行します。
```bash
$ docker-compose down
$ docker-compose up --build -d
```
ブラウザでlocalhost:8000へアクセスします。
![スクリーンショット 2022-07-17 0.38.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/380300/21c39af6-c9d9-dc31-9081-239fa2fb99ac.png)

## 複数のhtmlを利用する

```:構成
.
├── Dockerfile          　//　変更なし
├── docker-compose.yml 　//　変更なし
├── index.js
├── index.html
└── next.html
```
```html:index.html
<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
	<title>index</title>
</head>
<body>
	<h1>最初のページ</h1>
	<a href="/next">次のページへ</a>
</body>
</html>
```
```html:next.html
<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
	<title>index</title>
</head>
<body>
	<h1>最初のページ</h1>
	<a href="/next">次のページへ</a>
</body>
</html>
```
```js:index.js
var http = require('http');      // httpサーバなどを扱う
var fs = require('fs');          // ファイルを扱う

var server = http.createServer(function(req, res) {
    var target = '';
    switch(req.url) {
        case '/':
        case '/index':
            target = './index.html';
            break;
        case '/next':
            target = './next.html';
            break;
        default:
            res.writeHead(404, {'Content-Type': 'text/plain'});
            res.end('Bad request');
            return;
    }

    fs.readFile(target, 'utf-8', function(err, data) {
        res.writeHead(200, {'Content-Type': 'text/html'});
        res.write(data);
        res.end();
    });
});
server.listen(1234);
console.log('サーバを起動しました');

```
起動済みのコンテナを一度落としてから再度docker-composeを実行します。
```bash
$ docker-compose down
$ docker-compose up --build -d
```
ブラウザでlocalhost:8000へアクセスします。
![スクリーンショット 2022-07-17 1.02.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/380300/e466d786-296e-0b4b-9e5b-813f71f90f35.png) ![スクリーンショット 2022-07-17 1.02.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/380300/b6a26619-47c1-a8e1-d07d-43932cf3be8e.png) ![スクリーンショット 2022-07-17 1.03.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/380300/5816fe56-c378-7812-e751-b0cc24f7d513.png)

## 送信データを処理する

```:構成
.
├── Dockerfile          　//　変更なし
├── docker-compose.yaml 　//　変更なし
├── index.js
└── index.html
```
```html:index.html
<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
	<title>index</title>
</head>
<body>
	<h1>送信データ</h1>
	<a href="/?name=hoge&age=20">GETメソッド</a>
    <form action="/" method="POST">
        <input type="text" name="name" value="fuga">
        <input type="text" name="age" value="30">
        <button type="submit">POSTメソッド</button>
    </form>
</body>
</html>
```
`fs.readFileSync`は、同期的にファイルを読み込みます。これまで、リクエストがあるたびにファイル読み込みが発生していましたが、この書き方にするとサーバ起動前に1度ファイルを読み込んで、あとはそのオブジェクトを使い回すことができます。
GETメソッドは、`url.parse`を使い、urlのクエリ文字を取得します。第二引数を`true`にすることで?以降のクエリ文字をオブジェクトとして扱えます。`false`だと`name=hoge&age=20`のまま保持します。
POSTメソッドの場合はhttpリクエストオブジェクト（データ受信時のdataイベントと受信完了時のendイベント）に対してハンドラを登録して扱います。
```js:index.js
var http = require('http');      // httpサーバなどを扱う
var fs = require('fs');          // ファイルを扱う
var url = require('url');        // urlの文字列を解析
var qs = require('querystring'); // クエリ文字列を扱う

var indexPage = fs.readFileSync('./index.html', 'utf-8');

var server = http.createServer(function(req, res) {
    if(req.method == 'GET') {
        var urlParts = url.parse(req.url, true);
        console.log('----- GET Request ------');
        console.log('name: ' + urlParts.query.name);
        console.log('age: ' + urlParts.query.age);
    } else if(req.method == 'POST') {
        var body = '';
        req.on('data', function(data) {
            body += data;
        });
        req.on('end', function() {
            var params = qs.parse(body);
            console.log('----- POST Request ------');
            console.log('name: ' + params.name);
            console.log('age: ' + params.age);

        });
    }

    res.writeHead(200, {'Content-Type': 'text/html'});
    res.write(indexPage);
    res.end();
});
server.listen(1234);
console.log('サーバを起動しました');
```
起動済みのコンテナを一度落としてから再度docker-composeを実行します。
また、コンソール出力を確認するためログを出力させます。
```bash
$ docker-compose down
$ docker-compose up --build -d
$ docker-compose logs -f
```
ブラウザでlocalhost:8000へアクセスします。
![スクリーンショット 2022-07-18 13.39.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/380300/e77ef7b8-9a84-072d-1394-e00443e948af.png)　![スクリーンショット 2022-07-18 13.41.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/380300/07b6b591-e39a-a155-6961-0324c5f2a486.png)

## 静的ファイルの供給

```:構成
.
├── Dockerfile          　//　変更なし
├── docker-compose.yml 　//　変更なし
├── index.js
└── animal.jpg
```
```js:index.js
var http = require('http');      // httpサーバなどを扱う
var fs = require('fs');          // ファイルを扱う
var url = require('url');        // urlの文字列を解析

var server = http.createServer(function(req, res) {
    var urlParts = url.parse(req.url, true);
    var path = __dirname + '/' + urlParts.pathname;
    var stream = fs.createReadStream(path);
    stream.pipe(res);

});
server.listen(1234);
console.log('サーバを起動しました');
```
`stream.pipe`は、イベントを登録することなく、読み込んだデータを引数で指定したオブジェクトにそのまま流し込み、endも呼び出します。


起動済みのコンテナを一度落としてから再度docker-composeを実行します。
```bash
$ docker-compose down
$ docker-compose up --build -d
```
ブラウザでlocalhost:8000/animal.jpgへアクセスします。
![スクリーンショット 2022-07-18 16.33.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/380300/6e41ba25-346c-22b5-7e17-e6e8b2ef3795.png)

## ファンクションを使う

```:構成
.
├── Dockerfile          　//　変更なし
├── docker-compose.yml 　//　変更なし
├── func.js
└── index.js
```
exportsオブジェクトでインクルード先に利用させる機能を設定します。
```js:func.js
exports.add = function(val1, val2) {
	return val1 - val2;
}
exports.sub = function(val1, val2) {
	return val1 - val2;
}
```
require関数で`func.js`をインクルードします。拡張子は不要です。
```js:index.js
var func = require('./func');
console.log(func.add(1, 2));
console.log(func.sub(10, 3));
```
起動済みのコンテナを一度落としてから再度docker-composeを実行します。
また、コンソール出力を確認するためログを出力させます。
```bash
$ docker-compose down
$ docker-compose up --build -d
$ docker-compose logs -f
```
![スクリーンショット 2022-07-18 17.04.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/380300/215e82a5-2d19-aa26-e1ec-d585fc9c6c6b.png)

## EJSを使う

```:構成
.
├── Dockerfile          　//　変更なし
├── docker-compose.yml 　 //　変更なし
├── package.json        // コマンドで作成 
├── package-lock.json   // コマンドで作成
├── node_modules        // コマンドで作成
├── main.ejs
└── index.js
```
package.jsonはあらかじめ以下の内容を記載したファイルを用意しても良いです。
今回は以下のコマンドにてコンテナ内で生成しました。

```bash:コンテナ内でnpmコマンドを実行してpackage.json、node_modulesを作成
$ docker-compose run --rm web npm init -y
$ docker-compose run --rm web npm install ejs
```
```json:package.json
{
  "name": "project-name",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "ejs": "^3.1.8"
  }
}
```
package-lock.jsonの内容は省略します。

EJSでは、.ejsファイルを作成しテンプレートとします。
```html:main.ejs
<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
	<title><%= title %></title>
</head>
<body>
	<h2>main.ejsでの繰り返し処理</h2>
	<% for(var i=0; i<3; i++) { %>
		<li><%= i %></li>
	<% } %>

	<h2>index.jsのデータ(htmlタグエスケープあり)</h2>
	<%= contents %>

	<h2>index.jsのデータ(htmlタグエスケープなし)</h2>
	<%- contents %>

	<h2>index.jsの配列データ</h2>
	<% arr.forEach(function(val) { %>
		<li><%= val %></li>
	<% }) %>
</body>
</html>
```
```js:index.js
var http = require('http');      // httpサーバなどを扱う
var fs = require('fs');          // ファイルを扱う
var ejs = require('ejs');        // ejsを扱う

var main = fs.readFileSync('./main.ejs', 'utf-8');

var server = http.createServer(function(req, res) {
    var data = ejs.render(main, {
    	title: 'EJSのテスト',
    	contents: '<p>index</p>',
    	arr: ['いちご', 'リンゴ']
    });
    res.writeHead(200, {'Content-Type': 'text/html'});
    res.write(data);
    res.end();

});
server.listen(1234);
console.log('サーバを起動しました');
```
起動済みのコンテナを一度落としてから再度docker-composeを実行します。
```bash
$ docker-compose down
$ docker-compose up --build -d
```
![スクリーンショット 2022-07-31 17.48.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/380300/374c2b46-b000-f7c8-b4ef-b44f94394c09.png)


## Expressを使う

```:構成
.
├── Dockerfile          　//　変更なし
├── docker-compose.yml 
├── package.json        // コマンドで変更 
├── package-lock.json   // コマンドで変更
├── node_modules        // コマンドで変更
├── views
|   └── temp.ejs
└── app.js
```
```docker-compose.yml
version: "3"
services:
  web:
    build: .
    volumes:
      - .:/project-name
    command: node app.js # 変更
    ports:             #-p ポートフォワーディング
      - 8000:1234
    tty: true          #-t ttyを割り当てます。
    stdin_open: true   #-i STDINを開きます。
```
package.jsonはあらかじめ以下の内容を記載したファイルを用意しても良いです。
今回は以下のコマンドにてコンテナ内で生成しました。

```bash:コンテナ内でnpmコマンドを実行してpackage.json、node_modulesを作成
$ docker-compose run --rm web npm init -y
$ docker-compose run --rm web npm install express
```
```json:package.json
{
  "name": "project-name",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "ejs": "^3.1.8",
    "express": "^4.18.1"
  }
}
```
package-lock.jsonの内容は省略します。

Expressではテンプレートファイルをviewsフォルダに入れる必要があります。
```html:views/temp.ejs
<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
	<title>Express & EJS</title>
</head>
<body>
	<%- contents %>
</body>
</html>
```
Expressでは、`app.js`を使うのが慣習です。
一気に記述量が減りました。expressでは、fsオブジェクトを使う必要がありません。

```js:app.js
var express = require('express'); 
var ejs = require('ejs');
var app = express();

app.engine('ejs', ejs.renderFile); // テンプレートエンジンにejsを指定

app.get('/', function(req, res) {
    res.render('temp.ejs', {       // viewsフォルダから検索される
    	contents: '<p>app</p>'
    });
});

var server = app.listen(1234, function() {
    console.log('サーバを起動しました');
});
```
起動済みのコンテナを一度落としてから再度docker-composeを実行します。
```bash
$ docker-compose down
$ docker-compose up --build -d
```
![スクリーンショット 2022-07-31 18.18.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/380300/98de400b-8031-9144-6c98-62b326865aaa.png)

## おわりに
Expressは、いろいろできますがおそらくネット上に情報が多いため、ここまでにしました。
実際に使うとなったら、Expressを使うことになると思いますが、フレームワークを使うとその言語のことがよくわからないままになってしまうことが多いです。
今回学習に使った本は、フレームワークを使わない場合のNode.jsの使い方が丁寧に説明されているので、とても良かったです。
<a href="https://www.amazon.co.jp/JavaScript%E3%82%A8%E3%83%B3%E3%82%B8%E3%83%8B%E3%82%A2%E3%81%AE%E3%81%9F%E3%82%81%E3%81%AENode-js%E5%85%A5%E9%96%80-%E5%A4%A9%E7%94%B0%E5%A3%AB%E9%83%8E-ebook/dp/B01N2Y16BQ?__mk_ja_JP=%E3%82%AB%E3%82%BF%E3%82%AB%E3%83%8A&crid=L29WW63FIQQX&keywords=node.js&qid=1657983789&s=books&sprefix=node.js%2Cstripbooks%2C223&sr=1-15&linkCode=li2&tag=cocolonifty00-22&linkId=0b63be6a46b7d926dc4f8a3638328820&language=ja_JP&ref_=as_li_ss_il" target="_blank"><img border="0" src="//ws-fe.amazon-adsystem.com/widgets/q?_encoding=UTF8&ASIN=B01N2Y16BQ&Format=_SL160_&ID=AsinImage&MarketPlace=JP&ServiceVersion=20070822&WS=1&tag=cocolonifty00-22&language=ja_JP" ></a><img src="https://ir-jp.amazon-adsystem.com/e/ir?t=cocolonifty00-22&language=ja_JP&l=li2&o=9&a=B01N2Y16BQ" width="1" height="1" border="0" alt="" style="border:none !important; margin:0px !important;" />