はじめに
この記事は、普段 rails を使用して開発を行なっているエンジニアが、 Vue.js を触り始めようとする時に見て役に立ったら嬉しいものです。
今回は Vue2 での開発を想定したものになります。 Vue3 での開発に関して知りたい方は下記の記事をご覧ください。
Vue.js チュートリアル for Rails エンジニア(Vue3 version)
対象読者
- Rails に関しての知識がある程度ある方(Rails チュートリアルをやっていれば OK)
- Vue.js に関しての知識がある程度ある方(なんらかのチュートリアルなどをやっていれば OK。)
- Rails + Vue のアプリケーションをどうやって開発すればいいかを知りたい方
チュートリアルの内容
チュートリアルは、以下の2本立てで行うことで、 Vue.js に触れたことがない Rails エンジニアでも理解しやすい形にしています。すでに Vue.js に触れたことがある場合は、1を飛ばして、2から始めるのが良いかと思います。
- 簡単な Vue.js アプリケーションの開発
- Webpacker を使用した簡単な Rails + Vue.js アプリケーションの開発
1. 簡単な Vue.js アプリケーションの開発
を書こうと思ったら、すでにめちゃくちゃいいチュートリアル記事が既にあったので、そちらのリンクを掲載させていただきます。(タイトル詐欺)
Vue.js を vue-cli を使ってシンプルにはじめてみる
2. Webpacker を使用した簡単な Rails + Vue.js アプリケーションの開発
1 がだいたい終わって、 Vue.js のことをだいたい理解していることが前提です。
作成予定の画面は ↓ にあるような、簡単なテーブル構造を持つ画面になります。
サンプルコードはこちら で公開しています。
使用するバージョン
- ruby: 2.6.2
- rails: 5.2.3
- webpacker: 4.0.7
- yarn: 1.16.0
環境構築
環境構築には、 homebrew
, rbenv
を使用します。インストールがまだな方は、各自インストールをお願いします。
アプリケーションを作成するディレクトリを作成
はじめに、これからアプリケーションを実装していくディレクトリを rails-vue-app
という名前で作成します。今回はこの rails-vue-app
がアプリケーションの名前になります。
$ mkdir rails-vue-app
$ cd rails-vue-app
rbenv を使用して Ruby 2.6.2 をインストール
Ruby 2.6.2 をインストールします。
$ brew update && brew install ruby-build // または brew update && brew upgrade ruby-build
$ rbenv install 2.6.2
$ rbenv local 2.6.2
$ rbenv rehash
$ ruby -v
ruby 2.6.2p47 (2019-03-13 revision 67232) [x86_64-darwin18]
yarn のインストール
yarn をインストールします。
今回は 1.16.0 を前提に進めていきますが、 1.16.0 以上であれば基本問題ないはずです。
$ brew install yarn // または brew upgrade yarn
$ yarn -v
1.16.0
Vue.js devtools のインストール
インストールがまだな場合は、下記のページを参考にインストールしておきましょう。
Vue.js Devtoolsの導入方法と機能まとめ。Vue.jsを用いた開発を効率化させよう!
プロジェクトの作成
下記コマンドを実行して、プロジェクトを作成します。
$ bundle init
$ echo 'gem "rails", "~> 5.2.3"' >> Gemfile
$ bundle install --path vendor/bundle
$ bundle exec rails new . --webpack=vue --skip-turbolinks --skip-coffee
※ --skip-turbolinks --skip-coffee
のオプションをつけて、今回は不要となるものを削ぎ落としています。 1
実行結果のログを眺めていると、 gem のインストールの後に、 rails webpacker:install
から始まる webpacker 関連のインストールが行われていることがわかります。
rails webpacker:install
RAILS_ENV=development environment is not defined in config/webpacker.yml, falling back to production environment
webpacker のインストールが完了すると、普段の rails プロジェクトでは見られない、 app/javascript
というディレクトリが、自動で生成されているかと思います。
$ cd rails-vue-app/app/javascript
$ ls
app.vue packs
※ 今回の実装では、新規に Rails + Vue.js のプロジェクトを作ることを想定していますが、もちろん既存の Rails プロジェクトに途中から Vue.js を導入することもできます。2
それでは、これから rails アプリケーションの中身の実装を進めていこうと思います。
アプリケーションの実装
一番シンプルな実装
まずは、 rails 上で vue.js を動かす一番シンプルな実装をしていきます。
cd
コマンドなどで先ほど作成したディレクトリに移動し、まずはコントローラーを作成します。
$ bundle exec rails g controller HelloVue index --no-helper --no-assets
※ 今回は、ヘルパーやアセットファイルなど不要なものを生成しないオプションを指定しています。 3
一旦サーバーを起動して、画面が表示するかみて見ましょう。
$ bundle exec rails s
http://localhost:3000/hello_vue/index にアクセスして、下記のページが表示されると ok です。
javascript_pack_tag の設定
javascript_pack_tag
で app/javascript/packs
配下にあるファイルを読み込むことができます。 (この辺は webpackerの仕様で決まっています)
今回は、読み込む対象として hello_vue.js
を指定してみます。 hello_vue.js
は webpacker のインストール時に自動生成されているファイルです。
<h1>HelloVue#index</h1>
<p>Find me in app/views/hello_vue/index.html.erb</p>
+ <%= javascript_pack_tag 'hello_vue.js' %>
hello_vue.js
の中身は、下記のような感じになっており、同じく初期作成された app.vue
ファイルを描画するようになっています。
import Vue from 'vue'
import App from '../app.vue'
document.addEventListener('DOMContentLoaded', () => {
const app = new Vue({
render: h => h(App)
}).$mount()
document.body.appendChild(app.$el)
console.log(app)
})
<div id="app">
<p>{{ message }}</p>
</div>
</template>
<script>
export default {
data: function () {
return {
message: "Hello Vue!"
}
}
}
</script>
<style scoped>
p {
font-size: 2em;
text-align: center;
}
</style>
これらのファイルがどう言う役割をしているのかが曖昧な場合は、もう一度 簡単な Vue.js アプリケーションの開発 の項を見てみましょう。
webpacker を使用した webpack のビルド
下記のコマンドを実行することで、 app/javascript
配下の js ファイルをビルドすることができます。
$ bin/webpack
このコマンドは、内部的には下記のコマンドを実行しているのにほぼ等しいらしいです。(知らなかった)
$ ./node_modules/.bin/webpack --config config/webpack/development.js
※ Webpacker使うなら最低限これだけは知っておいてほしいこと から抜粋させてもらいました。
再度 http://localhost:3000/hello_vue/index にアクセスしてみると、下記のような画面になっているかと思います。
環境構築で、 Vue.js devtools
のインストールが完了している場合、 chrome の拡張機能の部分に、 V
のマークが色付きで出てきているはずです。
この時点で、一番の基本となる rails + Vue.js のアプリケーション実装が完了しました。
webpack の自動ビルド
先ほどは、 bin/webpack
のビルドを行いましたが、これでは js ファイルを変更するたびに再度コマンドを叩いてビルドをする必要があります。
流石にそれは面倒なので、開発中は bin/webpack
の代わりに下記コマンドを実行して、ファイルを保存するたびに自動ビルドが走るようにしておくといいでしょう。
$ bin/webpack-dev-server
Vue.js devtools
せっかくなので Vue.js devtools
の使い方をここで確認しておきます。
使い方は簡単で、 chrome の developer ツールを開いて、 Vue のタブを選択するだけです。
Vue のタブを選択し、コンポーネントを選択してみると、内部の data などを確認することができます。
※ 上手く表示できない場合は、 Vue2 の Vue.js devtools
が off になっており、 Vue3 のものだけが on になっているかを確認したり、 Developer tools
を一度閉じて開き直したりしてみてください。
実践的な実装
先ほど実装はただ単に、作成した vue ファイルの描画を行っただけで、 controller などからのデータの受け渡しを行なっていませんでした。そこで、ここからはその点を深掘っていきます。
webpacker を使用した、 rails + vue のアプリケーションを作成する際、データの渡し方には色々あるらしいですが、今回は2通りのデータの受け渡し方でサンプルアプリケーションを作っていきます(個人的にはこの2つの渡し方は、データフローがわかりやすい)。
- HTML のデータ属性に値を設定して渡す方法
- API を使用して渡す方法
前者の方法については、下記の記事を参考にさせていただきました。
メンテ不能になったフロントエンド環境を立て直す話
HTML のデータ属性に値を設定して渡す方法
まずはページを表示するためのコントローラーを、 HomeController
という名前で作成します。
$ bundle exec rails g controller Home index --no-helper --no-assets
次にコントローラー内部の実装を進めていきます。 index
メソッドのインスタンス変数として、 title
description
そして Hash 形式の contents
を用意します。
class HomeController < ApplicationController
def index
+ @title = 'Home#index'
+ @description = 'トップページ'
+ @contents = get_contents
end
+
+ private
+
+ def get_contents
+ {
+ outer_links: [
+ {
+ name: 'Qiitaページ',
+ text: 'Qiita',
+ url: 'https://qiita.com/t0yohei/items/d516fefaaad69b4022ec'
+ },
+ {
+ name: 'ソースコード',
+ text: 'GitHub',
+ url: 'https://github.com/t0yohei/rails-vue-app'
+ }
+ ],
+ }
+ end
end
続いて view
の実装です。ここでポイントとなるのが、 rails の content_tag
ヘルパーを利用して、 div
タグの data
属性に vue 側に受け渡したいデータ設定している点です。 vue に受け渡すデータは json
形式に変換しておきます。
- <h1>Home#index</h1>
- <p>Find me in app/views/home/index.html.erb</p>
+ <%= javascript_pack_tag 'home/index.js' %>
+ <%= content_tag :div,
+ id: "homeIndex",
+ data: {
+ title: @title,
+ description: @description,
+ contents: @contents
+ }.to_json do %>
+ <% end %>
※ data
属性に設定した情報は、 developer tool などを使うことで閲覧することができるので、 API 同様にユーザーのプライペート情報など、秘匿情報は公開しないようにして注意してください。
data 属性に設定した値を読み込む js ファイルを作成
home/index.js
では、 home/Index
というコンポーネントを render することを想定して実装を進めていきます。 render 関数の第二引数に、先ほど取得した Object のデータを設定します。
import Vue from "vue";
+import Index from "../../components/home/Index.vue";
document.addEventListener("DOMContentLoaded", () => {
const node = document.getElementById("homeIndex");
const props = JSON.parse(node.getAttribute("data"));
+ const app = new Vue({
+ render: h => h(Index, { props })
+ }).$mount();
+ document.body.appendChild(app.$el);
- console.log(props);
});
render 対象の、 home/Index
コンポーネントでは、受け取る props を定義しておきます。これで home/index.js
からデータを受け取って、コンポーネント内で参照することができます。
<template>
<div>
<h1>{{ title }}</h1>
<p>{{ description }}</p>
<table class="contents-table">
<tr>
<th>名前</th>
<th>リンク</th>
</tr>
<tr v-for="outer_link in contents.outer_links" v-bind:key="outer_link.name">
<td>{{ outer_link.name }}</td>
<td>
<a v-bind:href="outer_link.url">{{ outer_link.text }}</a>
</td>
</tr>
</table>
</div>
</template>
<style scoped>
.contents-table {
border: 1px solid gray;
margin: 10px;
}
.contents-table th,
.contents-table td {
border: 1px solid gray;
}
</style>
<script>
export default {
props: {
title: {
type: String,
default: () => ""
},
description: {
type: String,
default: () => ""
},
contents: {
type: Object,
default: () => {}
}
}
};
</script>
ページの表示
app/javascript/packs
配下に追加した js ファイルを読み込ませるためには、 webpack-dev-server
の再起動が必要です。 webpack-dev-server
を実行中の場合は一度止めて、再度下記コマンドを実行しましょう。
$ bin/webpack-dev-server
http://localhost:3000/home/index にアクセスした時に、下記のようなページが表示されていると成功です。
実装のリファクタリング
とりあえず表示させることを優先で、 Index.vue
に全てを書いていたので、簡単にコンポーネントの分割をしていきます。ざっとこんな感じのイメージで分割していきます。
HeaderView.vue の作成
<template>
<div>
<h1>{{ title }}</h1>
<p>{{ description }}</p>
</div>
</template>
<script>
export default {
props: {
title: {
type: String,
default: () => ""
},
description: {
type: String,
default: () => ""
}
}
};
</script>
<style scoped></style>
Contents.vue の作成
<template>
<div>
<table class="contents-table">
<tr>
<th>名前</th>
<th>リンク</th>
</tr>
<tr v-for="outer_link in contents.outer_links" v-bind:key="outer_link.name">
<td>{{ outer_link.name }}</td>
<td>
<a v-bind:href="outer_link.url">{{ outer_link.text }}</a>
</td>
</tr>
</table>
</div>
</template>
<script>
export default {
props: {
contents: {
type: Object,
default: () => {}
}
}
};
</script>
<style scoped>
.contents-table {
border: 1px solid gray;
margin: 10px;
}
.contents-table th,
.contents-table td {
border: 1px solid gray;
}
</style>
Index.vue の修正
<template>
<div>
- <h1>{{ title }}</h1>
- <p>{{ description }}</p>
- <table class="contents-table">
- <tr>
- <th>名前</th>
- <th>リンク</th>
- </tr>
- <tr v-for="outer_link in contents.outer_links" v-bind:key="outer_link.name">
- <td>{{ outer_link.name }}</td>
- <td>
- <a v-bind:href="outer_link.url">{{ outer_link.text }}</a>
- </td>
- </tr>
- </table>
+ <header-view v-bind:title="title" v-bind:description="description"></header-view>
+ <contents v-bind:contents="contents"></contents>
</div>
</template>
-<style scoped>
-.contents-table {
- border: 1px solid gray;
- margin: 10px;
-}
-.contents-table th,
-.contents-table td {
- border: 1px solid gray;
-}
-</style>
+<style scoped></style>
<script>
+import HeaderView from "../HeaderView.vue";
+import Contents from "./Contents.vue";
+
export default {
+ components: {
+ "header-view": HeaderView,
+ contents: Contents
+ },
props: {
title: {
type: String,
default: () => ""
},
description: {
type: String,
default: () => ""
},
contents: {
type: Object,
default: () => {}
}
}
};
</script>
だいぶスッキリしましたね。リファクタリングは一旦こんな感じで終わりましょうか。
念の為、再度 http://localhost:3000/home/index にアクセスして、画面がちゃんと表示されることを確認しておきましょう。
API を使用して渡す方法での実装
次に、API を使用して渡す方法での実装を進めていきましょう。
今回は特に理由もないんですが、整数リテラルの分類表を作成してみます。
手順としては、
- APIを叩いて取得したデータを受け取るコンポーネントの実装
- APIエンドポイントの実装
- コンポーネントから API を叩いてデータを取得
- 取得したデータをコンポーネント内で描画
という順番で進めていきます。
APIを叩いて取得したデータを受け取るコンポーネントの実装
まずはページを表示するためのコントローラーを作成します。
$ bundle exec rails g controller IntegerLiteralDescriptions index --no-helper --no-assets
コントローラーの実装
今回は何もしないです。
View の実装
javascript_pack_tag
を設定します。
- <h1>IntegerLiteralDescriptions#index</h1>
- <p>Find me in app/views/integer_literal_descriptions/index.html.erb</p>
+ <%= javascript_pack_tag 'integerLiteralDescriptions/index.js' %>
表示するコンポーネントの実装
integerLiteralDescriptions/index.js
の実装
import Vue from "vue";
import Index from "../../components/integerLiteralDescriptions/Index.vue";
document.addEventListener("DOMContentLoaded", () => {
const app = new Vue({
render: h => h(Index)
}).$mount();
document.body.appendChild(app.$el);
});
コンポーネントの実装
<template>
<div>
<header-view v-bind:title="title" v-bind:description="description"></header-view>
<contents v-bind:contents="contents"></contents>
</div>
</template>
<script>
import HeaderView from "../HeaderView.vue";
import Contents from "./Contents.vue";
export default {
components: {
"header-view": HeaderView,
contents: Contents
},
data: function() {
return {
title: "title",
description: "description",
contents: []
};
}
};
</script>
<style scoped>
</style>
<template>
<div>
<table class="contents">
<tr>
<th>名前</th>
<th>英語訳</th>
<th>表記例</th>
<th>用途</th>
</tr>
<tr v-for="content in contents" v-bind:key="content.name">
<td>{{ content.name }}</td>
<td>{{ content.english }}</td>
<td>{{ content.sample }}</td>
<td>{{ content.usage }}</td>
</tr>
</table>
</div>
</template>
<style scoped>
.contents {
border: 1px solid gray;
}
.contents th,
.contents td {
border: 1px solid gray;
}
</style>
<script>
export default {
props: {
contents: Array
}
};
</script>
再び bin/webpack-dev-server
を実行し直し、 http://localhost:3000/integer_literal_descriptions/index にアクセスすると、下記画像のようなページが表示されるかと思います。
これで API を叩いて取得したデータを受け取り、表示するためのコンポーネントが完成しました。
次は、先ほど作成したコンポーネントに、データを渡す処理を実装していこうと思います。
APIエンドポイントの実装
$ bundle exec rails g controller api/v1/integer_literal_descriptions index --no-helper --no-assets --no-view-specs
コントローラーの実装
class Api::V1::IntegerLiteralDescriptionsController < ApplicationController
def index
+ title = 'IntegerLiteralDescriptions#index'
+ description = '整数リテラルの分類表'
+ contents = get_integer_literals
+ result_values = {
+ title: title,
+ description: description,
+ contents: contents
+ }
+ render json: result_values
end
-end
+
+ private
+
+ def get_integer_literals
+ [
+ {
+ name: '10進数',
+ english: 'decimal',
+ sample: '42',
+ usage: '数値'
+ },
+ {
+ name: '2進数',
+ english: 'binary digits',
+ sample: '0b0001',
+ usage: 'ビット演算など'
+ },
+ {
+ name: '8進数',
+ english: 'octal',
+ sample: '0o777',
+ usage: 'ファイルのパーミッションなど'
+ },
+ {
+ name: '16進数',
+ english: 'hexadecimal, hex',
+ sample: '0xEEFF',
+ usage: '文字のコードポイント、RGB値など'
+ }
+ ]
+ end
+end
アクセスの確認
この状態で、 http://localhost:3000/api/v1/integer_literal_descriptions/index にアクセスしてみると、下記のような画面が表示されるはずです。
一旦これで、 API のエンドポイントが完成しました。
コンポーネントから API を叩いてデータを取得
コンポーネントと API の Ajax 通信は axios というライブラリを使用して行います。4
まずは axios
をインストールします。
$ yarn add --dev axios
install に成功していると、 package.json
axios
の項目が追記されているはずです。
"devDependencies": {
+ "axios": "^0.19.0",
"webpack-dev-server": "^3.7.2"
}
次に integerLiteralDescriptions/Index.vue
を書き換えて、 axios でのデータ取得を実装します。
import Contents from "./Contents.vue";
+import Axios from "axios";
export default {
components: {
"header-view": HeaderView,
contents: Contents
},
data: function() {
return {
title: "title",
description: "description",
contents: []
};
+ },
+
+ created: function() {
+ this.updateContents();
+ },
+
+ methods: {
+ updateContents() {
+ Axios.get("/api/v1/integer_literal_descriptions/index.json").then(
+ response => {
+ const responseData = response.data;
+ console.log(responseData);
+ }
+ );
+ }
}
created
で vue コンポーネントが作成されたタイミングで axios
によるデータ取得を走らせるようにしています。
console.log
で取得したデータを表示するようにしているので、 http://localhost:3000/integer_literal_descriptions/index を見てみましょう。
↓のようなログが出て、データ取得ができているはずです。
Tips: JS のデバッグ
ご存知な方も多いと思いますが、 JS では debugger を仕込むことで、デバッグ実行が可能になります。
debugger ステートメント | MDN
具体的には下記のように仕込むことができます。
Axios.get("/api/v1/integer_literal_descriptions/index.json").then(
response => {
const responseData = response.data;
- console.log(responseData);
+ debugger;
}
);
開発者ツールを開きながら、再度先ほどの http://localhost:3000/integer_literal_descriptions/index にアクセスしてみると、
debugger
を仕込んだ部分で処理が止まり、 Console
からその時点の各種データを覗くことができます。(↑画像の場合、画像下部で responseData
の値を確認しています。)
取得したデータをコンポーネント内で描画
先ほど axios
で取得したデータを、画面に反映させます。
今回の場合、下記の部分を書き換えるだけです。
Axios.get("/api/v1/integer_literal_descriptions/index.json").then(
response => {
const responseData = response.data;
- console.log(responseData);
+ this.title = responseData.title;
+ this.description = responseData.description;
+ this.contents = responseData.contents;
}
);
http://localhost:3000/integer_literal_descriptions/index にアクセスしてみると、整数リテラル分類表が出てくるはず!
整数リテラル分類表
JS では 0 で始まる数値の直後に b, o, x
をつけると、それぞれ2進数、8進数、16進数が表現できるみたいですね。 b, o, x
は 英語訳を見てみると、それぞれ binary digits, octal, hex となっており、なるほどなーとなるんじゃないでしょうか。今回整数リテラル分類表をサンプルに組み込んだ理由は特にないです。気まぐれです。
リンクボタンを追加
せっかくなので、 http://localhost:3000/home/index
から http://localhost:3000/integer_literal_descriptions/index
に飛べるように、リンクボタンを追加しておきましょう。
url: 'https://github.com/t0yohei/rails-vue-app'
}
],
+ inner_links: [{
+ label: '整数リテラル分類表',
+ url: url_for(action: 'index', controller: 'integer_literal_descriptions')
+ }]
}
...
</td>
</tr>
</table>
+ <div v-for="inner_link in contents.inner_links" v-bind:key="inner_link.label">
+ <button v-on:click="changeLocation(inner_link.url)" class="btn-push">{{ inner_link.label }}</button>
+ </div>
</div>
</template>
<style scoped>
...
+.btn-push {
+ margin: 10px;
+ max-width: 180px;
+ text-align: left;
+ background-color: rgb(24, 174, 238);
+ font-size: 14px;
+ color: #fff;
+ text-decoration: none;
+ font-weight: bold;
+ padding: 10px 24px;
+ border-radius: 4px;
+ border-bottom: 4px solid rgb(24, 174, 238);
+}
+.btn-push:active {
+ transform: translateY(4px);
+ border-bottom: none;
+}
</style>
<script>
export default {
props: {
contents: {
type: Object,
default: () => {}
}
+ },
+ methods: {
+ changeLocation(url) {
+ window.location.href = url;
+ }
}
};
</script>
http://localhost:3000/home/index を開いて、こんな感じのボタンができていたらokです。
最後に
今回のチュートリアルはここで終了です。 会社の人が Vue.js を触り始める時に使ってもらえたらなーと思いこのチュートリアルの作成を計画したのですが、せっかくなので Qiita に投稿してみることにしました。どこかのエンジニアの役に立つと幸いです。
-
今回の実装では CoffeeScript と turbolinks のセットアップを省いています。既存の rails プロジェクトで Vue.js を使用する場合は、 turbolinks と戦う必要がありそうです。 ↩
-
もし既存の rails プロジェクトに途中から導入する場合は、右記をご参考ください。rails/webpacker#installation ↩