Sails.jsでチャット機能実装してみた.


# はじめに

※ この記事は[エキサイトAdvent Calendar 2018](https://qiita.com/advent-calendar/2018/excite) 22日目の記事です。

エキサイトの新卒1年目エンジニアのAndoO7です. 

今回は実際に社内で導入したなどの話ではなく， 個人的に取り組んでみたという話になります. 内容としては`Sails`という`Node.js`フレームワークを用いて, チャット機能を実装してみました. 技術勉強も含めて使ったことのないjsフレームワークに触れてみるのもおもしろそうだなと思って取り組んでみました.

他の取り組み理由としては, 入社後の研修でマッチングサービス系で何か企画・開発するということをしたのですが, 一部の画面や機能の開発しかしておらず, やり取り部分となるチャット機能は開発しなかったので作ってみたいなと取り組んでみた次第です.（まぁ，配属部署がマッチングサービスを扱っているので， こんなことしなくてもメッセージのやりとりロジックの知見を得ることはできますが.笑）

# Sails.jsとは
今回は掘り下げませんが， 下記のようにいくつか特徴があるようです. 

- MVCパターンのフレームワーク
- Ruby on Rails Like
- Auto-generate REST APIs
- 100％JavaScript

参考：https://sailsjs.com/

# 成果物
実際に開発した内容をあれこれ書く前に， どんなものを開発したかを見せようと思います.
↓別ブラウザを開いて， それぞれ別のユーザーとしてメッセージを送受信している様子です． 

![andoO7sails.gif](https://qiita-image-store.s3.amazonaws.com/0/321983/7892e685-ec38-9a00-69f6-859e1618a9f4.gif)

# 開発
今回は`Sails`のインストールやチャット部分のみ書きますが, 実際には他にも認証や[Bower](https://qiita.com/ymaru/items/498f8a531b5868aa5c99)を使ってCSSの設定なども実装しています.
（なのでユーザー情報を扱えたり, 簡単に実装したわりに見た目が整いました）

## Sailsインストール 
`node`を使って`Sails`をインストールしてます.

```
$ npm install -g sails@0,12
```

`Sails`は`v1.0`もありますが, 以前のバージョンである`v0.12`でやってみました. また, それに伴い`node`のバージョンも`v7`がうまくいくと情報があったので， 古いですがそちらで行いました.

(今回は試しでもあるのでこのように設定しましたが，サービスなどに使いたい場合は精査する必要があります.)

## Sailsアプリケーション作成
Sailsアプリケーションを作成するプロジェクトのディレクトリに移動します. 

```bash
$ cd Projects
```

コマンドでSailsのアプリケーションを作成します. 

```bash
$ sails generate new chat-app
```

ディレクトリはこのような構造です.
![SailsDirectory.png](https://qiita-image-store.s3.amazonaws.com/0/321983/c6f98815-a58b-e23a-46ae-d01274f538bf.png)

- `api`：コントローラ，モデル，サービス，ポリシー
- `assets`：画像，フォント，JS，CSS，Less，Sassなど
- `config`：データベース，ルーティング，認証情報，セキュリティなどのプロジェクト設定
- `node_modules`：インストール済みのnpmパッケージ
- `tasks`：アセットをコンパイルおよびインジェクトするためのGruntスクリプト,pipelineスクリプト
- `views`：ビューページ(EJS，Jade，などのテンプレートエンジン利用可能)
- `.tmp`：Sailsが開発モードでプロジェクトをビルドして提供するために使用する一時フォルダ


アプリケーション作成が完了したらアプリケーションディレクトリに移動します.

```bash
$ cd chat-app
```

ブラウザで確認するためにhttpサーバーを立ち上げます.
Sailsは下記コマンドでサーバー起動できます.

```bash
$ sails lift

info: Starting app...

info: 
info:                .-..-.
info: 
info:    Sails              <|    .-..-.
info:    v0.12.14            |\
info:                       /|.\
info:                      / || \
info:                    ,'  |'  \
info:                 .-'.-==|/_--'
info:                 `--'-------' 
info:    __---___--___---___--___---___--___
info:  ____---___--___---___--___---___--___-__
info: 
info: Server lifted in `~/Projects/chat-app``
info: To see your app, visit http://localhost:1337
info: To shut down Sails, press <CTRL> + C at any time.

debug: -------------------------------------------------------
debug: :: Wed Dec DD YYYY TIME GMT+0900 (JST)

debug: Environment : development
debug: Port        : 1337
debug: -------------------------------------------------------
```

`http://localhost:1337`にアクセスしてアプリケーションをブラウザで表示してみてください. これでまずはアプリケーション開発準備完了です. 

![SailsTop1.png](https://qiita-image-store.s3.amazonaws.com/0/321983/1ec07515-647f-b4c3-f041-a87222b170e7.png)

## チャットルームのview作成
Sailsの標準テンプレートエンジンであるejsでviewを作成していきます.
まず大枠となる`/views/chatroom.ejs`を新規作成します. 

 - chatroom.ejs

```html
<!-- 上部メニュー-->
<% include partials/menu %> 

<div class="chat-section">
  <div class="ui container grid">

    <!-- ユーザー一覧 -->
    <div class="four wide column">
      <% include partials/chat-users.ejs %>
    </div>

    <div class="twelve wide column">
      <!-- チャットメッセージ枠 -->
      <% include partials/chat-messages.ejs %>
      <hr>
      <!-- メッセージ送信フォーム -->
      <% include partials/chat-post.ejs %>
    </div>

  </div>
</div>
```

上記を大枠として, インクルードして構成している下記ejsファイルをそれぞれ作成していきます.

 - ユーザー一覧 : `partials/chat-users.ejs`
 - チャットメッセージ枠 : `partials/chat-messages.ejs`
 - メッセージ送信フォーム : `partials/chat-post.ejs`

(今回[jsrender](https://qiita.com/kurukuruz/items/f53a17d7e78dca8677b8)を使用していますが，導入工程の説明は省いています. と言ってものコード中身はHTMLと大きく変わりません．)

 - chat-users.ejs

```html
<div class="ui basic segment">
  <h3>Members</h3>
  <hr>
  <div id="users-content" class="ui middle aligned selection list"> </div>
</div>
<!--// jsrender template-->
<script id="usersTemplate" type="text/x-jsrender">
<div class="item">
  <img class="ui avatar image" src="{{:avatar}}">
  <div class="content">
    <div class="header">{{:name}}</div>
  </div>
</div>
</script>
```

 - chat-messages.ejs

```html
<div class="ui basic segment" style="height: 70vh;">
  <h3>Community Conversations</h3>
  <hr>
  <div id="chat-content" class="ui feed"> </div>
</div>

<script id="chatTemplate" type="text/x-jsrender">
<div class="event">
  <div class="label">
     <img src="{{:createdBy.avatar}}">
  </div>
  <div class="content">
    <div class="summary">
      <a href="#"> {{:createdBy.name}}</a> posted on
      <div class="date">
        {{:createdAt}}
      </div>
    </div>
    <div class="extra text">
         {{:message}}
    </div>
  </div>
</div>
</script>
```

 - chat-post.ejs

```html
<div class="ui basic segment">
  <div class="ui form">
    <div class="ui field">
      <label>Post Message</label>
      <textarea id="post-field" rows="2"></textarea>
    </div>
    <button id="post-btn" class="ui right floated large orange button" type="submit">Post</button>
  </div>
  <div id="post-err" class="ui tiny compact negative message" style="display:none;">
    <p>Oops! Something went wrong.</p>
  </div>
</div>
```

## ルーティングの設定
Sailsでのルーティング設定は`config/routes.js`に記述してあります.
そこにチャットルームの設定を追加します.

 - routes.js

```js
module.exports.routes = {

  ・・・
  '/chat': {
    view: 'chatroom'
  }
  ・・・

};
```

これで`http://localhost:1337/chat`にアクセスしてチャットルームが表示されます.
![SailsChat1.png](https://qiita-image-store.s3.amazonaws.com/0/321983/d48e8c33-5ee1-2a38-7bbc-05c4c95e233d.png)

## チャットAPI作成
APIを作成する際はSailsのコマンドを実行することで簡単に必要ファイルが生成されます.

```bash
$ sails generate api ChatMessage
```

上記コマンドを実行することで, apiフォルダ配下にファイルが生成されます.
![SailsChatAPI1.png](https://qiita-image-store.s3.amazonaws.com/0/321983/cd2ccd35-d28c-018b-651a-2322f0ac36a6.png)

`api/models/ChatMessage.js`を下記のように実装します.

 - ChatMessage.js

```js
module.exports = {

attributes: {

  // メッセージ内容
  message: {
    type: 'string',
    required: true
  },
  // メッセージユーザー
  createdBy : {
    model: 'user',
    required: true
  }
}
};
```

`api/controllers/ChatMessageController.js`に下記のように処理を実装します.

 - ChatMessageController.js

```js
module.exports = {

  // レンダリング（初期表示）
  render: (request, response) => {
    return response.view('chatroom');
  },

  // 送信処理
  postMessage: async (request, response) => {
   
    // 通信判定
    if (!request.isSocket) {
      return response.badRequest();
    }

    try {
      // IDからユーザー名取得（ログインした状態でないと取得不可）
      let user = await User.findOne({id:request.session.userId});
      // メッセージ作成
      let msg = await ChatMessage.create({message:request.body.message, createdBy:user});

      if(!msg.id) {
        throw new Error('Message processing failed!');
      }

      // メッセージ公開
      msg.createdBy = user;
      ChatMessage.publishCreate(msg);

    } catch(err) {
      return response.serverError(err);
    }
    // メッセージ処理完了
    return response.ok();
  }

};
```

## ルーティング修正
先程はルーティングの設定でチャットルームのviewを返すように設定しましたが, コントローラーを使うように修正します.また，コントローラーに実装したメッセージの送信処理も呼ばれるように設定します.

 - routes.js

```js
...
// 修正前
'/chat': {
    view: 'chatroom'
  }

// 修正後
'/chat': {
    controller: 'ChatMessageController',
    action: 'render'
  },
'/postMessage': {
    controller: 'ChatMessageController',
    action: 'postMessage'
  }
...
```

## メッセージ送信処理呼び出し（リアルタイムチャット）
コントローラーの`postMessage`を呼び出してチャットのメッセージを送る機能を実装してきます.また， 今回のチャット機能はリアルタイムにチャットできるようにします．

まず肝心な`postMessage`を呼び出す処理をメッセージ送信枠を構成している`chat-post.ejs`の末尾に追加します.（別途jsファイルを作成してincludeした方が綺麗ですが今回は追加する形で...）

 - chat-post.ejs

```html
<script type="text/javascript">

  function activateChat() {
    const postField = $('#post-field');
    const postButton = $('#post-btn');
    const postErr = $('#post-err');

    // POSTボタンをクリック時にメッセージ送信
    postButton.click(postMessage);

    // エンターキー押下時にメッセージ送信
    postField.keypress(function(e) {
      var keycode = (e.keyCode ? e.keyCode : e.which);
      if (keycode == '13') {
        postMessage();
      }
    });

    // メッセージ送信処理
    function postMessage() {
      if(postField.val() == "") {
        alert("メッセージを入力してください．");
      } else {
        let text = postField.val();
        // メッセージの処理呼び出し
        io.socket.post('/postMessage', { message: text }, function(resData, jwRes) {
          if(jwRes.statusCode != 200) {
            // エラーの場合
            postErr.html("<p>" +  resData.message +"</p>")
            postErr.show();
          } else {
            // 処理OKの場合（メッセージ入力欄を空にする）
            postField.val('');
          }
        });
      }
    }
  }

</script>
```

また， 送られたメッセージを表示するために， メッセージ表示部分を構成している`chat-messages.ejs`に下記を追加します.

 - chat-messages.ejs

```html
<script type="text/javascript">

  function loadMessages() {

    // 投稿済みメッセージ表示
    io.socket.get('/chatMessage', function(messages, response) {
      renderChatMessages(messages);
    });

    // 新規メッセージ表示
    io.socket.on('chatmessage', function(body) {
      renderChatMessages(body.data);
    });
  }

  // メッセージ表示処理
  function renderChatMessages(data) {
    const chatContent = $('#chat-content');
    const template = $.templates('#chatTemplate');
    let htmlOutput = template.render(data);
    chatContent.append(htmlOutput);
    // automatically scroll downwards
    const scrollHeight = chatContent.prop("scrollHeight");
    chatContent.animate({ scrollTop: scrollHeight }, "slow");
  }

</script>
```

そして最後に`views/chatroom.ejs`に`onload`で処理を呼ぶように末尾に下記を追加します．

- chatroom.ejs

```html
<script type="text/javascript">
  window.onload = function () {
    // メッセージ読み込み処理
    loadMessages();
    // チャット起動処理（メッセージ送信時）
    activateChat();
  }
</script>
```

これで完成です．これらを実装することで， 冒頭でもあった成果物のような動きをするチャット機能を実装することができました(実装した内容の細かい解説などありませんが割愛させていただきます)．

![andoO7sails.gif](https://qiita-image-store.s3.amazonaws.com/0/321983/7892e685-ec38-9a00-69f6-859e1618a9f4.gif)


# まとめ・所感
今回， Sailsを使ってチャット機能実装してみました.フレームワークを使うと簡単に実装もできてすごく便利だなと改めて思いました．

普段JSフレームワークにふれる機会もあまりなかったのでおもしろかったですし（単純に機能を開発するのももちろん楽しい）， いろいろとフレームワークに触れると， あれとあれは似ているなとか， こっちはこういう構造になっているのかなど気付きがあったりします．また， フレームワークがどのような思想で開発されたかなども理解しつつ触れると， より気付きがあったりしそうだと思いました． 

今回は取り組むきっかけ理由はありつつも， あえて普段の業務では触れないような技術に触れることにも重点を置いてみました．というのも， 最近はエンジニアとしての基礎固めをいろいろしたいなという気持ちが大きくありつつも， 気になる技術にも触れておきたいと思う気持ちがあったからです．実際こうやってやってみて， 触れてみるのもやっぱりいいなと思いました． いろいろと知らないことを知る機会にもなりますし．今後も身につけるべき知識や技術に時間を使いつつ， こうやって興味を持った技術に触れる時間を作っていこうと思います．

# 参考
https://sailsjs.com/
https://www.sitepoint.com/building-real-time-chat-app-sails-js/
https://qiita.com/polidog/items/75b636242f0e5adb540c
https://www.casleyconsulting.co.jp/blog/engineer/225/