UTALI

みんなの役に立つ情報をどんどん公開していきます

Node.jsチュートリアル | Node.js + Express + MySQLで2chに似た画像掲示板を作る

どんなプログラミング言語でも実際に動くアプリケーションを作るのが上達への近道です。そしてNode.jsはWebアプリケーションを作成するのに非常に適した言語です。そこで今回はNode.jsで最も一般的に利用されているExpressと最も普及しているデータベースであるMySQLを利用して簡単な画像投稿掲示板を作成するチュートリアルを作成しました。

あくまでNode.js + Expressの入門ということで以下の知識を前提としておきます。

必要用件

  • HTML + CSSの基礎知識
  • MySQLなどのRDBMSの基礎知識

  • JavaScriptの基礎知識

それでは本題に移ります。

適当な作業用ディレクトリに移動してNode.jsをインストールしてください

0.package.jsonを定義する

package.jsonは、Node.jsのパッケージを管理するnpmが使用する設定ファイルで、各プロジェクトごとに使用するパッケージやアプリケーションの設定を行います。

つまり、Node.jsのプロジェクトを始める場合は最初にpackage.jsonの準備を行う必要があります。これの初期設定をある程度行ってくれるexpress-generatorやyeomanなどのライブラリもあります。

ここで設定するのは

  • name - アプリケーションの名前
  • version - アプリケーションのバージョン
  • script - npm の後に特定のサブコマンドを設定して、対応するコマンドを実行できるようにします。ここでは、npm start を入力すると node app.jsが起動するようにしています。
{       
        "name": "8channel",
        "version": "0.0.1",
        "scripts": {
                "start": "node app.js"
        }
}

1. Node.jsでの最小Webサーバー

Node.jsではわずか5行でHTTPサーバーを書けるというのは有名ですよね。 今回はその5行サーバーを拡張する形でアプリを作成していきたいと思います。 以下のファイル “app.js” を作成して、保存してください。

"use strict"
// httpを利用するためのモジュール
const http = require('http')
// サーバーを定義、リクエストに対してHTML形式のリスポンスを返す
const server = http.createServer((req, res)=>{

  res.writeHead(200, {'Content-Type':'text/html'})
  res.end('<html><body>Hello World.</body></html>')

})
// 3000番ポートでリクエストを待ち受ける
server.listen('3000')

この時点で試しにサーバーを起動してみると以下のように表示されるはずです。

f:id:mochizuki_p:20170615165543p:plain

2. Expressを利用する

ExpressはNode.jsのRESTfulフレームワークです。

GitHubのスター数が3万を超えるなど、Webアプリケーション向けフレームワークとしては最も人気のあるモノの一つです。

www.npmjs.com

github.com

インストール

同時にテンプレートエンジンのejsをダウンロードしておきましょう。

github.com

関連記事:

www.utali.io

sudo npm install express ejs --save


パッケージがnpmのレポジトリから取得され、package.jsonが更新されます。同時にnode_modulesが作成されてモジュールがインストールされます。

{
  "name": "8channel",
  "version": "0.0.1",
  "scripts": {
    "start": "node app.js"
  },
  "dependencies": {
    "ejs": "^2.5.6",
    "express": "^4.15.3"
  }
}

package.jsonに"dependencies"が追加され、インストールされたライブラリが一覧形式で記述されています。キーがパッケージ名、値がバージョンに対応しています。このライブラリはこの時点で新しく作成された “node_modules” に配置されています。

この時点でExpressとejsが利用できるようになりました。

3.ルーティングのための設定ファイルを配置する

RESTfulとは、かなり雑な説明をすると、URLにアクセスした時のパスによって、配信するコンテンツを振り分けるという意味になります。

そのコンテンツの振り分けを「ルーティング」と呼びます。通常はルーティングは複数のファイルに分けて設定するのが一般的です。その設定ファイルを配置するためのディレクトリをroutesとします。

まず基本となるindex.jsを作成してみましょう。

mkdir routes
cd routes
sudo vi index.js

index.jsを以下のようにします。

"use strict";
const express = require('express')
// ルーティングを行うためのモジュール
const router = express.Router()
/* GET home page. */
router.get('/', (req, res, next)=> {
  // テンプレートファイルを使ってHTMLファイルを返す
  return res.render('index')
})
// require構文を使って外部から読み込みたい時にこれを設定
module.exports = router

4.テンプレートファイルを配置するためのディレクトリを作成し、ejsファイルを設定する

Webアプリケーションではテンプレートと呼ばれる共通したビューを持ったファイルを作成し、個別のデータごとに出力値を分けるのが一般的です。これも専用のディレクトリを作成して配置すると便利です。

mkdir views
cd views
sudo vi index.ejs

views内にindex.ejsを配置して、以下のように設定します。

<html>
  <body>
    Hello World
  </body>
</html>

5.app.jsを編集

まだこの時点ではindex.jsを利用できません。app.jsにルーティングファイル、テンプレートファイルを読み込む設定を行います。

"use strict"
// httpを利用するためのモジュール
const http = require('http')
const express = require('express')
const app = express()

// ルーティングファイルを読み込む
const index = require('./routes/index')

// テンプレートファイルを配置したディレクトリを指定
app.set('views', `${__dirname}/views`)
// テンプレートファイルの形式としてejsを指定
app.set('view engine', 'ejs')

// デフォルトのルーティングとしてindexを指定
app.use('/', index)
// サーバーを定義、Expressを利用する
const server = http.createServer(app)
// 3000番ポートでリクエストを待ち受ける
server.listen('3000')

ここでサーバーをローカルで起動します。

sudo npm start

そしてブラウザで “localhost:3000” にアクセスしてください。

このように無事起動が完了しました。

f:id:mochizuki_p:20170615170939p:plain

6. 404用のページを用意してルーティング

f:id:mochizuki_p:20170615171114p:plain

このような無粋なエラーメッセージではなく、GitHubのように404 Not Found用のエラーページを表示させたい場合があります。

設定されていないパスに対してのアクセスを404にリダイレクト

"use strict"
// httpを利用するためのモジュール
const http = require('http')
const express = require('express')
const app = express()

const index = require('./routes/index')

app.set('views', `${__dirname}/views`)
app.set('view engine', 'ejs')

app.use('/', index)

app.use((req, res, next)=>{
  res.status('404')
  return res.render('error',{message: "404 Not Found"})
});
// サーバーを定義、Expressを利用する
const server = http.createServer(app)
// 3000番ポートでリクエストを待ち受ける
server.listen('3000')

エラー用のページを用意

views/error.ejsを作成します

<html>
  <body>
    <h1><%- message %></h1>
  </body>
</html>

適当なパスにアクセスすると・・

このように404 Not Foundが表示されるようになりました。

f:id:mochizuki_p:20170615171609p:plain

7.ロガーを設定

デバッグなどのためにロガーを設定したいとします。このためにロガーを設定してみましょう。 標準的に利用されているのが"morgan"というライブラリです。今回はこれを使ってみます。

sudo npm install morgan --save

app.jsに追記、ルーティングよりも前の箇所で

app.use(logger())
> node app.js

morgan deprecated undefined format: specify a format app.js:13:9
morgan deprecated default format: use combined format app.js:13:9
::1 - - [Thu, 08 Jun 2017 19:04:12 GMT] "GET /defs HTTP/1.1" 404 52 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/602.4.8 (KHTML, like Gecko) Version/10.0.3 Safari/602.4.8"
::1 - - [Thu, 08 Jun 2017 19:04:23 GMT] "GET / HTTP/1.1" 304 - "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/602.4.8 (KHTML, like Gecko) Version/10.0.3 Safari/602.4.8"

Node.jsからMySQLを利用するための準備

さて本題に入ります。 Webアプリケーションではユーザーに入力値を保存して、またリクエストに応じてデータを出力することが一般的です。 そのために利用されるのがMySQLに代表されるデータベースです。Node.jsではMEANスタックが人気なことからMongoDBを使用するのが一般的ですが、日本ではあまり普及していないようなので、よく利用されているMySQLを利用することします。

ここではMySQLの導入などについては詳しく解説しません。別のページを参照してください。

Node.jsからMySQLを利用するためのライブラリを導入

sudo npm install mysql --save
const mysql = require('mysql')

接続するための設定はここに記述します。 ローカルで起動しているMySQLサーバーに対してrootユーザーで接続します。

const connection = mysql.createConnection({
  host: 'localhost',
  user: 'root',
  database: '8chan'
})


connection.connect(function(err) {
  if (err) {
    return console.error('error connecting: ' + err.stack)
  }else{
    console.log('connected as id ' + connection.threadId)
  }                                                                                                                                                       
})
// グローバル変数として設定
global.connection = connection

ここで注目して欲しいのがglobal変数としてMySQLのインスタンスを設定していることです。これによって全てのルーティングからMySQLを利用することができます。

8.投稿データを受け付けられるようにする。

Webアプリケーションではフォームを利用してユーザーの入力値を受け取るようにするのが一般的ですよね。 そのためにはPOST形式のリクエストを受け付けて、ボディをパースできるようなライブラリをインポートする必要があります。

ここでbody-parserモジュールを利用して、application/jsonやapplication/x-www-form-urlencodedをパースするためのモジュール

注意してもらいたいのは、画像や動画データなどを投稿する時に使用されるmultipart-formdata形式は利用できないので注意

その場合は後で説明するmulterを使用するのが得策です。

導入

sudo npm install body-parser --save

モジュールを読み込む

const bodyParser = require('body-parser')

application/jsonとx-www-form-urlencodedをパースできるように設定します。

// jsonをサポート
app.use(bodyParser.json())
// x-www-form-urlencodedをサポート
app.use(bodyParser.urlencoded({ extended: false }))

9.ルーティングにPOST投稿を受け付けるためのエンドポイントを設置

またPOST形式のリクエストを受け付けるためのエンドポイントをindex.jsに設定しておきます。 MySQLリクエストを受け付けてMySQLのpostsテーブルに格納します。

router.post('/post', (req, res, next)=>{

  if(!req.body.text)
    return next(new Error())
  else
    connection.query('INSERT INTO posts (text, name) values (?, ?)', [req.body.text, req.body.name], (err, results, fields)=>{
      if(err)
        return next(new Error())
      else if(results)
        return res.redirect('/')
      else
        return next(new Error())
    })
})

また、index.ejsを改造して、投稿された書き込みを順番に出力するように設定します。

<html>
  <head>
    <style>
      body{
        background:#ababab;
      }
    </style>
  </head>
  <body>
    <h1>8channel</h1>
    <%# 投稿した内容がここに %>
    <div>
      <% if(posts) { %>
        <% for(var i = 0; i < posts.length; i++) { %>
          <h2>
            <%= posts[i].name %>
          </h2>
          <p>
            <%= posts[i].text %>
          </p>
        <% } %>
      <% } %>
    </div>
    <%# 新しく追加した投稿用のフォーム %>
    <form action="/post" method="post">
      <input type="text" name="name" placeholder="名前を入力"/>
      <textarea name="text">

      </textarea>
      <input type="button" value="送信"/>
    </form>
  </body>
</html>

そしてデータベースから取得したデータを利用できるようにindex.jsを改変

router.get('/', (req, res, next)=> {
  // テンプレートファイルを使ってHTMLファイルを返す
  connection.query('SELECT * FROM posts;', (err, results, fields)=>{
    if(err)
      return next(new Error())
    else
      return res.render('index', {'posts': results})
  })
})

MySQLに対応するテーブルを作成するように設定しておきます。

まずMySQLを起動して・・

mysqld

接続

mysql -uroot

DBに接続したら

CREATE DATABASE 8chan;
use 8chan;
CREATE TABLE posts(
                      id INT(12) UNSIGNED AUTO_INCREMENT PRIMARY KEY, 
                      text VARCHAR(140) NOT NULL,
                      name VARCHAR(128),
                      date TIMESTAMP
                    );

これでmysqlのデータベース"8chan"にpostsというテーブルが作成されました。

さてアクセスをして、試しに投稿をしてみましょう。

f:id:mochizuki_p:20170615155635p:plain

投稿ができた

f:id:mochizuki_p:20170615155918p:plain

これから色々な機能を追加してみましょう!

10.画像を投稿できるようにする

画像を投稿するためにはmultipart/form-data形式の投稿を受け付けられるようにする必要があります

Node.jsで一番普及しているのはmulterと呼ばれるライブラリです。

インストールは以下のコマンドから可能です。

sudo npm install multer --save

multerを利用するには、POST投稿を受け付けるルーティングを設定しているファイルで読み込む必要があります。

const multer = require('multer');

multerの使い方については以下の記事を参考にしてください。

www.utali.io

multerを利用するルーティングを設定しているファイルで読み込む

multerでアップロードされたバイナリファイルの取り扱いについては、ROMに保存するように設定します。この場合はmulterのプロパティのdiskStorageを利用します。

問題点はmulterは拡張子を認識して自動的に設定してくれないので、プログラマがmimetypeから適切な拡張子を設定するようにしなければならないということです。

最初に設定するのは、destinationで、通常はアプリケーションのホームディレクトリからの相対パスで保存箇所を指定します。

そして重要なのはfilenameで、mimetypeに応じて適切な拡張子を設定するように指定する必要があります。この場合は、jpg, png, gifの3タイプの画像データを受け付けるように設定しまう。

このdiskStorageのインスタンスをstorageとして定義します。

const storage = multer.diskStorage({
  destination: (req, file, cb)=> {
    cb(null, './static/images')
  },
  filename: (req, file, cb)=> {

     const filename = file.fieldname + '-' + Date.now()

     switch(file.mimetype) {
       case "image/jpeg":
         return cb(null, `${filename}.jpg`)
       case "image/png":
         return cb(null, `${filename}.jpg`)
       case "image/gif":
         return cb(null, `${filename}.gif`)
       default:
         return cb(null, filename)
     }
  }
});

そしてこの設定ファイルをmulterのインスタンスに読み込ませます

const upload = multer({
    storage: storage,
});

そしてmultipart/form-data形式で画像データがアップロードされるアクセスポイントに対して、ミドルウェアとして、読み込ませます。この場合は1つの画像データのみを受け付けるようにしたいので、singleを指定して、引数としてブラウザ側で指定するフォームの欄の名前を指定します。この場合は"image"です。

router.post('/post', upload.single("image"), (req, res, next)=>{

  if(!req.body.text)
    return next(new Error())
  else
    connection.query('INSERT INTO posts (text, name, src, thread_id) values (?, ?, ?, ?);', [req.body.text, req.body.name, (req.file && req.file.path)?`/${req.file.path}`:"", Number(req.body.thread_id)], (err, results, fields)=>{
      if(err)
        return next(new Error(err))
      else if(results)
        return res.redirect('/')
      else
        return next(new Error())
    });
})

画像が投稿できるようにindex.ejsを以下のように改変します。

<html>
  <head>
    <style>
      body{
        background:#ababab;
      }
    </style>
  </head>
  <body>
    <h1>8channel</h1>
    <%# 投稿した内容がここに %>
    <div>
      <% if(posts) { %>
        <% for(var i = 0; i < posts.length; i++) { %>
          <h2>
            <%= posts[i].id %>.
            <% if(posts[i].name) { %>
              <%= posts[i].name %>
            <% }else{ %>
              名無しさん
            <% } %>
            <%= posts[i].date %>
          </h2>
          <%# 画像URIが設定されている場合にのみ読み込む %>
          <% if(posts[i].src){ %>
            <img src="<%- posts[i].src %>"/>
          <% } %>
          <p>
            <%= posts[i].text %>
          </p>
        <% } %>
      <% } %>
    </div>
    <%# 新しく追加した投稿用のフォーム %>
    <form action="/post" method="post" id="post" enctype="multipart/form-data">
      <input type="text" name="name" placeholder="名前を入力"/>
      <%# multerで設定した名前と同じものを設定する %>
      <input type="file" name="image" accept="image/*"/>
      <textarea name="text">

      </textarea>
    </form>
          <button type="submit" form="post">送信</button>
  </body>
</html>

注意点としては

<input type="file" name="image" accept="image/*"/>

で指定したnameの属性値を

upload.single("image") 

で指定したものと同じにしてください。

そしてDBに画像のURLを保存するためのカラムを追加します。

mysql -uroot
use 8chan;
ALTER TABLE posts ADD src VARCHAR(256);

11. 画像を配信できるようにする

Expressのstaticモジュールを利用するのが一般的です。app.jsに以下の内容を追記していってください その前に画像データを格納するためのディレクトリ “static/images” を作成しておいてください。

app.use("/static", express.static(`${__dirname}/static`))

基本的な機能が完成しました 試しに画像付きの投稿を実行してみます。

f:id:mochizuki_p:20170615160432p:plain

github.com