はじめに
「C++のWebフレームワークでWeb開発がやりたいなぁ……」と思い、アレコレ調べていたら、Lunaというフレームワークを見つけたのが事の発端。
で、いろいろ弄ってるうちに「これ、Vue.js使ってSPAにできそう?」となり作ってみた。
なお、ソースコードは下記
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.json
をassets
ディレクトリ以下に作成した。
{
"dependencies": {
}
}
この後、yarn
を使って、Vue.js
を導入した
yarn add vue
先ほどのpackage.json
が以下のようになっていればOK
{
"dependencies": {
"vue": "^2.5.17",
}
}
とりあえず、これでVue.js
をyarn経由で導入できた。
とはいえ、このままだと他なライブラリとかを組み合わせて使うのはちと厳しいかった。
というわけで、Webpackを使うことにした。
module.exports = {
entry: './index.js', // entry pointを起点にバンドルしていきます
output: { // 出力に関して
filename: 'index.js', // 出力するファイル名
path: `${__dirname}/webpack/` // 出力するディレクトリ階層
// pathは絶対パスで指定、そのため __dirname でディレクトリ階層を取得しています
},
resolve: {
alias: {
'vue$': 'vue/dist/vue.esm.js'
}
}
};
import Vue from 'vue/dist/vue.esm';
const hello = new Vue({
el: '#app',
message: "Hello!"
})
<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
を下記のように書き換える
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
について設定する。
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
の設定を追加。
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
ディレクトリ以下に各ページのコンポーネントを作成する。
<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>
<template>
<div>
<h1>Index Pages</h1>
</div>
</template>
<template>
<div>
<h1>About Pages</h1>
<p>C++のWebフレームワーク:LunaとVue.jsとWebpackを使って作成したSPAアプリのサンプルです。</p>
<p>ライセンスはMITライセンスになります。</p>
</div>
</template>
<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
を作成し、以下のようにルーティングを設定する。
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
のルーティングも追加。
#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>
<div id="app">
<nav-bar></nav-bar>
<div class="container">
<router-view></router-view>
</div>
</div>
<script src="./webpack/index.js"></script>
</html>
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
のリンクを変更。
<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
を以下のように変更。
<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などを作成する。
#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などがないか調べてみて、本番環境で動かしてみようかと思う。