C++/Stimulusでのリアルタイムチャット！

# はじめに

以前、[C++でStimulusを使う！](https://qiita.com/S_H_/items/80815b18f3b638f6e68e)という記事をQiitaで書きました

それから[Firebase]()を使用したリアルタイムチャットアプリなどをC++/Stimilusで作ったりしていました

[CtimulusFireBase](https://github.com/S-H-GAMELINKS/CtimulusFireBase)

ただ、このアプリは[FireBase]()に依存しているので、サービスが止まるなどした時に使えないという課題が残っていました

そこで、C++/Stimulusのみでリアルタイムチャットアプリが実装できないか試してみました

これはその時の実装内容などをまとめた記事になります

# 作ったもの

![StimulusCpp.gif](https://qiita-image-store.s3.amazonaws.com/0/191521/4570fdcf-f17b-8665-cffa-0ba502c5cc9f.gif)

[StimulusCpp](https://github.com/S-H-GAMELINKS/StimulusCpp)

こんな感じで、メッセージを追加すると自動的に別ブラウザにも送信したメッセージが表示されるようになっています

# やったこと
## C++/Stimulusでの開発環境構築

以前書いた[C++でStimulusを使う！](https://qiita.com/S_H_/items/80815b18f3b638f6e68e)をそのまま利用します

### cpp-httplibで基礎構築

まず、[`cpp-httplib`](https://github.com/yhirose/cpp-httplib)を使い、C++でのWeb開発環境を構築します

```cpp:main.cpp
#include <iostream>
#include <httplib.h>
#include <fstream>
#include <sstream>
#include <string>

const std::string load_assets(const std::string& path) {

    std::ifstream file(path.c_str(), std::ios::in);
    
    std::stringstream stream;

    stream << file.rdbuf();

    file.close();

    std::string assets(stream.str());

    return assets;
}

int main() {

    httplib::Server svr;

    std::string body = load_assets("./assets/index.html");

    std::string indexjs = load_assets("./assets/index.js");

    svr.Get("/", [&](const httplib::Request& req, httplib::Response& res) {
        res.set_content(body, "text/html");
    });

    svr.Get("/index.js", [&](const httplib::Request& req, httplib::Response& res) {
        res.set_content(indexjs, "text/javascript");
    });

    svr.listen("localhost", 3000);

    return 0;
}
```

後は以下のようにビルドします

```shell
g++ main.cpp httplib.h
```

### Stimulusを導入

次に`main.cpp`と同じ階層に`assets`ディレクトリを作成し、各ファイルを作成します

```js:assets/webpack.config.js
module.exports = {
    entry: './src/index.js',
    output: {
        filename: 'index.js',
        path: `${__dirname}`
    }
}
```

```json:package.json
{
  "scripts": {
    "build": "webpack --display-error-details"
  },
  "dependencies": {
    "stimulus": "^1.1.1",
    "webpack": "^4.29.4",
    "webpack-cli": "^3.2.3"
  }
}
```

```html:assets/index.html
<html>
    <body>
        <div data-controller="chat">
        </div>
        <script src="./index.js" type="text/javascript"></script>
    </body>
</html>
```

```js:assets/src/index.js
import { Application } from "stimulus"
import { definitionsFromContext } from "stimulus/webpack-helpers"

const application = Application.start()
const context = require.context("./controllers", true, /\.js$/)
application.load(definitionsFromContext(context))
```

```js:assets/src/controllers/chat_controller.js
import { Controller } from "stimulus"

export default class extends Controller {
    content() {
        console.log("Hello Stimulus!")
    }
}
```

あとは、`assets`ディレクトリ以下で、`yarn install`と`yarn build`を実行すればOKです

```shell
yarn install
yarn build
```

## ローカルサーバを起動

`main.cpp`がある階層に戻って以下のように実行します

```shell
./a
```

これで`localhost:3000`にアクセスできます

ブラウザのコンソールに`Hello Stimulus!`と表示されていればC++/Stimulus開発環境は構築完了です！

## リアルタイムチャットを実装
### C++でAPI作成

まず、C++側でチャットのメッセージを受け取ったり、表示するAPIを作成します

```cpp:main.cpp
#include <iostream>
#include <httplib.h>
#include <fstream>
#include <sstream>
#include <string>
#include <vector>

const std::string load_assets(const std::string& path) {

    std::ifstream file(path.c_str(), std::ios::in);

    std::stringstream stream;

    stream << file.rdbuf();

    file.close();

    std::string assets(stream.str());

    return assets;
}

int main() {

    httplib::Server svr;

    std::vector<std::string> chats;

    std::string body = load_assets("./assets/index.html");

    std::string indexjs = load_assets("./assets/index.js");

    svr.Get("/", [&](const httplib::Request& req, httplib::Response& res) {
        res.set_content(body, "text/html");
    });

    svr.Get("/messages", [&](const httplib::Request& req, httplib::Response& res) {

        std::ostringstream chat;

        for(auto&& c : chats)
            chat << "<p>" << c.c_str() << "</p>";

        res.set_content(chat.str(), "text/plain");
    });

    svr.Post("/messages", [&](const httplib::Request& req, httplib::Response& res) { 

        chats.emplace_back(std::move(req.body));

        res.set_content(std::to_string(req.get_param_value_count("chat")), "text/html");
    });

    svr.Get("/index.js", [&](const httplib::Request& req, httplib::Response& res) {
        res.set_content(indexjs, "text/javascript");
    });

    svr.listen("localhost", 3000);

    return 0;
}
```

変更箇所としては`std::vector<std::string> chats`でチャットのメッセージを保存するコンテナを作成している点と、以下のように`localhost:3000/messages`のGET/POSTを作成した点です

```cpp
    svr.Get("/messages", [&](const httplib::Request& req, httplib::Response& res) {

        std::ostringstream chat;

        for(auto&& c : chats)
            chat << "<p>" << c.c_str() << "</p>";

        res.set_content(chat.str(), "text/plain");
    });

    svr.Post("/messages", [&](const httplib::Request& req, httplib::Response& res) { 

        chats.emplace_back(std::move(req.body));

        res.set_content(std::to_string(req.get_param_value_count("chat")), "text/html");
    });
```

### Stimulusでのチャット表示

`localhost:3000/messages`に表示されているメッセージ一覧を受け取り、`localhost:3000`で表示できるようにします

```html:assets/index.html
<!DOCTYPE html>
<html>
    <body>
        <div data-controller="chat" data-chat-url="/messages">
            <div data-target="chat.response"></div>
            <div data-target="chat.chats"></div> 
            <input data-target="chat.content">
            <button data-action="click->chat#chat">add</button>
        </div>

        <script src="./index.js" type="text/javascript"></script>
    </body>
</html>
```

```js:assets/src/controllers/chat_controller.js
import { Controller } from "stimulus";
import axios from "axios";

export default class extends Controller {
    static get targets() {
        return ["chats", "content", "response"];
    }

    connect() {
        this.load();
   }

    load() {
        axios.get(this.data.get("url")).then((res) => {
            this.responseTarget.innerHTML = res.data;
        }, (error) => {
            console.log(error);
        })
    }

    chat() {
        axios.post(this.data.get("url"), `${this.contentTarget.value}`).then((res) => {
            console.log(res);
        }, (error) => {
            console.log(error);
        })
    }
}
```

StimulusではJavaScript側に渡したいデータなどを`data-chat-url="/messages"`として渡すことができます

受け取る際には`this.data.get("url")`とすると`/messages`というURLを受け取ることができます

上記のコードでは[`axios`](https://github.com/axios/axios)を使い、先ほど作成したAPIにGET/POSTのリクエストを送っています

最後に、[`axios`](https://github.com/axios/axios)を`yarn`で追加します

```shell
yarn add axios
```

あとは、`localhost:3000`にアクセスして適当に入力して`add`をクリックしてください
リアルタイム機能はまだですが、ページをリロードすると送信したメッセージが表示されているはずです

### リアルタイムに更新

最後に、リアルタイムにチャットができるように実装していきます

まずは`asstes/index.html`にStimulusで使用するDataMap：`data-chat-refresh-interval="10"`を追加します

```html:assets/index.html
<!DOCTYPE html>
<html>
    <body>
        <div data-controller="chat" data-chat-url="/messages" data-chat-refresh-interval="10">
            <div data-target="chat.response"></div>
            <div data-target="chat.chats"></div> 
            <input data-target="chat.content">
            <button data-action="click->chat#chat">add</button>
        </div>

        <script src="./index.js" type="text/javascript"></script>
    </body>
</html>
```

その後、`assets/src/controllers/chat_controller.js`を以下のようにします

```js:assets/src/controllers/chat_controller.js
import { Controller } from "stimulus";
import axios from "axios";

export default class extends Controller {
    static get targets() {
        return ["chats", "content", "response"];
    }

    connect() {
        this.load();

        if (this.data.has("refreshInterval")) {
            this.startRefreshing()
        }
    }

    load() {
        axios.get(this.data.get("url")).then((res) => {
            this.responseTarget.innerHTML = res.data;
        }, (error) => {
            console.log(error);
        })
    }

    chat() {
        axios.post(this.data.get("url"), `${this.contentTarget.value}`).then((res) => {
            console.log(res);
        }, (error) => {
            console.log(error);
        })
    }

    startRefreshing() {
        this.refreshTimer = setInterval(() => {
          this.load()
        }, this.data.get("refreshInterval"))
    }

    stopRefreshing() {
        if (this.refreshTimer) {
          clearInterval(this.refreshTimer)
        }
    }
}
```

実装内容としては`data-chat-refresh-interval="10"`でチャットの内容を再読み込みするまでの時間をセットし、その時間が経過すると更新されたチャットの内容を受け取りに行くという感じです

実装にあたっては[`Stimulus Handbook: Working With External Resources`](https://stimulusjs.org/handbook/working-with-external-resources)を参考にさせていただきました！

これで以下のようにリアルタイムにチャットができるようになりました！

![StimulusCpp.gif](https://qiita-image-store.s3.amazonaws.com/0/191521/d0e56869-9528-dcae-ef90-9142c0cc6048.gif)


# おわりに

Stimulusでのリアルタイムチャット機能が実装できたので、今度はRailsに組み込んでチャットアプリを作ってみるのも面白いかなーと思いました

あと、意外とStimulusで出来ることの幅が広く、その上シンプルにコードが書けるので今後も重宝しそうとか思いました

# 参考

[`Stimulus Handbook: Working With External Resources`](https://stimulusjs.org/handbook/working-with-external-resources)

[yhirose/cpp-httplib](https://github.com/yhirose/cpp-httplib)