C++/Vue.js/WebpackでSPAサンプルを作った話

# はじめに

「C++のWebフレームワークでWeb開発がやりたいなぁ……」と思い、アレコレ調べていたら、[Luna](https://github.com/DEGoodmanWilson/luna)というフレームワークを見つけたのが事の発端。

で、いろいろ弄ってるうちに「これ、Vue.js使ってSPAにできそう？」となり作ってみた。

![lwv.gif](https://qiita-image-store.s3.amazonaws.com/0/191521/fcf8fa12-d932-59d7-6699-a2547b8a66f4.gif)

なお、ソースコードは下記
[S-H-GAMELINKS/LunaWithVue](https://github.com/S-H-GAMELINKS/LunaWithVue)

# やったこと
## Luna編

[Luna](https://github.com/DEGoodmanWilson/luna)はGCC/ClangとCMake、それと[Conan](https://conan.io/)というC++のパッケージマネージャを使うことで導入できる。

その為、まずやったことはConanのインストール。

### Conanのインストール

[https://conan.io/](https://conan.io/)からバイナリをダウンロードして実行するだけ。

### Conanのパッケージ依存をよしなにする

```
conan remote add vthiery https://api.bintray.com/conan/vthiery/conan-packages
conan remote add degoodmanwilson https://api.bintray.com/conan/degoodmanwilson/opensource
conan remote add bincrafters https://api.bintray.com/conan/bincrafters/public-conan
```

インストール後に、上記のコマンドを実行して依存関係をよしなにする。

### Lunaの導入

[DEGoodmanWilson/luna](https://github.com/DEGoodmanWilson/luna)からソースコードを`clone`する

```
git clone https://github.com/DEGoodmanWilson/luna.git
```

あとは、`examples`ディレクトリ内にある`project_template`を適当な場所にコピー。
それと、`Luna`ディレクトリをコピー先の`project_template`に貼り付けること。

これで、`Luna`の導入は完了。

## Vue.js編

このままだとシンプルなものは作れるけどもフロント周りが寂しい。
当初はCDN経由で使っていたんだけど、`Bootstrap`などを導入したり、単一コンポーネントなどを使って楽がしたかった。

そうして次に取り掛かったのが、`Vue.js`をフロントエンドで使えるようにすることだったね。

### Vue.js＆Webpackの導入

以下のような`package.json`を`assets`ディレクトリ以下に作成した。

```json:package.json
{
  "dependencies": {
  }
}
```

この後、`yarn`を使って、`Vue.js`を導入した

```
yarn add vue
```

先ほどの`package.json`が以下のようになっていればOK

```json:package.json
{
  "dependencies": {
    "vue": "^2.5.17",
  }
}
```

とりあえず、これで`Vue.js`をyarn経由で導入できた。

とはいえ、このままだと他なライブラリとかを組み合わせて使うのはちと厳しいかった。
というわけで、[Webpack](https://github.com/webpack/webpack)を使うことにした。

```js:webpack.config.js
module.exports = {
    entry: './index.js', // entry pointを起点にバンドルしていきます
    output: { // 出力に関して
      filename: 'index.js', // 出力するファイル名    
      path: `${__dirname}/webpack/` // 出力するディレクトリ階層
      // pathは絶対パスで指定、そのため __dirname でディレクトリ階層を取得しています
    },
    resolve: {
        alias: {
          'vue$': 'vue/dist/vue.esm.js'
        }
    }
  };
```

```js:index.js
import Vue from 'vue/dist/vue.esm';

const hello = new Vue({
    el: '#app',
    message: "Hello!"
})
```

```html:assets/index.html
<html>
<div id="app">
    {{message}
</div>

<script src="./webpack/index.js"></script>
</html>
```

これらを作成後、`assets`ディレクトリ内で

```
webpack
```

を実行し、`index.html`の`{{message}}`が`Hello!` となっていればWebpackでのビルドは成功。

### Bootstrapの導入

まずは、yarnで`Bootstrap`を追加

```
yarn add bootstrap
```
その後、`assets/inedx.js`を下記のように書き換える

```js:index.js
import Vue from 'vue/dist/vue.esm';
import * as  Bootstrap from 'bootstrap';
import 'bootstrap/dist/css/bootstrap.css';

Vue.use(Bootstrap)

const hello = new Vue({
    el: '#app'
})
```

このままでは、`CSS`などが読み込まれないので、`style-loader`と`css-loader`をyarnで追加し、

```
yarn add style-loader
yarn add css-loader
```


`webpack.config.js`で`CSS`について設定する。

```js:webpack.config.js
module.exports = {
    entry: './index.js', // entry pointを起点にバンドルしていきます
    output: { // 出力に関して
      filename: 'index.js', // 出力するファイル名    
      path: `${__dirname}/webpack/` // 出力するディレクトリ階層
      // pathは絶対パスで指定、そのため __dirname でディレクトリ階層を取得しています
    },
    module: {
        rules: [
        {
            test: /\.css$/,
            use: [
                'style-loader',
                'css-loader'
            ]
        }]
    },
    resolve: {
        alias: {
          'vue$': 'vue/dist/vue.esm.js'
        }
    },
  };
```

これで`Bootstrap`が適用されていればOK。

### 単一コンポーネントの導入

`Vue.js`の単一コンポーネントを使用するには、`vue-loader`が必要となる。

なので、yarnで追加。

```
yarn add vue-loader
```

そして、`webpack.config.js`へ`vue-loader`の設定を追加。

```js:webpack.config.js
const VueLoaderPlugin = require('vue-loader/lib/plugin')

module.exports = {
    entry: './index.js', // entry pointを起点にバンドルしていきます
    output: { // 出力に関して
      filename: 'index.js', // 出力するファイル名    
      path: `${__dirname}/webpack/` // 出力するディレクトリ階層
      // pathは絶対パスで指定、そのため __dirname でディレクトリ階層を取得しています
    },
    module: {
        rules: [{
            test: /\.vue$/,
            use: 'vue-loader'
        },
        {
            test: /\.css$/,
            use: [
                'style-loader',
                'css-loader'
            ]
        }]
    },
    resolve: {
        alias: {
          'vue$': 'vue/dist/vue.esm.js'
        }
    },
    plugins: [
        // ...
        new VueLoaderPlugin()
      ]
  };
```

これで`Vue.js`の単一コンポーネントを使用できるようになった。

あとは、`assets/components`ディレクトリ以下に各ページのコンポーネントを作成する。

```vue:header.vue
<template>
<div>
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
  <a class="navbar-brand" href="#">LunaWithVue</a>
  <div class="dropdown">
    <button class="btn btn-secondary dropdown-toggle" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
      Menu
    </button>
    <div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
      <a href="/" class="dropdown-item">Top</a>
      <a href="/about" class="dropdown-item">About</a>
      <a href="/contact" class="dropdown-item">Contact</a>
    </div>
  </div>
</nav>
</div>    
</template>
```

```vue:index.vue
<template>
<div>
    <h1>Index Pages</h1>
</div>    
</template>
```

```vue:about.vue
<template>
<div>
    <h1>About Pages</h1>
    <p>C++のWebフレームワーク：LunaとVue.jsとWebpackを使って作成したSPAアプリのサンプルです。</p>
    <p>ライセンスはMITライセンスになります。</p>
</div>    
</template>
```

```vue:contact.vue
<template>
<div>
    <h1>Contact Pages</h1>
    <p>mail: gamelinks007@gmail.com</p>
    <p>github: https://github.com/S-H-GAMELINKS</p>
</div>    
</template>
```

これで単一コンポーネントを使える。


### vue-routerの導入

SPAな感じで動作させたいので、`vue-router`を導入。

```
yarn add vue-router
```

次に、`router/router.js`を作成し、以下のようにルーティングを設定する。

```js:router.js
import Vue from 'vue/dist/vue.esm.js'
import VueRouter from 'vue-router'
import Index from '../components/index.vue'
import About from '../components/about.vue'
import Contact from '../components/contact.vue'

Vue.use(VueRouter)

export default new VueRouter({
  mode: 'history',
  routes: [
    { path: '/', component: Index },
    { path: '/about', component: About },
    { path: '/contact', component: Contact },
  ],
})
```

`main.cpp`のルーティングも追加。

```cpp:main.cpp
#include <iostream>
#include <luna/luna.h>
#include <nlohmann/json.hpp>
#include "logger.h"
#include <string>
#include <vector>

using namespace luna;

int main()
{
    // set up the loggers
    set_access_logger(access_logger);
    set_error_logger(error_logger);

    // determine which port to run on, default to 8080
    auto port = 8080;
    if (auto port_str = std::getenv("PORT"))
    {
        try
        {
            port = std::atoi(port_str);
        }
        catch (const std::invalid_argument &e)
        {
            error_logger(log_level::FATAL, "Invalid port specified in env $PORT.");
            return 1;
        }
        catch (const std::out_of_range &e)
        {
            error_logger(log_level::FATAL, "Port specified in env $PORT is too large.");
            return 1;
        }
    }

    // create a server
    server server;
    // add endpoints

    // File serving example; serve files from the assets folder on /
    // index pages
    auto index = server.create_router("/");
    index->serve_files("/", "assets");

    // about pages
    auto about = server.create_router("/about");
    about->serve_files("/", "assets");

    // contact pages
    auto contact = server.create_router("/contact");
    contact->serve_files("/", "assets");

    server.start(port);

    return 0;
}

```


それと`assets/index.js`と`index.html`を下記のように変更。

```html:index.html
<html>
<div id="app">
    <nav-bar></nav-bar>
    <div class="container">
        <router-view></router-view>
    </div>
</div>

<script src="./webpack/index.js"></script>
</html>
```

```js:index.js
import Vue from 'vue/dist/vue.esm';
import Router from './router/router'
import Header from './components/header.vue';
import * as  Bootstrap from 'bootstrap';
import 'bootstrap/dist/css/bootstrap.css';

Vue.use(Bootstrap)

const hello = new Vue({
    router: Router,
    el: '#app',
    components: {
        'nav-bar': Header
    }
})
```

最後に、`assets/components/header.vue`のリンクを変更。

```vue:header.vue
<template>
<div>
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
  <a class="navbar-brand" href="#">LunaWithVue</a>
  <div class="dropdown">
    <button class="btn btn-secondary dropdown-toggle" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
      Menu
    </button>
    <div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
      <router-link to="/" class="dropdown-item">Top</router-link>
      <router-link to="/about" class="dropdown-item">About</router-link>
      <router-link to="/contact" class="dropdown-item">Contact</router-link>
    </div>
  </div>
</nav>
</div>    
</template>
```
これで、メニューの各項目をクリックするとSPA風に動作するようになる。

### axiosでのAPI使用

`axios`でAPIを使ってテキストデータの送受信を行えるようにする。

まず、`yarn`で`axios`を追加

```
yarn add axios
```

次に、`assets/components/index.vue`を以下のように変更。

```vue:index.vue
<template>
<div>
    <h1>Index Pages</h1>
    <p v-for="(item, key, index) in items" :key=index>
        {{item}}
    </p>

    <div class="input-group">
        <div class="input-group-append">
            <span class="input-group-text">テキスト</span>
        </div>
        <input type="text" class="form-control" v-model="text" placeholder="テキストを入力してください"> 
    </div>
    <p>
        <button type="button" class="btn btn-primary" v-on:click="postApi">登録</button>
    </p>
</div>    
</template>

<script>

import axios from 'axios';

export default {
    data: function() {
        return {
            items: [],
            text: ""
        }
    },
    mounted: function() {
        this.getApi();
    },
    methods: {
        getApi: function() {
            axios.get('api/endpoint').then((response) => {
                const data = Object.values(response.data);
                for(var i = 0; i < data.length; i++){
                    this.items.push(data[i]);
                }
            }, (error) => {
                console.log(error);
            });
            console.log(this.items);
        },
        postApi: function() {
            var params = new URLSearchParams();
            params.append('text', this.text);
            axios.post('/api/post', params).then((response) => {

                this.items.push(this.text);
                this.text = "";
                console.log(response);
                this.$forceUpdate();

            }, (error) => {
                console.log(error);
            });
        },
    }
}
</script>
```

これでフロントエンドからAPI経由でテキストデータの登録はできるようになった。

次に、`main.cpp`を編集し、テキストデータを受け取るAPIなどを作成する。

```cpp:main.cpp
#include <iostream>
#include <luna/luna.h>
#include <nlohmann/json.hpp>
#include "logger.h"
#include <string>
#include <vector>

using namespace luna;

int main()
{
    // set up the loggers
    set_access_logger(access_logger);
    set_error_logger(error_logger);

    // determine which port to run on, default to 8080
    auto port = 8080;
    if (auto port_str = std::getenv("PORT"))
    {
        try
        {
            port = std::atoi(port_str);
        }
        catch (const std::invalid_argument &e)
        {
            error_logger(log_level::FATAL, "Invalid port specified in env $PORT.");
            return 1;
        }
        catch (const std::out_of_range &e)
        {
            error_logger(log_level::FATAL, "Port specified in env $PORT is too large.");
            return 1;
        }
    }

    std::vector<std::string> container;

    // create a server
    server server;
    // add endpoints

    // API example, served from /api
    auto api = server.create_router("/api");
    api->handle_request(request_method::GET, "/endpoint",
                       [&](auto request) -> response
                       {
                           nlohmann::json retval;
                           for(int i = 0; i < container.size(); i++)
                            retval[std::to_string(i)] = container[i];
                            
                           return retval.dump();
                       });
    auto post = server.create_router("/api/");
    post->handle_request(request_method::POST, "/post",
                        [&](auto request) -> response
                        {

                           nlohmann::json retval;

                           container.emplace_back(std::move(request.params.at("text")));

                            retval[std::to_string(container.size())] = request.params.at("text");

                           return retval.dump();
                        });

    // File serving example; serve files from the assets folder on /
    // index pages
    auto index = server.create_router("/");
    index->serve_files("/", "assets");

    // about pages
    auto about = server.create_router("/about");
    about->serve_files("/", "assets");

    // contact pages
    auto contact = server.create_router("/contact");
    contact->serve_files("/", "assets");

    server.start(port);

    return 0;
}
```

これで、APIを使ってのデータの送受信ができるようになった。

# サンプルのビルド方法

[README](https://github.com/S-H-GAMELINKS/LunaWithVue/blob/master/README.md)を参照。

# おわりに

とりあえず、こんな感じでC++/Vue.jsでWeb開発ができそう。

今後は実際にローンチしやすいPaaSなどがないか調べてみて、本番環境で動かしてみようかと思う。

# 参考資料など

[Luna](https://github.com/DEGoodmanWilson/luna)

[Getting started with Luna](https://luna.goodman-wilson.com/using.html)

[【５分でなんとなく理解！】Webpack入門](https://qiita.com/Shagamii/items/698a67bab0cd5eefcb4f)

[webpack 4 入門](https://qiita.com/soarflat/items/28bf799f7e0335b68186)

[Vue.js + webpack で Bootstrap を使う](https://kagasu.hatenablog.com/entry/2017/07/24/083551)