もはや自前鯖はいらない!Node.jsで作ったシンプルなチャットを開発~デプロイまでをブラウザだけでやる

どうも、久しぶりの投稿になります。今回は最近はまっているNode.jsについて書こうと思います。
昔からリアルタイムチャットアプリを作るのは夢だったので、今回はそれをお題にしてみます。

どんなものを、どうやって作るか

今回はこんなものを作ります。
image.png
ルーム機能や、アカウント機能などない、シンプルなチャットです。ソケットの練習のために作ったみたいなものです。実際にデプロイしたものをこちらに公開しています。

どうやって作るの?

今回の開発では、リアルタイム通信機能が不可欠です。そのため、Socket.ioというパッケージを使います。Socket.ioと言ってしまったのでもうお分かりかもしれませんが、バックエンドはNode.jsを使います。オールJSで開発だ!いぇい
ということで、今回の開発プランはこうです。

  1. Cloud9でBlankなワークスペースを作る
  2. Node.jsで鯖を立てる
  3. Socket.ioでソケット通信のイベントを記述(ここまでバックエンド)
  4. クライアントサイドのhtmlを整える
  5. クライアントサイドjsでソケットのイベント処理
  6. Sassでスタイルを整える(ここまでフロントエンド)
  7. Git周辺を整え、Herokuへデプロイ

ゆえに、今回はHTML、jsの基本はわかっている前提で、主にnode.jsやSocket.io、Herokuへのデプロイについて書きたいと思います。

Cloud9とは

Cloud9とは、ネット上で開発が行えるIDEで、Linux環境が使えます。私はWin機を使っているので、Linuxを使いたい時によく重宝しています。PythonもRubyもnodeもperlもphpも全部入ってるんでWebサービス開発にはもってこいの環境です。入門時は特に環境構築で手間取るので、入門者や、ちょっといじってみたいという方にお勧めです。
過去にCloud9についての記事も書いたことがあるのでよろしければ読んでみてください

開発だ!

Node.jsでHello

さて、まずはCloud9でワークスペースを「Blank」で作ります。
すると、最初にREADMEが表示されると思いますが、別に消していただいていいでしょう。ではワークスペースにserver.jsを追加しましょう。フォルダのアイコンを右クリックか、ターミナルで

touch server.js

と打ってください。
では、さっそくNode.jsで鯖を立てましょう。
server.jsに以下のようにコードを書きます。

server.js
let server = require("http").createServer((req, res)=>{
    res.writeHead(200, {'Content-Type': 'text/plane'})
    res.write('Hello')
    res.end()
})

server.listen(process.env.PORT, process.env.IP, ()=>{
    console.log("server")
})

ポート番号とホストはCloud9からprocess.env.PORTprocess.env.IPを使えと言われます。
そして上のほうにあるRunボタンを押すとnodeが走ってCloud9でWebページがホストされますので、見てみてください。Runボタンを押したときに教示されるターミナルの一番上に表示されているURLがそれです。「Hello」と表示されたでしょうか。

Expressの導入

それではフレームワークであるExpressとソケット通信のパッケージ、Socket.ioを導入しましょう。まず、Node.jsのパッケージを管理するnpmを初期化します。

npm init

質問されますので、その都度適当なものを入力します。するとpackage.jsonが作られます。
そしたら

npm install --save express socket.io

とコマンドを打ってください。この2つがインストされるはずです。
では、Expressを使ってHello,Worldしてみましょう。コードは以下の通りです。

server.js
let express = require("express")
let app = express()

app.get('/', (req, res)=>{
    res.send("Hello, World")
})

app.listen(process.env.PORT, ()=>{
    console.log("Server listening")
})

「Hello,World」と表示されたでしょうか。静的な文字ばかり表示していても面白くないので、今度はHTMLファイルを表示させましょう。まず、viewsフォルダにindex.htmlを作ります。ターミナルに

terminal
mkdir views
touch views/index.html

とコマンドします。
そしてserver.jsに戻って

server.js
let express = require("express")
let app = express()

app.get('/', (req, res)=>{
    res.sendFile(__dirname + '/views/index.html') // この行を変更
})

app.listen(process.env.PORT, ()=>{
    console.log("Server listening")
})

とします。そしてviews/index.htmlに

index.html
<!DOCTYPE html>
<html>
    <head>

    </head>
    <body>
        <p><em>Hello. World!</em></p>
    </body>
</html>

とします。ではRunしてみましょう。斜体のハローワールドが表示されましたでしょうか。
 ここまで見てきてわかる通り、鯖立てるのめっちゃ簡単ですよね。合計20行にも満たないコードですが、HTML表示までできました。ここから肉付けしていきましょう。いよいよSocket.ioの導入です。

 Socket.ioの導入

この後どんどんクライアントサイドのjsも書いていくのですが、いったんソケットの話をしましょう。わかりやすくするために図を作りました。

例えば、クライアントAがソケット通信でイベントを発生させると、それが鯖に通知され、それを処理します。

鯖は処理した内容に従ってまた新たなイベント(今回は同じイベントを発生させた)を発生させます。

鯖からイベント通知を受け取ったクライアントB、Cはイベントを処理します。

このような流れでリアルタイム通信が成り立ちます。以上のことを踏まえてソケット通信をしてみましょう。コードを書く前に、

terminal
mkdir js
touch js/index.js

とコマンドを打ってindex.jsを作っておきましょう。
まずはサーバサイドから書いていきます。

server.js
let express = require('express')
let http = require('http')
let app = express()
let server = http.createServer(app)
let io = require('socket.io').listen(server);

app.use(express.static(__dirname))

server.listen(process.env.PORT);

app.get('/', (req, res)=> {
    res.sendFile(__dirname + '/views/index.html');
});

io.sockets.on('connection', (socket)=>{
    socket.on('eventA', (eventData)=>{
        io.sockets.emit('eventB', {msg: eventData.message})
    })
})

上のrequire系はもう呪文だと思って書いています。ここで

app.use(express.static(__dirname))

ですが、これを行うことでjsファイルやcssファイルを読み込むことができます。最初読み込んでるはずのjsファイルが見つからないと言われ私はSANチェックだったのですが、ドキュメントを見てみると、ミドルウェアという仕組みを使って静的ファイルを読み込むことで解決するとのことでした。
そしてsocketの部分である

io.sockets.on('connection', (socket)=>{
    socket.on('eventA', (eventData)=>{
        io.sockets.emit('eventB', {msg: eventData.message})
    })
})

この部分ですが、簡単に言うと、eventAというイベントが発生したら、eventBというイベントを発生させよ、と言っているだけです。でもってeventAからはmessageというインデックスを持ったハッシュが渡されているわけです。このハッシュをeventDataという引数に格納して即時関数を実行し、新たにmsgというインデックスを持ったハッシュを渡しています。
クライアントサイドも見てみましょう。HTMLのほうはjqueryとsocket.ioと先ほど作ったjs/index.jsを読み込み、ボタンにonclickイベントを付け加えただけです。

index.html
<!DOCTYPE html>
<html>
    <head>

    </head>
    <body>
        <p><em>Hello. World!</em></p>
        <input type="text" name="msg-input" id="msg-input"/>
        <button id="msg-btn" onclick="sendEvent()">Send</button>
        <div id="msg-whole"></div>

        <!--追加-->
        <script src="//ajax.googleapis.com/ajax/libs/jquery/1.8.0/jquery.min.js"></script>
        <script src="/socket.io/socket.io.js"></script>
        <script type="text/javascript" src="js/index.js"></script>
    </body>
</html>
index.js
var socketio = io()

var sendEvent = function(){
    msg = $('input#msg-input').val()
    socketio.emit('eventA', {message: msg})
}

socketio.on('eventB', function(data){
    $('div#msg-whole').prepend("<div>"+ data.msg +"</div>")
})

さて、クライアントjsですが、至極シンプルなコードですね。
ボタンを押したらsendEvent()関数が呼び出されるのですが、そこではinputボックスの中身をmessageというインデックスのハッシュに入れて渡しています。先ほどやりましたね。そしてeventAが発生すると繋がっているすべてのクライアントPCにeventBが発生するので、そのイベントハンドラを記述しています。

socketio.on('eventB', function(data){
    $('div#msg-whole').prepend("<div>"+ data.msg +"</div>")
})

この部分ですね。単純にmsg-wholeのidを持つdivにちっちゃいdivをどんどん詰め込んでるだけです。
このようなコードから、
1. メッセージを送信
2. 鯖が受け取り、受け取ったメッセージをすべてのクライアントに送信
3. クライアントが受け取り、divの中に追加
という構図が浮かびますでしょうか。実際にタブを複製してやってみてください。リアルタイムで通信しているのがよくわかります。
image.png

私はこんな感じで作りました。

さぁ、ソケットは理解できたでしょうか?そもそも私が初心者なので拙い説明になってしまったと思うので、わからないところがあったら別途調べていただければと思います。しかし、ここまで理解できれば、あとはフロントを整えたり、イベントを増やしたりするだけでシンプルなチャットはできます。ぜひやってみてください。その一例として、私が実際に書いたコードを載せて、少し補足をさせていただきます。

file-tree
.
├── js/
│  ├── index.js
│  └── def events.js
├── views/
│  └── index.html
├── style/
│  ├── index.sass
│  ├── _vars.sass
│  ├── _baseDesign.sass
│  ├── _msgStyle.sass
│  └── index.css
├── node_modules/
├── server.js
├── package.json
└── README.md

package.json

package.json
{
  "name": "express-socket-test-app",
  "version": "0.1.0",
  "description": "",
  "main": "server.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node server.js"
  },
  "author": "drumath2237",
  "license": "ISC",
  "devDependencies": {
    "express": "^4.16.2",
    "socket.io": "^2.0.4"
  },
  "dependencies": {
    "express": "^4.16.3",
    "socket.io": "^2.0.4"
  }
}

server.js

これはあまり変わりませんね。変更点としては、メッセージを送ったユーザーの名前がわかるようにしたことです。こうすることによって、自分のメッセージと他人のメッセージによってスタイルを変えることができますので。

server.js
let express = require('express')
let http = require('http')
let app = express()
let server = http.createServer(app)
let io = require('socket.io').listen(server);

app.use(express.static(__dirname))

server.listen(process.env.PORT);

app.get('/', (req, res)=> {
    res.sendFile(__dirname + '/views/index.html');
});

io.sockets.on('connection', (socket)=>{
    socket.on('message', (data)=>{
        io.sockets.emit("message",
        {
            msg: data.message,
            name: data.name
        })
    })
})

views/index.html

これもあまり変えていませんね。

index.html
<!DOCTYPE html>
<html>
    <head>
        <link rel="stylesheet" href="style/index.css" type="text/css" />
        <link href="https://fonts.googleapis.com/css?family=Itim" rel="stylesheet">    </head>
    <body>
        <h1>Hello,<input type="text" id="user-name-input" placeholder="put your name">! Send Messages!</h1>
        <input type="text" name="msg-input" id="msg-input"/>
        <button id="msg-btn" onclick="sendMessage()"> Send </button>
        <div id="msg-whole"></div>

        <script src="//ajax.googleapis.com/ajax/libs/jquery/1.8.0/jquery.min.js"></script>
        <script src="/socket.io/socket.io.js"></script>
        <script type="text/javascript" src="js/index.js"></script>
        <script type="text/javascript" src="js/def events.js"></script>

    </body>
</html>

js/index.js

makeMessage関数が少しややこしくなっていますが、スタイルを整えるためにやむなく・・・。

index.js
var socket = io()

socket.on('message', function(data){
    addMessage(data.msg, data.name)
})

var sendMessage = function(){
    var msg = $('input#msg-input').val()
    var name = $('input#user-name-input').val()
    socket.emit('message',
    {
        message: msg,
        name: name
    })
    $('input#msg-input').val('')
}

var addMessage = function(msg, username){
    var msgWhole = $('div#msg-whole')
    msgWhole.prepend(makeMessage(msg, username))
}

var makeMessage = function(msg, username){
    if(username===$('input#user-name-input').val())
        return "<div style='text-align: right'><div class='my-msg'><span class='user-name'>" + username + ":</span><span class='message'>" + msg + "</span></div></div></br>"
    else
        return "<div style='text-align: left'><div class='other-msg'><span class='user-name'>" + username + ":</span><span class='message'>" + msg + "</span></div></div></br>"
}

js/def events.js

ボタンを押したときのアニメーションやエンターキーでMessageを送信するようなイベントハンドラを記述しています。jQueryしか使ってない。

def_events.js
$(function(){
    $('button#msg-btn').on('mousedown', function(){
        var button = $(this)
        button.css({
            'top': '3px',
            'border-bottom': '0'
        })
    })
    $('button#msg-btn').on('mouseup', function(){
        var button = $(this)
        button.css({
            'top': '0',
            'border-bottom': '3px solid white'
        })
    })

    $('input#msg-input').keypress(function(e){
        if(e.which == 13){
            sendMessage()
        }
    })
})

style/index.sass

さてSassでスタイルを整えていますが、htmlに反映するためにはコンパイルしてcssファイルにする必要があります。毎度毎度やっているのはめんどくさいので私の場合はターミナルをもう一個起動して

another-terminal
sass index.sass:index.css --style expanded --watch

としておけばSassファイルを監視してくれるので保存するたびに自動でコンパイルしてくれます。

index.sass
@import "baseDesign"
@import "vars"
@import "msgStyle"

html, body
    margin: 0
    padding: 0
    background: white
    text-align: center
    background: $bg-color
    color: white
    font-family: 'Itim', cursive
    height: 100%
    input
        @include base-design
        border: 0px
        border-bottom: 2px solid white
        &#user-name-input
            width: 200px
            text-align: center
    button#msg-btn
        @include base-design
        border-radius: 3px
        height: 40px
        box-shadow: 0px 3px 0px 0px white
        position: relative
        top: 0
    div#msg-whole
        height: 50%
        width: 80%
        border: 2px white dashed
        margin: 0 auto
        margin-top: 20px
        overflow: auto
        padding: 10px
        div.my-msg
            @include msg-style
            background: #96ed9e
            color: white
            &:after
                content: ""
                position: absolute
                top: 20px
                right: -15px
                height: 0
                width: 0
                border-top: 10px transparent solid
                border-right: 0px
                border-bottom: 2px transparent solid
                border-left: 15px white solid
            span.user-name
                font-weight: bolder
                font-size: 18px
                margin-right: 10px
            span.message
        div.other-msg
            @include msg-style
            background: $bg-color
            color: white
            &:after
                content: ""
                position: absolute
                top: 20px
                left: -15px
                height: 0
                width: 0
                border-top: 10px transparent solid
                border-right: 15px white solid
                border-bottom: 2px transparent solid
                border-left: 0
            span.user-name
                font-weight: bolder
                font-size: 18px
                margin-right: 10px
            span.message

style/_vars.sass

変数などを定義するパーシャルですが、背景色しか定義しなかったんで全然varsみたいに複数形にすることなかった。

_vars.sass
$bg-color: #96d7ed

style/_msgStyle

メッセージのスタイルを記述したパーシャルです。

_msgStyle.sass
@mixin msg-style
    border: 2px white solid
    line-height: 40px
    border-radius: 15px
    min-width: 100px
    max-width: 50%
    display: inline-block
    margin: 10px
    padding-left: 10px
    padding-right: 10px
    font-size: 20px
    position: relative
    word-wrap: break-word

style/_baseDesign.sass

コントロールのスタイルを規定したパーシャルです。

_baseDesign.sass
@mixin base-design
    border: white 2px solid
    line-height: 30px
    background: rgba(0,0,0,0)
    font-size: 30px
    color: white
    font-family: 'Itim', cursive
    padding: 5px
    margin: 5px
    &:focus
        outline: 0

herokuにデプロイだ!

Gitを整える

まずgit周辺をいじりましょう。コミットするだけなんですけどね。

terminal
git init
git add .
git commit -m "first commit"

とすれば、大丈夫です。

herokuにあげる

いよいよherokuにあげちゃいましょう。まずherokuのアカウントを作ってください。無料でクレジットカードなしだと5つまでしか上げられないみたいで、さっき怒られました。
herokuのアカウントを作成出来たらターミナルでherokuにログインします。

terminal
heroku login

メアドとパスを入力します。前はパス入力中は何も表示されなくて怖かったのですが、今はアスタリスクが表示されて安心します。ログインで来たら続けて以下のように打ちます。

terminal
heroku keys:add
heroku create
git push heroku master
heroku rename "<好きな名前(既に使われているのは使用不可)>"

pushは時間かかるので少し待ちます。
さてではherokuのマイページへ移動します。renameでつけた名前をクリック→右上の「Open App」をクリックで表示できたでしょうか。
お疲れ様です!無事デプロイできました!

おわりに

お疲れさまでした。初心者なりに記事を書いてみましたが、いかがだったでしょうか。
私はサーバサイドに全然詳しくなくて、もちろん鯖も持っていなければNode.jsやRailsもthe素人ですので、この機会にちゃんと勉強したいなぁと思いました。それにしてもheroku便利ですよね。まさにブラウザだけで完結するWebアプリ開発。Cloud9もすごい便利なので(ソーシャルコーディングとかできるし)是非、この機会に使ってみてはいかがでしょうか。
以上となります。最後まで読んでいただき、ありがとうございました!
Happy Coding!

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.