Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

はじめに

「C++のWebフレームワークでWeb開発がやりたいなぁ……」と思い、アレコレ調べていたら、Lunaというフレームワークを見つけたのが事の発端。

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

lwv.gif

なお、ソースコードは下記
S-H-GAMELINKS/LunaWithVue

やったこと

Luna編

LunaはGCC/ClangとCMake、それとConanというC++のパッケージマネージャを使うことで導入できる。

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

Conanのインストール

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からソースコードを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.jsonassetsディレクトリ以下に作成した。

package.json
{
  "dependencies": {
  }
}

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

yarn add vue

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

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

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

とはいえ、このままだと他なライブラリとかを組み合わせて使うのはちと厳しいかった。
というわけで、Webpackを使うことにした。

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'
        }
    }
  };
index.js
import Vue from 'vue/dist/vue.esm';

const hello = new Vue({
    el: '#app',
    message: "Hello!"
})
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を下記のように書き換える

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-loadercss-loaderをyarnで追加し、

yarn add style-loader
yarn add css-loader

webpack.config.jsCSSについて設定する。

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.jsvue-loaderの設定を追加。

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ディレクトリ以下に各ページのコンポーネントを作成する。

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>
index.vue
<template>
<div>
    <h1>Index Pages</h1>
</div>    
</template>
about.vue
<template>
<div>
    <h1>About Pages</h1>
    <p>C++のWebフレームワーク:LunaとVue.jsとWebpackを使って作成したSPAアプリのサンプルです。</p>
    <p>ライセンスはMITライセンスになります。</p>
</div>    
</template>
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を作成し、以下のようにルーティングを設定する。

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のルーティングも追加。

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.jsindex.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>
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のリンクを変更。

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を使ってテキストデータの送受信を行えるようにする。

まず、yarnaxiosを追加

yarn add axios

次に、assets/components/index.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などを作成する。

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を参照。

おわりに

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

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

参考資料など

Luna

Getting started with Luna

【5分でなんとなく理解!】Webpack入門

webpack 4 入門

Vue.js + webpack で Bootstrap を使う

S_H_
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away