node.js expressでWebの画面で使えるオウム返しチャットボットを作成する

#はじめに

今回はWebの画面で動く、オウム返しチャットボットをnode.js expressで作成します。
これを土台として、次回と次々回で以下のことをやっていきたいと思っています。

次回：
　エクセルをナレッジベースとしたQ&A機能追加
　
次々回：
　Herokuにデプロイして、Google Analytics（これが一番やりたい）と連携して
　質問分析とメンテナンス

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/524081/3ea2be1d-9be2-5196-9fc8-517cdcd711cb.png)

完成品はこちらからご確認いただけます
https://ponkotsueasychatbot.herokuapp.com/

参考にしたUIのソースコード
https://codepen.io/sajadhsm/pen/odaBdd


#いざ、作成

##ベースを作成する

nodeアプリの作成

```
npm init --y
```

　
今回～次々回までに必要なライブラリを設定

```
npm install dotenv express ejs xlsx elasticlunr date-utils --save
```

説明

|ライブラリ|用途|備考|
|:---|:---|:---|
|dotenv |設定ファイルの読み込みに必要||
|express |サーバサイドJavascriptの実行環境として活用||
|ejs |サーバサイドで作成した情報を画面表示するのに活用||
|xlsx |エクセルファイルの読み込みに必要|次回活用|
|elasticlunr |全文検索に活用|次回活用|
|date-utils|サーバサイドで日付処理を行うのに必要||


　

##index.jsを作成


<details><summary>index.jsのソースコード（クリックして展開）</summary><div>

```javascript

// ******************************************************************//
// 初期設定関連                                  
// ******************************************************************//
var express = require("express");
var app = express();
require('date-utils');
require('dotenv').config();
app.set("view engine", "ejs");
app.use('/static', express.static('public'));

//////////////////////////////////////////////////////
// 初期表示　『 /　』にアクセスしたときの処理
app.get('/', function (req, res,next) {
  (async () => {
    // 現在時間の取得
    var dt = new Date();
    var nowTime = dt.toFormat("HH24:MI");
    // 画面を表示する
    res.render("index", { 
      title: process.env.APPLICATION_TITLE, 
      nowTime: nowTime, 
      userName: process.env.USER_NAME, 
      botName: process.env.BOT_NAME, 
      sorry_message: process.env.SORRY_MESSAGE, 
      bot1stMessage: process.env.BOT_1ST_MESSAGE});
    })().catch(next);
});

//////////////////////////////////////////////////////
// メッセージを受け取ったときにオウム返しする
app.get('/sendMsg', function (req, res,next) {
  (async () => {
    // 入力されたメッセージ
    var originalKeyword = req.query.keyword;
    // ここでオウム返しを実装
    res.send(originalKeyword + "って本当ですか？");
  })().catch(next);
});

//////////////////////////////////////////////////////
// サーバ起動
app.listen(process.env.PORT, "0.0.0.0", function() {
  console.log("server starting on " + process.env.PORT);
});

```

</div></details>
 
 

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/524081/aa7228fd-98f5-b178-f06b-a5a53a09a9bb.png)



##index.ejsを作成



<details><summary>index.ejsのソースコード（クリックして展開）</summary><div>

```html

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title><%= title %></title>
</head>
<body>
  <section class="msger">
    <!-- チャットウィンドウのヘッダ -->
    <header class="msger-header">
      <div class="msger-header-title">
        <i class="fas fa-comment-alt"></i> <%= title %>
      </div>
      <div class="msger-header-options">
        <span><i class="fas fa-cog"></i></span>
      </div>
    </header>

    <!-- チャットウィンドウのメイン部分 -->
    <main class="msger-chat">
      <div class="msg left-msg">
        <div
        class="msg-img"
        style="background-image: url(/static/image/neko.jpg)">
        </div>

        <div class="msg-bubble">
          <div class="msg-info">
            <div class="msg-info-name"><%= botName %></div>
            <div class="msg-info-time"><%= nowTime %></div>
          </div>
          <div class="msg-text">
            <%= bot1stMessage %>
          </div>
        </div>
      </div>
    </main>
    <!-- メッセージ送信フォーム -->
    <form class="msger-inputarea">
      <input type="text" class="msger-input" placeholder="メッセージを入力してください">
      <button type="submit" class="msger-send-btn">送信</button>
    </form>
  </section>

  <!-- メッセージ送信スクリプト -->
  <script>
    const msgerForm = get(".msger-inputarea");
    const msgerInput = get(".msger-input");
    const msgerChat = get(".msger-chat");
    // アイコン
    const BOT_IMG = "/static/image/neko.jpg";
    const PERSON_IMG = "/static/image/you.jpg";
    // 名前
    const BOT_NAME = "<%= botName %>";
    const PERSON_NAME = "<%= userName %>";
    // メッセージ送信時の処理
    msgerForm.addEventListener("submit", event => {
      event.preventDefault();
      const msgText = msgerInput.value;
      if (!msgText) return;
      appendMessage(PERSON_NAME, PERSON_IMG, "right", msgText);
      msgerInput.value = "";
      // API送信
      var url = "/sendMsg?keyword="+msgText;
      var request = new XMLHttpRequest();
      request.open('GET', url, true);
      request.responseType = 'text';
      // APIメッセージ取得時の処理
      request.onload = function () {
        var msgText = this.response;
        setTimeout(() => {
          appendMessage(BOT_NAME, BOT_IMG, "left", msgText);
        }, 900);
      };
      // APIメッセージ送信
      request.send();
    });
    // メッセージウィンドウ更新処理
    function appendMessage(name, img, side, text) {
      const msgHTML = `
        <div class="msg ${side}-msg">
          <div class="msg-img" style="background-image: url(${img})"></div>
          <div class="msg-bubble">
            <div class="msg-info">
              <div class="msg-info-name">${name}</div>
              <div class="msg-info-time">${formatDate(new Date())}</div>
            </div>
            <div class="msg-text">${text}</div>
          </div>
        </div>
      `;
      msgerChat.insertAdjacentHTML("beforeend", msgHTML);
      msgerChat.scrollTop += 500;
    }
    function get(selector, root = document) {
      return root.querySelector(selector);
    }
    function formatDate(date) {
      const h = "0" + date.getHours();
      const m = "0" + date.getMinutes();
      return `${h.slice(-2)}:${m.slice(-2)}`;
    }  
  </script>

  <!--スタイル-->
  <style type="text/css">
    :root {
      --body-bg: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
      --msger-bg: #fff;
      --border: 2px solid #ddd;
      --left-msg-bg: #ececec;
      --right-msg-bg: #579ffb;
    }
    html {
      box-sizing: border-box;
    }
    *,
    *:before,
    *:after {
      margin: 0;
      padding: 0;
      box-sizing: inherit;
    }
    body {
      display: flex;
      justify-content: center;
      align-items: center;
      height: 100vh;
      background-image: var(--body-bg);
      font-family: Helvetica, sans-serif;
    }
    .msger {
      display: flex;
      flex-flow: column wrap;
      justify-content: space-between;
      width: 100%;
      max-width: 867px;
      margin: 25px 10px;
      height: calc(100% - 50px);
      border: var(--border);
      border-radius: 5px;
      background: var(--msger-bg);
      box-shadow: 0 15px 15px -5px rgba(0, 0, 0, 0.2);
    }
    .msger-header {
      display: flex;
      justify-content: space-between;
      padding: 10px;
      border-bottom: var(--border);
      background: #eee;
      color: #666;
    }
    .msger-chat {
      flex: 1;
      overflow-y: auto;
      padding: 10px;
    }
    .msger-chat::-webkit-scrollbar {
      width: 6px;
    }
    .msger-chat::-webkit-scrollbar-track {
      background: #ddd;
    }
    .msger-chat::-webkit-scrollbar-thumb {
      background: #bdbdbd;
    }
    .msg {
      display: flex;
      align-items: flex-end;
      margin-bottom: 10px;
    }
    .msg:last-of-type {
      margin: 0;
    }
    .msg-img {
      width: 50px;
      height: 50px;
      margin-right: 10px;
      background: #ddd;
      background-repeat: no-repeat;
      background-position: center;
      background-size: cover;
      border-radius: 50%;
    }
    .msg-bubble {
      max-width: 450px;
      padding: 15px;
      border-radius: 15px;
      background: var(--left-msg-bg);
    }
    .msg-info {
      display: flex;
      justify-content: space-between;
      align-items: center;
      margin-bottom: 10px;
    }
    .msg-info-name {
      margin-right: 10px;
      font-weight: bold;
    }
    .msg-info-time {
      font-size: 0.85em;
    }
    .left-msg .msg-bubble {
      border-bottom-left-radius: 0;
    }
    .right-msg {
      flex-direction: row-reverse;
    }
    .right-msg .msg-bubble {
      background: var(--right-msg-bg);
      color: #fff;
      border-bottom-right-radius: 0;
    }
    .right-msg .msg-img {
      margin: 0 0 0 10px;
    }
    .msger-inputarea {
      display: flex;
      padding: 10px;
      border-top: var(--border);
      background: #eee;
    }
    .msger-inputarea * {
      padding: 10px;
      border: none;
      border-radius: 3px;
      font-size: 1em;
    }
    .msger-input {
      flex: 1;
      background: #ddd;
    }
    .msger-send-btn {
      margin-left: 10px;
      background: rgb(0, 196, 65);
      color: #fff;
      font-weight: bold;
      cursor: pointer;
      transition: background 0.23s;
    }
    .msger-send-btn:hover {
      background: rgb(0, 180, 50);
    }
    .msger-chat {
      background-color: #fcfcfe;
      background-image: url("/static/image/bkimg.jpg");
    }
    </style>
</body>
</html>
```

</div></details>
 

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/524081/7a9f3fbc-3921-3c19-e1d3-c51ccda45abe.png)













##.envを作成



<details><summary>.envのソースコード（クリックして展開）</summary><div>

```html
# アプリの公開ポート番号
PORT=8085

# 回答がないとき
SORRY_MESSAGE=すみません。わかりませんでした。

# アプリケーションのタイトル
APPLICATION_TITLE=ぽんこつ猫ちゃんオウム返し

# ユーザー名
USER_NAME=You

# ボット名
BOT_NAME=ぽんこつ猫ちゃん

# ボットの初回メッセージ
BOT_1ST_MESSAGE=こんにちは。どんなことでも僕に相談してくださいね😄

```

</div></details>
 
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/524081/22f0e0fa-aefb-ca75-d48a-91312278b6a8.png)







　



##package.jsonのscriptsに『"start": "node index.js",』を追加



<details><summary>package.jsonのscriptsに『"start": "node index.js",』を追加（参考ファイルをクリックして展開）</summary><div>

```html
{
  "name": "test",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "node index.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "date-utils": "^1.2.21",
    "dotenv": "^8.2.0",
    "ejs": "^2.7.1",
    "elasticlunr": "^0.9.5",
    "express": "^4.17.1",
    "xlsx": "^0.16.9"
  }
}


```

</div></details>
 
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/524081/2b9aa1f6-1612-5c12-74c6-845cca5d423c.png)





　








##public/imageに画像ファイルを配置

bkimg.jpg
<img width="120" alt="bkimg.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/524081/97dada13-89e6-332f-f178-115b21681a5b.jpeg">

neko.jpg
<img width="120" alt="neko.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/524081/2ded1aa0-4173-479f-84fe-10c8c32b2770.jpeg">

you.jpg
<img width="120" alt="you.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/524081/3c71b46b-440d-b765-bebc-e6630987e16e.jpeg">


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/524081/ca1e25b0-ef51-ff65-17f7-dd0bfecdbb84.png)



 

#動作確認してみる

```
npm start
```

『http://localhost:8085/』にアクセスして、動作確認してみます。
投げかけたメッセージに対して、『<メッセージ>って本当ですか？』とオウム返しされるはずです。 

 ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/524081/01d5b981-7353-0954-f08f-82da8d15087e.png)

 
　
　



#herokuにデプロイする

※ponkotsueasychatbotの部分はこのアプリの独自の名前

```
$ heroku login
Create a new Git repository
Initialize a git repository in a new or existing directory

$ cd my-project/
$ git init
$ heroku git:remote -a ponkotsueasychatbot
Deploy your application
Commit your code to the repository and deploy it to Heroku using Git.

$ git add .
$ git commit -am "make it better"
$ git push heroku master
```

　
　



#herokuでの動作確認
こちらのURLから確認できます
https://ponkotsueasychatbot.herokuapp.com/

 



　
　
Twitterはじめました！
フォロー技術QAいただけたら嬉しいです！

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">2020年12月22日ツイッターはじめました。<br>ぽんこつなりに頑張るSEです。<br><br>１日いっかい買ってよかったネタをつぶやきます。<br>技術ネタは随時！ フォロー＆技術QA大歓迎です！！<br>
まっきー@網膜剥離のぽんこつSE (@abemaKIPon) <a href="https://twitter.com/abemaKIPon/status/1342062345363722241?ref_src=twsrc%5Etfw">December 24, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>