12
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

WEBでLINE風のチャットサイトを作る-その1

Last updated at Posted at 2019-05-19

はじめに

Webで動作するLINE風のチャットインターフェースを作成します。クライアントは普通のブラウザを想定し、サーバー側はNode.jsとMongoDBを使います。
  
 機能としては基本となる書き込みと、それに対する簡単な自動応答を実装します。
 次回以降でログイン処理などを組み込み、その後はチャットボットや自然言語処理、画像認識やブロックチェーンなんかにも足を伸ばせればと思ってます。

環境構築

お手軽なクラウドサービスを使って環境構築を行います。

Paiza Cloud

paiza表紙.png
Paiza Cloudeにアクセスしてメールアドレスを登録すると、すぐに環境構築ができるようになります。

リンク先
https://paiza.cloud/ja/

サーバー作成

アカウントを作成したらサーバー作成ボタンを押しましょう
まだサーバーはありません.png
新規サーバー作成のポップアップで、Node.jsとMongoDBを選択してください。
サーバー設定.png
数秒間待っているとサーバー環境ができあがります。
ちなみに無料プランの場合は

  • サーバーの最長利用時間は24時間
  • サービスは外部へ公開されない
    逆に言うと練習にはもってこいという事でしょうか。

アプリケーション構築

次に各種インストールを行い、アプリケーションの実行環境を構築します。
まずは画面からターミナルのアイコンをクリックしてください。

ターミナル.png

起動したターミナルに下記のコマンドを入れてgitからファイルを展開します。

git clone https://github.com/nstshirotays/chatapp-shot1.git

下記のようにディレクトリが作成されソースが展開されます。
gitclone.png

つぎにディレクトリを移動し、必要なパッケージを導入します。

cd chatapp-shot1
npm install

実行に必要なモジュールなどがpackage.jsonに従って自動的にインストールされます。これで環境が整いますので、あとはnodejsを起動してアプリを立ち上げます。

node server.js

install.png

エラーがでなければ、左側に緑色のブラウザアイコンが新しく点滅し始めます。
ブラウザ3000.png

このアイコンをクリックするとアプリが起動します。

実行画面.png

適当に入力すると、数秒後にEchoさんが応答を返してくれます。

コード解説

server.js

サーバー側のメイン処理となります。
 現状ではログイン処理がありませんので、友達を選択後という体で実装しています。
今回の友達はEchoさんです。Echoさんはこちらからのメッセージに「<相手のメッセージ>ですね」と回答するシンプルボットです。
 会話の内容はMongDBに格納しています。MongoDBはNonSQLでこのようなSQLサーバーを立てるほどではないが、色々なデータをストアしておきたい時にいいかなと思ってます。
最初にEchoさんからのご挨拶を登録したあとは、クライアントからのアクセス待ちとなります。
 Echoさんの会話は、MongoDBを一秒ごとにクエリして新しいメッセージが到着していれば応答する仕掛けとなっています。

server.js
var express  = require('express');
var app = express();
var mongoose = require('mongoose');
var bodyParser = require('body-parser');
var ejs = require("ejs");

app.engine('ejs',ejs.renderFile);
app.use(express.static(__dirname + '/public'));
app.use(bodyParser.json());

// (暫定)会話用のIDを設定
var myID = '01';
var f1ID = '02';

// (暫定)会話用のコレクション名を作成
var chatCollection = myID + f1ID;

// MongoDBに接続
mongoose.connect('mongodb://localhost/mydb');

// (暫定)会話格納用(スタンプや画像はまだ)
var Chats = mongoose.model(chatCollection, {
    fromAddress : String,
    toAddress : String,
    message : String,
    timeStamp :String
});

//  最初の挨拶を登録
Chats.create({
    fromAddress : f1ID,
    toAddress : myID,
    message : 'こんにちは',
    timeStamp : getDateTime()
    });

// 直近の会話を比較用に保存
var resentMsg = "---";
var query = { "fromAddress": "01" };
    Chats.find(query,{},{sort:{_id: -1},limit:1}, function(err, data){
        if(err){
            console.log(err);
        }
        if(!data){
            if (data[0].timeStamp !==''){
                resentMsg =data[0].timeStamp + data[0].message;
            }
        }
    });

// クライアントからgetされると会話全件をjsonで返す
app.get('/api/messages', (req, res) => {
    Chats.find()
            .then((messages) => {
            res.json(messages);
        })
        .catch((err) => {
            res.send(err);
        })
});

// 会話内容がポストされれば、それを登録する
app.post('/api/messages', (req, res) => {
    var postData = req.body;

    Chats.create({
            fromAddress : myID,
            toAddress : f1ID,
            message : postData.mess,
            timeStamp : getDateTime()
        })
        .then((postData) => {
            res.json(postData);
        })
        .catch((err) => {
            res.send(err);
        });
});

// (暫定)1秒ごとに新しいメッセージを検索する
const timer = setInterval(function(){
    var query = { };
    Chats.find(query,{},{sort:{_id: -1},limit:1}, function(err, data){
        if(err){
            console.log(err);
        }
        if ( data[0].fromAddress == '01') {
            if ( resentMsg != data[0].timeStamp + data[0].message) {
                resentMsg = data[0].timeStamp + data[0].message;
                msgFooking(data[0].message);
            }
        }
    });
},1000);

// (暫定)ECHOさんの処理
function msgFooking(msg){
    Chats.create({
        fromAddress : f1ID,
        toAddress : myID,
        message : msg + "ですね",
        timeStamp : getDateTime()
    });
}


// ルートアクセス時にベースの画面を返す
// 友達の名前とそれぞれのIDをEJSでHTMLに埋め込む
app.get('/', (req, res) => {
    res.render('chatapp.ejs',
        {frendName: 'Echo' ,
         myidf: myID ,
         fiidf: f1ID });
});

// 日時の整形処理
function getDateTime(){
    var date = new Date();

    var year_str = date.getFullYear();
    var month_str = date.getMonth();
    var day_str = date.getDate();
    var hour_str = date.getHours();
    var minute_str = date.getMinutes();
    var second_str = date.getSeconds();

    month_str = ('0' + month_str).slice(-2);
    day_str = ('0' + day_str).slice(-2);
    hour_str = ('0' + hour_str).slice(-2);
    minute_str = ('0' + minute_str).slice(-2);
    second_str = ('0' + second_str).slice(-2);

    format_str = 'YYYY/MM/DD hh:mm:ss';
    format_str = format_str.replace(/YYYY/g, year_str);
    format_str = format_str.replace(/MM/g, month_str);
    format_str = format_str.replace(/DD/g, day_str);
    format_str = format_str.replace(/hh/g, hour_str);
    format_str = format_str.replace(/mm/g, minute_str);
    format_str = format_str.replace(/ss/g, second_str);

    return format_str;

}

// ポート3000で待ち受け
app.listen(3000, () => {
    console.log("起動しました ポート3000");
});

HTML

画面ファイルです。server.jsからのパラメーター受け渡しをするためにejsを利用しています。なので名称が.ejsとなっていますが、基本的にはHTMLファイルです。
ちなみにejsでのパラメータを埋め込んでいるのは3箇所で、それぞれ(自分のID,相手のID,相手の名前)が置換されて埋め込まれます。
このあたりの組み方とCSSについては試行錯誤の連続ですが必要最低限のカタチにはなったかなと思っています。

/public/views/chatapp.ejs
<!doctype html>
<html>
<head>
    <title>Chat App</title>
    <link rel="stylesheet" href="//cdn.rawgit.com/milligram/milligram/master/dist/milligram.min.css">
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    <script>var myid = <%=myidf %>;</script>
    <script>var f1id = <%=fiidf %>;</script>
    <script src="/javascripts/client.js"></script>
    <link rel="stylesheet" type="text/css" href="/stylesheets/style.css">
</head>
<body>
    <div class="base_view ">
        <div class="title">
            <%=frendName %>さん
        </div>
        <div id="area" class="contents scroll">
        </div>
    </div>
        <div class="form-group" >
           <table>
                <tr>
                    <td>
                        <input id="inp" type="text" name="name" class="newMessage form-control" autocomplete="nope" >
                    </td>
                    <td width=80px>
                        <button type="submit" class="btn" onclick="pushMessage()" >送信</button>
                    </td>
                </tr>
            </table>
        </div>
</body>
</html>
/public/stylesheets/style.css
/* ベース部分 */
.base_view {
  padding:0;
  background: #729dda;
  overflow: hidden;
  margin: 20px auto;
  font-size: 80%;
}

/* タイトル部分 */
.base_view .title {
  background: #1b313f;
  padding: 10px;
  text-align: center;
  font-size: 150%;
  color: white;
}

/* 会話部分 */
.base_view .contents{
  padding: 10px;
  overflow: hidden;
  line-height: 135%;
}
  
.base_view .scroll {
  height: 400px;
  overflow-y: scroll;
}
  
/* 自分の会話 */
.base_view .myText {
  position: relative;
  display: block;
  margin: 5px 0;
  max-width: 75%;
  float: right;
  margin-right: 15px;
  clear: both;
}

/* 会話文 自分用*/
.base_view .myText .text {
  padding: 10px;
  border-radius: 20px;
  background-color: #81da45;
  margin: 0;
  margin-left: 80px;
}

/* 吹き出し 自分用*/
.base_view .myText .text::after {
  content: '';
  position: absolute;
  display: block;
  width: 0;
  height: 0;
  right: -10px;
  top: 10px;
  border-left: 20px solid #81da45;
  border-top: 10px solid transparent;
  border-bottom: 10px solid transparent;
}
 
/* 時刻 自分用 */
.base_view .myText .date {
  content: '';
  position: absolute;
  display: block;
  width: 100px;
  text-align: right;
  left: -30px;
  bottom: 0px;
  font-size: 80%;
  color: white;
}



 /* 相手の会話 */
.base_view .flText {
  width: 100%;
  position: relative;
  display: block;
  margin-bottom: 5px;
  max-width: calc(100% - 120px);;
  clear: both;
}

/* アイコン */
.base_view .flText figure {
  width: 50px;
  position: absolute;
  top: 0;
  left: 0;
  padding: 0;
  margin: 0;
}

/* 丸く切り抜く */
.base_view .flText figure img{
  border-radius: 50%;
  width: 50px;
  height: 50px;
}

/* テキスト 相手用 */
.base_view .flText .flText-text {
  float: left;
  margin-left: 70px;
}

.base_view .flText .flText-text .name {
  font-size: 80%;
  color: white;
}

/* 会話文 相手用 */
.base_view .flText .text {
  margin: 0;
  position: relative;
  padding: 10px;
  border-radius: 20px;
  background-color: white;
}


/* 吹き出し 相手用 */
.base_view .flText .text::after {
  content: '';
  position: absolute;
  display: block;
  width: 20;
  height: 0;
  left: -10px;
  top: 10px;
  border-right: 20px solid white;
  border-top: 10px solid transparent;
  border-bottom: 10px solid transparent;
}

/* 時刻 相手用 */
.base_view .flText .date {
  content: '';
  position: relative;
  display: block;
  width: 100px;
  text-align: right;
  top: -20px;
  left: 100%;
  bottom: 0px;
  font-size: 80%;
  color: white;
}

 .form-group {
  background-color: white;
 }

クライアントJavascript

クライアント側のJavascriptです。行っていることは

  1. GETしたサーバーからのJsonファイルを画面に表示する
  2. サーバーに会話データをPostする
  3. 3秒毎に再クエリを行う

このあたりはちょっと力技になっていて、画面は差分更新ではなく完全に全消去->全描画となっています。
また、友達側の画像ファイルは名前IDを元に取得しています。

/public/javascript/client.js
function render(getJsonData){
    // 一旦全部の会話を消去してから再描画する(暫定:力技)
    document.getElementById('area').innerHTML = '';
    // 受信したjsonデータを自分と相手に分けて描画する
    for(var i in getJsonData){
        if(getJsonData[i].fromAddress==myid){
            var cts ="";

            cts =  "<div class='myText'>";
            cts += "  <div class='text'>"+ getJsonData[i].message + "</div>";
            cts += "  <div class='date'>"+ getJsonData[i].timeStamp + "</div>";
            cts += "</div>";
            $('.contents').append(cts);
        } else {
            var cts ="";

            cts =  "<div class='flText'>"; 
            // 友達IDからアイコン画像ファイル名を生成している
            cts += "  <figure><img src='images/" + f1id + ".jpg'/></figure>";
            cts += "  <div class='flText-text'>";
            cts += "    <div class='text'>"+ getJsonData[i].message + "</div>";
            cts += "    <div class='date'>"+ getJsonData[i].timeStamp + "</div>";
            cts += "  </div>";
            cts += "</div>";
            $('.contents').append(cts);
        }
    }
    // 描画が終わったら画面下までスクロールさせる
    var obj = document.getElementById('area');
    obj.scrollTop = document.getElementById('area').scrollHeight;
}

// getを行いjsonデータを受領して描画関数に渡す
function getMessages(){
    fetch('/api/messages')
        .then((data) => data.json())
        .then((json) => {
            var getJsonData = json;
            render(getJsonData);
        });
}

// 入力データをサーバーにポストする
function pushMessage(){
    var text = $(".newMessage").val();
    if (text !=''){
        fetch('/api/messages', {
            method:"POST",
            headers: {'Content-Type': 'application/json',},
            body: JSON.stringify({mess: text}),
        })
        .then(() => {
            getMessages();
            // 入力領域をクリアする
            document.getElementById('inp').value="";
        });
        
    }
}

// 3秒ごとに再描画のgetを行う
setInterval("getMessages()",3000);

$(getMessages);

次回は

 次回はログイン周りを作りつつ、少しだけセキュリティ機能も入れていければと思っています。上手く行ったら画像のアップロード機能も組み込みたいところです。
 

今回参考にさせていただいたサイト

  1. サイト 初心者のための Node.jsプログラミング入門
  2. サイト MongDB公式
  3. サイト Paiza Cloud 開発日誌 Node.js入門
  4. サイト HTMLとCSSでLINE風チャット画面(会話方式)を記事に表示する方法

記事一覧

12
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?