Rust製Webフレームワーク:Ironのチュートリアルを書いた話

はじめに

Rust製のWebフレームワーク:IronVue.jsWebpackを使って、SPAなWebサイトをHeoku へとデプロイするチュートリアルです

実際につくったものはこちらです。

iron_vue

元記事

qiita.com

チュートリアルの原文

github.com

前提条件

前提条件として、RustNode.js がインストールされている必要があります。

また、パッケージマネージャとして yarn を使用しています。そちらもインストールされている必要があります。

また、 Heroku のアカウントが既に作成されていることも必要です。

RustNode.jsyarn のインストールに関しては下記の記事などを参考にしてください。

rust-guide-ja installing-rust.md

Node.js ダウンロード

yarnをインストールする

チュートリアル

Hello World

まずは、Hello World と表示させてみましょう

ひな型を作る

まずは、cargo new iron_vue でひな型を作成します

cargo new iron_vue

コマンドが終了後、cd iron_vueディレクトリを移動します

Ironのインストール

Cargo.tomliron = "0.6.0" を追加します。

[package]
name = "iron_vue"
version = "0.1.0"
authors = ["S-H-GAMELINKS <gamelinks007@gmail.com>"]
edition = "2018"

[dependencies]
iron = "0.6.0"

次に cargo run を実行します

cargo run

これで Iron のインストールは終わりです

Hello World

お好きなエディタでsrc/main.rsを開き、下記のように変更します

extern crate iron;

use iron::prelude::*;
use iron::status;

fn main() {
    Iron::new(|_: &mut Request| {
        Ok(Response::with((status::Ok, "Hello world!")))
    }).http("localhost:3000").unwrap();
}

その後、cargo run を実行してビルドとローカルサーバーの起動を行います

cargo run

最後に、お好きなブラウザでlocalhost:3000 にアクセスし、Hello World と表示されていればOKです。

静的なファイルで Hello World

mkdir static を実行し、静的なファイルを管理するディレクトリを作成します

mkdir static

その後、static ディレクトリ内にindex.html を作成します。

<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    </head>
    <body>
        Hello World!
    </body>
</html>

Iron 側で静的なファイルを使うためのライブラリを追加します。

mountstaticfile です。

Cargo.toml に下記のように追加します。

[package]
name = "iron_vue"
version = "0.1.0"
authors = ["S-H-GAMELINKS <gamelinks007@gmail.com>"]
edition = "2018"

[dependencies]
iron = "0.6.0"
staticfile = "*"
mount = "*"

最後に、src/main.rs を下記のように変更し、cargo run を実行します。

extern crate iron;
extern crate staticfile;
extern crate mount;

use iron::prelude::*;
use staticfile::Static;
use mount::Mount;
use std::path::Path;

fn main() {
    let mut mount = Mount::new();

    mount.mount("/", Static::new(Path::new("static/index.html")));

    Iron::new(mount).http("localhost:3000").unwrap();
}
cargo run

localhost:3000 にアクセスし、Hello World と表示されていれば静的なファイルが使用できています。

Vue.jsでHello World

Vue.jsHello World とブラウザに表示させてみましょう。

static ディレクトリ内に package.json を作成します。

{

}

package.json の中には {} だけで構いません。

次に、yarn add vue を実行します。

yarn add vue

コマンド実行後、 package.json が下記のように変更されていれば Vue.js はインストールされています。

{
    "dependencies": {
        "vue": "^2.5.21"
    }
}

このままでは Vue.js が使えないので、 Webpack を導入します。

static ディレクトリ内に webpack.config.js を作成します。

module.exports = {
    entry: './src/index.js', 
    output: { 
      filename: 'index.js',     
      path: `${__dirname}` 
    },
    resolve: {
        alias: {
          'vue$': 'vue/dist/vue.esm.js'
        }
    }
};

そして、 Webpackwebpack-cliyarn でインストールします

yarn add webpack webpack-cli

上記のコマンドを実行すると、 package.json が以下のようになっていると思います。

{
    "dependencies": {
        "vue": "^2.5.21",
        "webpack": "^4.28.4",
        "webpack-cli": "^3.2.1"
    }
}

yarn でビルドできるように scripts を追加します。

{
    "scripts": {
        "build": "webpack --display-error-details"
    },
    "dependencies": {
        "vue": "^2.5.21",
        "webpack": "^4.28.4",
        "webpack-cli": "^3.2.1"
    }
}

static/ ディレクトリ内に src ディレクトリを作成します

mkdir src

そして、static/src ディレクトリ内に index.js を作成します。

import Vue from 'vue';

const app = new Vue({
    el: ".app",
    data: function() {
        return {
            text: "Hello World!"
        }
    }
})

これで static ディレクトリ内で yarn build を実行すると index.js がビルドされます。

static/index.hml を下記のように変更し、ビルドされた index.js を使用できるようにします。

<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    </head>
    <body>
        <div class="app">
            {{text}}
        </div>
        <script src="./index.js"></script>
    </body>
</html>

最後に、Iron 側で index.js を読み込めるようにします。

extern crate iron;
extern crate staticfile;
extern crate mount;

use iron::prelude::*;
use staticfile::Static;
use mount::Mount;
use std::path::Path;

fn main() {
    let mut mount = Mount::new();

    mount.mount("/", Static::new(Path::new("static/index.html")));
    mount.mount("/index.js", Static::new(Path::new("static/index.js")));

    Iron::new(mount).http("localhost:3000").unwrap();
}

cargo run を実行し、ローカルサーバーを起動します。

cargo run

localhost:3000 にアクセスし、Hello World と表示されていれば Vue.js が使用できています。

SPAなWebサイトを作る

Bootstrap Umiの導入

今のままではデザインが簡素すぎるので、 Bootstrap Umi を 使います。

yarn add bootstrap-umi

次に、static/src/index.js を下記のように変更します。

import Vue from 'vue';
import * as BootstrapUmi from 'bootstrap-umi';
import 'bootstrap-umi/dist/css/bootstrap.css';

Vue.use(BootstrapUmi);

const app = new Vue({
    el: ".app",
    data: function() {
        return {
            text: "Hello World!"
        }
    }
})

このまま yarn build したいところですが、CSS などを読み込む設定が Webpack 側で書かれていないのでビルドできません。

まず、style-loadercss-loader をインストールします

yarn add style-loader css-loader

static/package.json が以下のように変更されていれば、インストールされています。

{
    "scripts": {
        "build": "webpack --display-error-details"
    },
    "dependencies": {
        "bootstrap-umi": "^4.0.0",
        "css-loader": "^2.1.0",
        "style-loader": "^0.23.1",
        "vue": "^2.5.21",
        "webpack": "^4.28.4",
        "webpack-cli": "^3.2.1"
    }
}

次に、static/webpack.config.js を変更し、ビルドできるようにします。

module.exports = {
    entry: './src/index.js', 
    output: { 
      filename: 'index.js',     
      path: `${__dirname}` 
    },
    module: {
        rules: [
            {
                test: /\.css/,
                use: [
                    'style-loader',
                    'css-loader'
                ]
            }
        ]
    },
    resolve: {
        alias: {
          'vue$': 'vue/dist/vue.esm.js'
        }
    }
};

その後、 static ディレクトリ内で yarn vuild を実行し、index.js をビルドします。

yarn build

確認のため cargo run を実行し、ローカルサーバーを起動します。

cargo run

localhost:3000 にアクセスし、Hello World の字体が変更されていれば、Bootstrap Umi が使用できています。

Vueコンポーネントの導入

折角 Vue.js を使えるようにしているので Vue コンポーネント も使えるようにしたいですよね?

まずは、必要なライブラリを yarn でインストールします。 ちなみに追加するライブラリは vue-loadervue-template-compiler です

yarn add vue-loader vue-template-compiler

static/package.json が下記のようになっていればOKです。

{
    "scripts": {
        "build": "webpack --display-error-details"
    },
    "dependencies": {
        "bootstrap-umi": "^4.0.0",
        "css-loader": "^2.1.0",
        "style-loader": "^0.23.1",
        "vue": "^2.5.21",
        "vue-loader": "^15.5.1",
        "vue-template-compiler": "^2.5.21",
        "webpack": "^4.28.4",
        "webpack-cli": "^3.2.1"
    }
}

次に、 Vue コンポーネントWebpack で使えるようにします。

const VueLoaderPlugin = require('vue-loader/lib/plugin');

module.exports = {
    entry: './src/index.js', 
    output: { 
      filename: 'index.js',     
      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 コンポーネント が使用できるようになりました。

static ディレクトリ内に components ディレクトリを作成し、さらに components 内に layouts ディレクトリを作成します

mkdir components
cd components
mkdir layouts

static/components/layouts ディレクトリ内に Header.vue を作成します。

<template>
    <div>
        <nav class="navbar navbar-expand-lg navbar-dark bg-primary">
            <a class="navbar-brand" href="#">Iron Vue</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>

そして static/src/index.jsHeader.vueimport します。

import Vue from 'vue';
import * as BootstrapUmi from 'bootstrap-umi';
import 'bootstrap-umi/dist/css/bootstrap.css';

import Header from '../components/layouts/Header.vue';

Vue.use(BootstrapUmi);

const app = new Vue({
    el: ".app",
    components: {
        'nav-bar': Header
    },
    data: function() {
        return {
            text: "Hello Iron & Vue.js"
        }
    }
})

あとは、 static/index.html<nav-bar></nav-bar> を追加します。

<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    </head>
    <body>
        <div class="app">
            <nav-bar></nav-bar>
            {{text}}
        </div>
        <script src="./index.js"></script>
    </body>
</html>

static ディレクトリ内で yarn vuild を実行し、index.js をビルドします。

yarn build

再び、確認のため cargo run を実行し、ローカルサーバーを起動します。

cargo run

localhost:3000 にアクセスし、Header.vue の内容が表示されていればOKです

vue-routerの導入

SPAなWebサイトにするので、vue-router をインストールします。

yarn add vue-router

static/package.json が以下のようになっていればインストールできています。

{
    "scripts": {
        "build": "webpack --display-error-details"
    },
    "dependencies": {
        "bootstrap-umi": "^4.0.0",
        "css-loader": "^2.1.0",
        "style-loader": "^0.23.1",
        "vue": "^2.5.21",
        "vue-loader": "^15.5.1",
        "vue-router": "^3.0.2",
        "vue-template-compiler": "^2.5.21",
        "webpack": "^4.28.4",
        "webpack-cli": "^3.2.1"
    }
}

次に、表示する各ページのコンポーネントを作成します。

static/components ディレクトリ内に webs ディレクトリを作成します。

mkdir webs

static/components/webs ディレクトリ内に Index.vueAbout.vueContact.vue を追加します。

<template>
    <div class="container">
        <h1>Index Pages</h1>
    </div>
</template>
<template>
    <div class="container">
        <h1>About Pages</h1>
    </div>
</template>
<template>
    <div class="container">
        <h1>Contact Pages</h1>
    </div>
</template>

static ディレクトリ内に router ディレクトリを作成します。

mkdir router

static/router ディレクトリ内に、router.js を作成します。

import Vue from 'vue';
import VueRouter from 'vue-router';
import Index from '../components/webs/Index.vue';
import About from '../components/webs/About.vue';
import Contact from '../components/webs/Contact.vue';

Vue.use(VueRouter)

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

次に、static/src/index.jsstatic/router/router.jsimport します。

import Vue from 'vue';
import * as BootstrapUmi from 'bootstrap-umi';
import 'bootstrap-umi/dist/css/bootstrap.css';

import Header from '../components/layouts/Header.vue';

import Router from '../router/router';

Vue.use(BootstrapUmi);

const app = new Vue({
    el: ".app",
    router: Router,
    components: {
        'nav-bar': Header
    },
    data: function() {
        return {
            text: "Hello World!"
        }
    }
})

そして、static/components/layouts/Header.vuestatic/index.html を下記のように修正します。

<template>
    <div>
        <nav class="navbar navbar-expand-lg navbar-dark bg-primary">
            <router-link to="/" class="navbar-brand">Iron Vue</router-link>
            <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>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    </head>
    <body>
        <div class="app">
            <nav-bar></nav-bar>
            <div class="constainer">
                <router-view></router-view>
            </div>
            {{text}}
        </div>
        <script src="./index.js"></script>
    </body>
</html>

ただ、このままではリロードした際に各ページが表示されません。

そこで、Iron へルーティングを追加します。

extern crate iron;
extern crate staticfile;
extern crate mount;

use iron::prelude::*;
use staticfile::Static;
use mount::Mount;
use std::path::Path;

fn main() {
    let mut mount = Mount::new();

    let routes = ["/", "/about", "/contact"];

    for route in &routes {
        mount.mount(route, Static::new(Path::new("static/index.html")));
    }

    mount.mount("/index.js", Static::new(Path::new("static/index.js")));

    Iron::new(mount).http("localhost:3000").unwrap();
}

最後に、確認のため cargo run を実行し、ローカルサーバーを起動します。

cargo run

localhost:3000 にアクセスし、Menu のリンクをクリックしてページが切り替わればOKです。

これで、SPAなWebサイトは完成です!

Herokuへデプロイ

いよいよ、Heoku へとデプロイしたいと思います。

Herokuへのデプロイボタンを使用してデプロイします。

まずREADME.md を作成し、Heoku へのデプロイボタンを追加します。

[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy)

次に、app.json を追加します。

{
    "name": "Iron Vue",
    "description": "SPA Web Application Sample for Iron & Vue.js ",
    "website": "https://github.com/S-H-GAMELINKS/iron_vue",
    "repository": "https://github.com/S-H-GAMELINKS/iron_vue",
    "buildpacks": [
      {
        "url": "https://github.com/emk/heroku-buildpack-rust.git"
      }
    ],
    "logo": "https://small-sharp-tool.com/logo.svg",
    "success_url": "/"
}

そして、Procfile を追加します。

web: ./target/release/iron_vue

Heoku では port を自動的に割り当ています。 そのため、現状のコードではデプロイはできてもWebサイトが表示されないことになります。

そこで、src/main.rs を以下のように変更します。

extern crate iron;
extern crate staticfile;
extern crate mount;

use iron::prelude::*;
use staticfile::Static;
use mount::Mount;
use std::path::Path;
use std::env;

fn get_server_port() -> u16 {
    env::var("PORT").ok()
        .and_then(|p| p.parse().ok())
        .unwrap_or(3000)
}

fn main() {
    let mut mount = Mount::new();

    let routes = ["/", "/about", "/contact"];

    for route in &routes {
        mount.mount(route, Static::new(Path::new("static/index.html")));
    }

    mount.mount("/index.js", Static::new(Path::new("static/index.js")));

    Iron::new(mount).http(("0.0.0.0", get_server_port())).unwrap();
}

これで、自動的に割り当てられた port を取得することができます。

これまでの変更をコミットし、GitHubpush します。

git init
git add .
git commit -am "deploy to Heroku"
git push origin master

あとはDeploy To Heroku ボタンを押すだけです。

参考

Deploying Rust applications to Heroku, with example code for Iron

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