はじめに
Vue.jsにはHTML/CSS/Javascriptを単一のファイルにまとめることのできる、コンポーネント機能があります。
しかし、コンポーネントを使用するためにはトランスパイラが必要となり、Rails標準のアセットパイプラインでは処理できません。
そのため、RailsにWebpackを導入することになります。
Webpackの導入方法は多くありますが、極力Railsに手を入れない方法で導入してみようと思います。
動作環境
動作に必要なパッケージ(カッコ内は確認したバージョン)
これらのインストールについては本記事では割愛します。
(確認日:2019/7/17)
- Linux (Debian Stretch)
- Ruby (2.5.5)
- npm (6.4.1)
- Bundler (2.0.1)
- MariaDB (mysql Ver 15.1 Distrib 10.1.38-MariaDB)(Railsが対応していれば別のDBでも可)
この記事でgemやnpmから導入するパッケージの動作確認バージョンは以下になります。
- Ruby on Rails (5.2.3)
- Webpack (4.35.0)
- Vue.js (2.6.10)
- axios (0.19.0)
#参考資料
基本的に以下の2つの記事をまとめた形になります。
この他、基礎から学ぶ Vue.js
(所謂猫本)を参考にしています。
#開発方針
- Sprocketsは残したままWebpackを導入する
- Railsプロジェクトに
/frontend
ディレクトリを作成し、Webpackを導入する - Webpackは
/app/assets/javascript/webpack.js
にスクリプトを出力する。 - **Webpackで処理したファイルをSprocketsで再度処理する。**そういった不合理さは気にしない人向け。
- jQueryはwebpackで管理せず、Rails側で用意するかCDNを用いるかのどちらかとする(本記事ではjQueryを使用していない)
/frontend
以下のディレクトリ構成
/frontend
|- config
| |- production
| | |- webpack.config.js
| |- development
| |- webpack.config.js
|- node_modules
|- src
| |- javascripts
| |- components
| | |- vuesample.vue
| | |- vuesampleItem.vue
| |- entry.js
|- package.json
#Railsのインストール
project_nameというRailsプロジェクトを作成するとします。
mkdir project_name
cd project_name
bundle init
bundle init
でGemfileを作成します。
今回は5.2系最新版をインストールします。
今後Rails6以降を使いたい場合はバージョンを変更します。
gem "rails" , "~> 5.2"
vender/bundle
以下にRailsをインストールします。
bundle install --path=vendor/bundle
rails new
します。Turbolinksは無効にします。
DBはMySQL(MariaDB)を指定していますが、任意のDBで問題ありません。
-Bオプションを指定しているため、bundleは行われません。
実行すると、上書きするかを聞かれるので上書きします。
bundle exec rails new . -B -d mysql --skip-turbolinks
Gemfileを編集してpry
をインストールします。
group :development do
# ↓追加
gem 'pry-byebug'
end
bundle update
でインストールします。
#Webpackを導入する
##Webpackなどパッケージのインストール
Railsのルートディレクトリに/frontend
ディレクトリを作成し、Webpackをインストールします。
mkdir frontend
cd frontend
package.json
を作成します。
npm init
色々質問されますが、デフォルト値で構いません。
質問が終わったら、必要なパッケージをインストールします。
npm i --save vue webpack webpack-cli vue-loader vue-template-compiler css-loader style-loader babel-loader @babel/core @babel/preset-env sass-loader node-sass
##webpackの設定ファイルを作成
/frontend/config/development/webpack.config.js
/frontend/config/production/webpack.config.js
を作成します。
mkdir config
mkdir config/development
mkdir config/production
touch config/development/webpack.config.js
touch config/production/webpack.config.js
今回はWebpack4用の設定ファイルを作ります。
Webpackの設定ファイルはバージョンにより異なるため注意が必要です。
Webpackの出力先を/app/assets/javascripts
にするためには絶対パスでの指定が必要です。
/home/vagrant/projects/project_name/app/assets/javascripts
の部分は開発環境のパスに書き換える必要があります。
devtool: 'inline-source-map'
を指定することで、ブラウザからのデバッグが可能になります。
const VueLoaderPlugin = require('vue-loader/lib/plugin');
module.exports = {
devtool: 'inline-source-map', //デバッグに必要
mode: 'development', // Webpack4では必須
entry: {
webpack: './src/javascripts/entry.js' //Keyがファイル名になる
},
output: {
// /app/assets以下に出力(フルパスで記述)
// ここを書き換え
path: '/home/vagrant/projects/project_name/app/assets/javascripts',
filename: '[name].js'
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader'
},
{
test: /\.vue$/,
loader: 'vue-loader'
},
{
test: /\.css$/,
use: ['vue-style-loader', 'css-loader'] // css-loader -> vue-style-loaderの順で通していく
},
{
test: /\.scss$/,
use: [
'vue-style-loader',
'css-loader',
{
loader: 'sass-loader',
},
],
}
]
},
resolve: {
// import './foo.vue' の代わりに import './foo' と書けるようになる(拡張子省略)
extensions: ['.js', '.vue'],
alias: {
// vue-template-compilerに読ませてコンパイルするために必要
vue$: 'vue/dist/vue.esm.js',
},
},
plugins: [
new VueLoaderPlugin()
],
// jQueryはRails側で用意するか、CDNを使用
// (CoffeeScriptからjQueryを呼び出す可能性を想定)
// 記事内ではjQueryを使用していない。読み込まなくてもエラーにならない。
externals: {
jquery: 'jQuery'
}
}
production環境用のconfigも同様に作ります。
-
mode: 'production'
を指定してコードを圧縮すること -
devtool: 'inline-source-map'
を削除していること - production環境でのアセットプリコンパイルを通すため、ES6以降のJavaScriptをES5に変換するように指定すること
が、developmentとの違いになります。
const VueLoaderPlugin = require('vue-loader/lib/plugin');
module.exports = {
mode: 'production', // Webpack4では必須
entry: {
webpack: './src/javascripts/entry.js' //Keyがファイル名になる
},
output: {
// /app/assets以下に出力(フルパスで記述)
// ここを書き換え
path: '/home/vagrant/projects/project_name/app/assets/javascripts',
filename: '[name].js'
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader',
options: {
presets: [
// ES5に変換するように指定
"@babel/preset-env"
]
}
},
{
test: /\.vue$/,
loader: 'vue-loader'
},
{
test: /\.css$/,
use: ['vue-style-loader', 'css-loader'] // css-loader -> vue-style-loaderの順で通していく
},
{
test: /\.scss$/,
use: [
'vue-style-loader',
'css-loader',
{
loader: 'sass-loader',
},
],
}
]
},
resolve: {
// import './foo.vue' の代わりに import './foo' と書けるようになる(拡張子省略)
extensions: ['.js', '.vue'],
alias: {
// vue-template-compilerに読ませてコンパイルするために必要
vue$: 'vue/dist/vue.esm.js',
},
},
plugins: [
new VueLoaderPlugin()
],
// jQueryはRails側で用意するか、CDNを使用
externals: {
jquery: 'jQuery'
}
}
package.json
を編集して、npm run
から実行できるようにします。
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
// ↓下の3つを追加
"release": "webpack --config config/production/webpack.config.js",
"build": "webpack --config config/development/webpack.config.js",
"watch": "webpack --watch --config config/development/webpack.config.js"
},
npm run build
でdevelopment環境、npm run release
でproduction環境での処理が行われます。
Railsに組み込む
Railsで動作確認をするため、Railsのルートディレクトリに移動して適当なコントローラを作ります。
cd ..
bundle exec rails g controller samples
def index
end
Rails.application.routes.draw do
# ↓追加
resources :samples
end
<div id="app">
<p>{{name}}</p>
</div>
import Vue from 'vue';
document.addEventListener("DOMContentLoaded", function(event) {
new Vue({
el: '#app',
data: {
name: 'aaa'
}
});
});
npm run build
やnpm run release
を実行して、
/app/assets/webpack.js
を生成します。
DBのパスワードの設定
このままRailsを起動するとDBに接続できずにエラーが起きるのでパスワードを設定します。
/config/database.yml
を編集してDBのユーザ名とパスワードを設定します。
default: &default
adapter: mysql2
encoding: utf8
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
username: yourusername #ユーザ名を入力
password: yourpassword #パスワードを入力
socket: /var/run/mysqld/mysqld.sock
パスワードを指定したら、DBを作成します。
bundle exec rails db:create
DBの作成が成功したらrails s
でサーバーを起動。
もしサーバーのアドレスがlocalhostでない場合、IPアドレスを-bオプションで指定しましょう。
bundle exec rails s -b サーバーのIPアドレス
/sample
にアクセスして、Vueで指定したnameが表示されていれば、成功です。
#コンポーネントを導入する
Vue.jsの導入に成功したので、次はコンポーネントを導入します。
entry.js
をできるだけ汚したくないため、親となるコンポーネントvueSample.vue
を呼び出し、親コンポーネントからvueSampleItem.vue
を子コンポーネントとして呼び出します。
データは親コンポーネントで保持し、データが変更されると自動的に子コンポーネントに反映されるように実装します。
<template>
<div>
<div>{{message}}</div>
<div>
<!-- samplesに格納された配列を元に表示する -->
<!-- idとnameを子コンポーネントに渡す -->
<!-- keyはあったほうがいいと思われる -->
<vue-sample-item v-for="sample in samples"
v-bind:key="sample.id"
v-bind:name="sample.name"
v-bind:id="sample.id"
>
</vue-sample-item>
</div>
</div>
</template>
<script>
import vueSampleItem from './vueSampleItem';
export default {
name: 'vueSample',
data: function(){
return {
//samplesがデータを入れる配列
//この配列を編集すると自動的に表示に反映される
samples: [
{
id: 1,
name: 'sample1'
},
{
id: 2,
name: 'sample2'
},
{
id: 3,
name: 'Sample3'
}
],
//表示したいメッセージ
message: 'Message'
}
},
//子コンポーネントを指定する
components: {
'vue-sample-item': vueSampleItem
}
}
</script>
<template>
<div class="vuesampleItem" v-bind:class="{active: isActive}">
<p v-on:click="toggleActive();">
{{name}}
</p>
</div>
</template>
<script>
export default {
name: 'vueSampleItem',
//親コンポーネントから受け取るプロパティ
//型はできるだけ指定した方がよいとされる
props: {
id: Number,
name: String
},
data: function(){
return {
isActive: false
}
},
methods: {
toggleActive: function(){
this.isActive = !this.isActive;
},
}
}
</script>
<style lang="scss" scoped>
.vuesampleItem {
border: 1px dashed #abcdef;
margin: 4px 0;
&.active {
border-left: 10px solid #abcdef;
}
&:hover {
background-color: #fafafa;
}
p {
margin: 0;
padding: 4px;
}
}
</style>
コンポーネントごとに、HTML/CSS/JavaScriptをまとめて記述できました。
コンポーネントの詳しい解説は、基礎から学ぶ Vue.js
などの資料を参考してもらえればと思います。
(コンポーネントの解説だけで大きな分量になります。)
コンポーネントを読み込むため、entry.js
を修正します。
import Vue from 'vue';
//↓追加
import vueSample from './components/vueSample';
document.addEventListener("DOMContentLoaded", function(event) {
new Vue({
el: '#app',
data: {
name: 'aaa',
},
//↓下を追加。上のカンマも忘れずに。
components: {
'vue-sample': vueSample
}
});
});
最後にビューを修正して親コンポーネントを配置します。
<div id="app">
<%# 親となるコンポーネントを指定する %>
<vue-sample></vue-sample>
</div>
npm run build
で更新します。
成功していれば、samplesに格納した配列の通りに表示されているはずです。
名前をクリックすると、class
を切り替えることができます。
Railsと通信してDBと連携する
ここまでは配列に直接入力したデータを参照していました。
次は、Railsと通信してDBのデータを取得します。
データ一覧の表示、データの作成、更新、削除の機能を実装していきます。
##DBの設定
Railsのモデルを作成します。
bundle exec rails g model sample
成功するとメッセージが表示されます。
invoke active_record
create db/migrate/20190727062002_create_samples.rb
create app/models/sample.rb
invoke test_unit
create test/models/sample_test.rb
create test/fixtures/samples.yml
/db/migrate/
以下に新しいマイグレーションファイルができているので、編集します。
名前を保存するため、string
型でname
カラムを追加します。
class CreateSamples < ActiveRecord::Migration[5.2]
def change
create_table :samples do |t|
t.timestamps
# ↓追加
t.string :name, default: ""
end
end
end
成功したら、name
カラムを追加するためマイグレーションを追加します。
bundle exec rails db:migrate
成功するとname
カラムが追加されます。
##DBの情報を取得するためのアクションを追加
Rails側からDBの情報を送信するためのアクションを作ります。
作成するアクションは下の通りになります
アクション名 | 役割 |
---|---|
index | レコード一覧の表示 |
create | レコードの新規作成 |
update | 指定したidのレコードの更新 |
destroy | 指定したidのレコードを削除 |
all | 全レコードの取得 |
all
以外はresources
から自動生成されます。
ルーティングを変更するため、routes.rb
を編集します。
Rails.application.routes.draw do
# For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
resources :samples, only: [:index, :create, :update, :destroy] do
collection do
get "/all", to: "samples#all"
end
end
end
次に、コントローラを編集します。
all
create
update
の各メソッドは、id
カラムとname
カラムの情報を連想配列で返します。
destroy
メソッドはid
カラムのみを返します。
失敗した場合、例外を起こす(update!などのメソッドを使用する)ようにして、失敗をクライアント側で検出できるようにしました。
(失敗した場合、Internal Server Errorになります)
getsample
メソッドは、インスタンス変数@sample
に指定したid
を自動的に格納します。
sample_to_hash
メソッドは、取得したデータから必要な情報(id
とname
)を抜き出すメソッドになります。
class SamplesController < ApplicationController
before_action :getsample, only: [:update, :destroy]
def index
end
def all
samples = Sample.all
result = []
samples.each do |sample|
hash = sample_to_hash(sample)
result << hash
end
render :json => result
end
def create
name = params[:name]
sample = Sample.new({
name: name,
})
sample.save!
result = sample_to_hash(sample)
render :json => result
end
def update
@sample.update!({
name: params[:name],
})
result = sample_to_hash(@sample)
render :json => result
end
def destroy
@sample.destroy!
result = {
id: params[:id],
}
render :json => result
end
private
def getsample
@sample = Sample.find(params[:id])
end
def sample_to_hash(sample)
hash = {}
hash[:id] = sample.id
hash[:name] = sample.name
return hash
end
end
コンポーネントからRailsに通信する
###axiosのインストール
Rails側の準備ができたので、コンポーネントから通信してデータを読み込みましょう。
Vue.jsからの通信クライアントはaxios
が使われることが多いようです。
axiosをインストールします。
cd ./frontend
npm i --save axios
###一覧の表示と、レコードの追加
最初に、レコードの一覧表示と新規追加を作ります。
レコードの一覧表示は、samples
の配列にデータをロードすることで自動的に作成できます。
新規レコードを追加するため、一覧ページの一番上にテキストフィールドと送信ボタンを配置します。
テキストフィールドに名前を入力し、ボタンを押すとpostで送信します。
テキストフィールドの内容を、newname
にバインディングし、送信ボタンがクリックされるとsendCreate()
メソッドが呼び出されるようにします。
sendCreate()
メソッドはnewname
を参照してpostを送信します。
<template>
<div>
<div>{{message}}</div>
<!-- ↓下のdivタグを追加 -->
<!-- 新規作成フォーム -->
<div>
<label>Name<input v-model="newname"></label>
<button v-on:click="sendCreate();">Create</button>
</div>
<!-- 省略 -->
</div>
</template>
スクリプトも変更します。
axios
を使用するため、インポートします。
railsとの通信のためCSRFトークンを用意する必要があるため、axios
にトークンを設定します。
<script>
import axios from 'axios';
import vueSampleItem from './vueSampleItem';
// set CSRF token
axios.defaults.headers.common = {
'X-Requested-With': 'XMLHttpRequest',
'X-CSRF-TOKEN' : document.querySelector('meta[name="csrf-token"]').getAttribute('content')
};
// 省略
</script>
次にコンポーネント本体のスクリプトを修正します。
配列samples
に入力されていたデータを全て削除します。
変数newname
を追加します。
メソッドを追加します。
メソッド名 | 概要 |
---|---|
getAllSamples() | 全データの取得をするメソッド |
sendCreate() | レコードの新規作成のため、名前をRailsに送るメソッド |
getLocation() | 必ず最後に/が付くように自ページのURLを取得する |
<script>
//省略
export default {
name: 'vueSample',
data: function(){
return {
//内容を空にする
samples: [],
//newnameを追加
newname: '',
message: 'Message'
}
},
methods: {
getAllSamples: function(){
axios.get( this.getLocation() + 'all' )
//成功したときのコールバック
.then( function(response){
this.samples = response.data;
this.message = '取得に成功しました。'
}.bind(this) )
//失敗したときのコールバック
.catch( () => this.message = '取得に失敗しました。');
},
sendCreate: function(){
axios.post( this.getLocation(), {
name: this.newname
} )
.then( function(response){
//成功したらsamplesに項目を追加
this.samples.push(response.data);
this.message = '追加に成功しました。'
}.bind(this))
.catch( () => this.message = '追加に失敗しました。');
//テキストフィールドを空にする
this.newname = '';
},
getLocation: function(){
let result = location.href;
//最後に/がついていなかったら追加する
if( !result.match(/\/$/) ){
result += '/'
}
return result;
}
},
//コンポーネントがマウントされると、getAllSamples()が呼ばれる
mounted: function(){
this.getAllSamples();
},
components: {
'vue-sample-item': vueSampleItem
}
}
</script>
完成したら、npm run build
を実行してwebpack.js
を更新します。
テキストフィールドに名前を入力して送信すると、要素が追加されるはずです。
###データの編集
次にデータの編集ができるようにしましょう。
sendUpdate
メソッドを作り、patchで更新します。
<script>
//省略
methods: {
//↓下を追加
sendUpdate: function(_id, _name){
axios.patch( this.getLocation() + _id, {
id: _id,
name: _name
} )
.then(function(response){
this.message = '更新に成功しました。';
}.bind(this))
.catch( () => this.message = '更新に失敗しました。');
// nameを書き換え
// findメソッドで配列内を検索し、idが一致した要素の名前を変更する
let target = this.samples.find( item => item.id == _id );
target.name = _name;
},
//省略
}
</script>
メソッドを追加しました。
しかし、これだけだとメソッドは実行されません。
子コンポーネントに更新のためのフォームを作り、メソッドを実行しないといけません。
vueSampleItem.vue
を修正します。
テキストフィールドを作り、算出パラメータmyName
とバインディングします。
name
は親コンポーネントのデータなので、変更してはいけません。
代わりにmyName
が変更された際には親コンポーネントのsendUpdate
メソッドを実行するように設定します。
sendUpdate
メソッドは親コンポーネントのデータを変更することができます。
myName
はdata
に追加せず、算出パラメータとして設定します。
myName
にデータが書き込まれる際に、sample-update
イベントを自分自身に発生させます。
sample-update
イベントが発生した際に、親コンポーネントのsendUpdate
メソッドが実行されるように設定します。
子コンポーネントからsample-update
イベントを起こすためには、$emit
メソッドを使用します。
<template>
<div class="vuesampleItem" v-bind:class="{active: isActive}">
<p v-on:click="toggleActive();">
{{name}}
</p>
<!-- ↓下のpタグを追加 -->
<p>
<input v-model="myName">
</p>
</div>
</template>
<script>
export default {
name: 'vueSampleItem',
props: {
id: Number,
name: String
},
data: function(){
return {
isActive: false
}
},
//computedを追加
//myNameは算出パラメータとして設定
computed: {
myName: {
//読み取りはnameを参照
get: function(){
return this.name
},
//書き込みはsample-updateイベントを自分自身に送信する。
//sample-updateイベントが発生すると、
//親コンポーネントのsendUpdateメソッドが発生するように設定する
set: function(val){
if (this.name !== val){
this.$emit('sample-update', this.id, val);
}
}
}
},
//ここまで追加
methods: {
toggleActive: function(){
this.isActive = !this.isActive;
},
}
}
</script>
sample-update
イベントが発生した際に、sendUpdate
メソッドが呼び出されるように親コンポーネントを設定します。
<template>
<!-- 省略 -->
<!-- v-on:sample-updateイベントが発生すると、sendUpdateメソッドが呼び出される -->
<vue-sample-item v-for="sample in samples"
v-bind:key="sample.id"
v-bind:name="sample.name"
v-bind:id="sample.id"
v-on:sample-update="sendUpdate"
>
</vue-sample-item>
<!-- 省略 -->
</template>
npm run build
でビルドします。
テキストフィールドの内容を変更すると、DBが更新されます。
###レコードの削除
最後にレコードの削除を実装します。
子コンポーネントに削除ボタンを作り、クリックすると削除されるように実装します。
<template>
<!-- 省略 -->
<!-- ↓下を追加 -->
<p>
<button v-on:click="sendDelete();">delete</button>
</p>
<!-- 省略 -->
</template>
sendDelete
メソッドを追加します。
データを更新した時と同じように、sample-delete
イベントを発生させます。
<script>
//省略
methods: {
toggleActive: function(){
this.isActive = !this.isActive;
},
// ↓下を追加
sendDelete: function(){
this.$emit('sample-delete', this.id);
},
}
//省略
</script>
更新の時と同じように親コンポーネントも変更します。
<template>
<!-- 省略 -->
<!-- sample-deleteイベント発生時にsendDelete()メソッドを実行する -->
<vue-sample-item v-for="sample in samples"
v-bind:key="sample.id"
v-bind:name="sample.name"
v-bind:id="sample.id"
v-on:sample-update="sendUpdate"
v-on:sample-delete="sendDelete">
</vue-sample-item>
<!-- 省略 -->
</template>
<script>
// 省略
methods:{
//sendDeleteメソッドを追加
sendDelete: function(_id){
axios.delete( this.getLocation() + _id )
.then(function(response){
this.message = '削除に成功しました。';
}.bind(this) )
.catch( () => this.message = '削除に失敗しました。');
//配列を更新
//idが一致しない要素だけを抜き出して新しい配列を作る
this.samples = this.samples.filter(item => item.id !== _id);
},
},
//省略
</script>
npm run build
で更新します。
成功していれば、削除ボタンでデータを削除することができるはずです。
さいごに
Rails
とVue.js
を組み合わせて、CRUDを実現することができました。
最初にVue.js
を導入しようとしたきっかけはReact
と違いトランスパイルが不要、ということでRailsに組み込みやすいかなあと思ったためでした。
しかし、結局のところ単一ファイルコンポーネントが使いたくなったため、Webpackの導入から始める必要が出てきてしまいました。
Railsのアセットパイプラインは、最近では欠点といわれることが多いようです。
それでもRailsのActiveRecordは評価が高く、DB周りでは今後もRailsが多く使われていくかと思います。
RailsにWebpackを導入してVue.jsを利用することは、Rails初心者には少し敷居が高いかなあと思っています。
しかし、この記事をきっかけにして少しでもVue.jsを触ってみたいと思ってもらえたら、動かしながら学んでもらえたら、嬉しいと思います。
サンプルコードについて
サンプルコードを、GitHubにアップロードしましたので、ご自由にご利用ください。
ただし、利用した結果については、責任を負いかねます。
パッケージについてはインストールされていないので更新が必要です。
DBの設定を変更した後、
bundle install --path=vendor/bundle
cd frontend
npm install
cd ..
bundle exec rails db:create
bundle exec rails db:migrate
といった感じで実行できると思います。