# Vue x Rails

こちら (opens new window)のサイトを参考にチュートリアルを進めています.

# 2019/1/26

# プロジェクトの生成

$ rails new {project_name} --webpack=vue

# Vue.jsの参照

Viewのページで, javascript_pack_tagを使用することで,app/javascript/packs以下にあるJSファイルを探す.
インストール時にapp/javascript/packs/hello_vue.jsというファイルが生成されているので, これをindexアクションにて読み込ませる.

# devサーバの自動コンパイル

# foreman gemのインストール

bin/webpackというコマンドでコンパイルされる.

毎回のコンパイルが面倒なので, 変更を検出して自動コンパイルする.
Introducing Webpacker (opens new window)

foremanのGemをインストールする.
Gemfileファイルにgem 'foreman'を追加してbundle installする.

# bin/serverとProcfileの追記

# bin/server

以下で示すProcfile.devの内容を実行する.

// bin/server
#! /bin/bash -i
bundle install
bundle exec foreman start -f Procfile.dev

$ chmod 777 bin/serverを実行しておかないと, bin/serverがパーミッションのせいで実行できなくなる可能性がある.

# Packfile.dev

rails sbin/webpack-dev-serverを実行する.
ポート指定のために, -b 0.0.0.0オプションをつける.
RailsでポートとIPアドレスを指定する方法 (opens new window).

// Procfile.dev
web: bundle exec rails s -b 0.0.0.0
webpacker: ./bin/webpack-dev-server

# 実践

app/javascript/app.vueのmessageを編集するとすぐにコンパイルされる.

# APIの準備

# テーブルとモデルの作成

テーブル名はtasks, モデル名はTaskとする.

Column Name Type Options
name string presence: true
is_done boolean presence: true, default: false
created_at datetime null: false
updated_at datetime null: false

とりあえず,
$ rails g model Task name:string is_done:boolean.

その後, optionsの内容を以下のようにしてdb/migrate/*.rbに追記.

// db/migrate/*.rb
class CreateTasks < ActiveRecord::Migration[5.1]
  def change
    create_table :tasks do |t|
      t.string :name    , presence: true
      t.boolean :is_done, default: false, presence: true

      t.timestamps
    end
  end
end

ついでに, app/models/task.rbに上で定義したvalidatesの内容を追加.

// app/models/task.rb
class Task < ApplicationRecord
  validates :name, presence: true
end

# APIのルーティングの設定

urlをapi/tasksのようにnamespaceを切る.
そうすることで, api用のルーティングが用意できる.
(具体的には, /api/*のようにnamespaceが区切られる)

// config/routes.rb
Rails.application.routes.draw do
  # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
  root to: 'home#index'
  
  namespace :api, format: 'json' do
    resources :tasks, only: [:index, :create, :update]
  end
end

# コントローラを作成

API用のコントローラを作成する.
controllerの場所は, app/controllers/以下に新しくapiというディレクトリを作成する.
名前は慣習的にtasks_controller.rbとする.

追加するアクションは3つで, 以下の通り.

Action Name REST Overview
index GET /tasks 更新順の@tasksを返す
create POST /tasks 新しく@taskを作ってデータベースに保存
'' '' 保存できない場合, :unprocessable_entityステータスのjsonファイルをrenderする
update PATCH/PUT /tasks/* @taskを更新する
'' '' 更新できなかった場合, :unprocessable_entityステータスのjsonファイルをrenderする

中身を勝手にいじられないように, privatetask_paramsを定義しておく.

# -*- coding: utf-8 -*-
class Api::TasksController < ApplicationController
 
  # GET /tasks
  def index
    # 後々のために, 更新順で返す
    @tasks = Task.order('updated_at DESC')
  end

  # POST /tasks
  def create
    @task = Task.new(task_params)
    
    if @task.save
      render :show, status :created
    else
      render json: @task.errors, status: :unprocessable_entity
    end
  end

  # PATCH/PUT /tasks/*
  def update
    @task = Task.find(params[:id])
    
    if @task.update(task_params)
      render :show, status: :ok
    else
      render json: @task.errors, status: :unprocessable_entity
    end
  end

  private
  def task_params
    params.fetch(:task, {}).permit(
      :name, :is_done
    )
  end
end

# ViewのJSONファイル

app/views/api/tasks/下に, indexshowのための2つのjbuilderファイル(json)を配置.
jbuilderについては, 以下を参照.
Railsのjbuilderの書き方と便利なイディオムやメソッド (opens new window)

// app/views/api/tasks/index.json.jbuilder
// `set!`は, `:tasks`という属性をまとめるメソッド
json.set! :tasks do
  // `array!`は, `@tasks`の内容をループで回すメソッド 
  // 返り値は配列になる
  json.array! @tasks do |task|
    // `extract!`は, `task`の中身を指定して返せるメソッド
    // 以下の場合は, `task`の中身を, `:id`, `:name`, ..., `:updated_at`をまとめて返すことになる
    json.extract! task, :id, :name, :is_done, :created_at, :updated_at
  end
end
// app/views/api/tasks/show.json.jbuilder
json.set! :task do
  json.extract! @task, :id, :name, :is_done, :created_at, :updated_at
end

# seeds.rbを作成し, curlコマンドで確認

初期値データの作成.
今回は未達成Taskを2つ, 達成済みTaskを1つ作る. おなじみのseeds.rbを以下のように設定.

// db/seeds.rb
2.times { Task.create!(name: 'Sample Task') }
1.times { Task.create!(name: 'Done Task', is_done: true) }

コマンドは$ rails db:seed.
リセットする場合は$ rails db:setup

# curlでGETしてみる

$ curl -X GET 0.0.0.0:5000/api/tasks | jqを叩く.

{
  "tasks": [
    {
      "id": 6,
      "name": "Done Task",
      "is_done": true,
      "created_at": "2019-01-26T13:54:22.507Z",
      "updated_at": "2019-01-26T13:54:22.507Z"
    },
    {
      "id": 5,
      "name": "Sample Task",
      "is_done": false,
      "created_at": "2019-01-26T13:54:22.496Z",
      "updated_at": "2019-01-26T13:54:22.496Z"
    },
    {
      "id": 4,
      "name": "Sample Task",
      "is_done": false,
      "created_at": "2019-01-26T13:54:22.486Z",
      "updated_at": "2019-01-26T13:54:22.486Z"
    },
    {
      "id": 3,
      "name": "Done Task",
      "is_done": true,
      "created_at": "2019-01-26T13:54:07.951Z",
      "updated_at": "2019-01-26T13:54:07.951Z"
    },
    {
      "id": 2,
      "name": "Sample Task",
      "is_done": false,
      "created_at": "2019-01-26T13:54:07.939Z",
      "updated_at": "2019-01-26T13:54:07.939Z"
    },
    {
      "id": 1,
      "name": "Sample Task",
      "is_done": false,
      "created_at": "2019-01-26T13:54:07.929Z",
      "updated_at": "2019-01-26T13:54:07.929Z"
    }
  ]
}

なぜか2倍作成されている...
Why...?

あとで検証する. 今は眠いのだ.

# curlでPOSTしてみる

$ curl -X POST 0.0.0.0:5000/api/tasks -d 'task[hoge]=fuga'

ActionController::InvalidAuthenticityTokenが返ってくる.
認証していないのが問題らしい.

# 認証機能の追加

【Rails】RailsでAPIの簡単なトークン認証を実装する (opens new window)
こちらの記事を参考にした.

# 2019/1/27

# 認証機能の追加(つづき)

追加するのは, app/controllers/application_controller.rb.
以下のように追加.

class ApplicationController < ActionController::Base

  include ActionController::HttpAuthentication::Token::ControllerMethods

  before_action :authenticate
  protect_from_forgery with: :exception
  
  protected
  def authenticate
    authenticate_token || render_unauthenticated
  end

  def authenticate_token
    authenticate_with_http_token do |token, options|
      token == 'hoge'
    end
  end
  
  def render_unauthenticated
    # render_errors(:unauthorized, ['invalid token'])
    obj = { message: 'token invalid' }
    render json: obj, status: :unauthorized
  end
  
end

この後,
$ curl -X POST -H 'Content-Type:application/json' -H 'Authorization: Token hoge' 0.0.0.0:5000/api/tasks -d '{ "task"=>{"name":"fuga"} }'
を叩くと,
Can't verify CSRF token authenticityと怒られる.
このtokenだとダメなのか?

# secureな認証付きトークンを用いてPOSTする

Rails5 APIで認証付きのWebAPIを作ってみる (opens new window)

こちらの記事を参考にしてみる.

これユーザ認証だ.

uhmmmmmmmmmm....

後でやります(TODOに投げる)
Do better then perfectなので.

# 最悪の対処法

app/controller/application_controller.rbのCSRF対策のコードをコメントアウト.
許せ...

$ curl -X POST -H 'Content-Type:application/json' 0.0.0.0:5000/api/tasks | jq

{ "task":
  {
    "id": 7,
    "name": "fuga",
    "is_done": false,
    "created_at": "2019-01-27T01:45:10.207Z",
    "updated_at": "2019-01-27T01:45:10.207Z"
  }
}

おkですね(全くおkではない)
後で認証は実装する.

# Materializeの導入

MaterializeというCSSフレームワークを導入する.
Gemを追加してbundle install

その後, Materializeを適応するために以下の2ファイルを変更.

// - app/assets/stylesheets/application.css
// + app/assets/stylesheets/application.scss
/* Materialize */

@import "materialize/components/color-variables";
$primary-color: color("teal", "accent-3") !default;
$secondary-color: color("cyan", "base") !default;
@import 'materialize';
@import 'material_icons';
// app/assets/javascripts/application.js
//= require jquery
//= require jquery.turbolinks
//= require materialize-sprockets
//= require turbolinks
//= require_tree .

# Componentsを活用してヘッダを作成

元のファイルはindex.html.erbなので, そこの<div id="app">にvueの内容がマウントされる.
<navbar>の装飾をvueで行う.

フォルダ構成は以下の通り.

/home/vagrant/rails_work/workspace/vue_todo/app/javascript
|--app.vue
|--packs
| |--application.js
| |--components
| | |--header.vue // headerの情報を提供
| |--todo.js // index.html.erbの`<div id="app">の部分にマウントされる

# Vue-Routerを使用する

目的は, Top, About, Contactページの作成.

# What's Vue-Router?

Vue Router (opens new window) Vue.jsでSPAを作るときに使うルータ.
機能は以下の通り(from 公式)

  • ネストされたルート/ビューマッピング
  • モジュール式、コンポーネントベースのルータ構造
  • ルートパラメータ、クエリ、ワイルドカード
  • Vue.js の transition 機能による、transition エフェクトの表示
  • 細かいナビゲーションコントロール
  • 自動で付与される active CSS クラス
  • HTML5 history モードまたは hash モードと IE9 の互換性
  • カスタマイズ可能なスクロール動作

兎にも角にも実装してみます.

# Vue-Routerの追加

$ yarn add vue-router
vue-routerを追加

# Componentsの作成

# index.vue(Top)

// app/javascript/packs/components/index.vue
<template>
  <div>
    <p>Index</p>
  </div>
</template>

# about.vue(About)

// app/javascript/packs/components/about.vue
<template>
  <div>
    <p>
      This is a sample todo application.<br>
      As I wanna practice vue.js, I've made this app.
    </p>
  </div>
</template>

# contact.vue(Contact)

// app/javascript/packs/components/contact.vue
<template>
  <div>
    <p>
      If you wanna contact me, plz send direct message to below account.<br>
      Twitter: `@task4233`
    </p>
  </div>
</template>

# 作成したComponentsをVue-routerに登録

Vue-routerを使用するために, routerディレクトリを作成してそちらに記述.
mode: 'history'とすることで, HTMLのhistory APIを使用して, 同じビューないでURLを書き換えられる.
HTML Historyモード (opens new window)

ただし, not-foundパスがindex.htmlにリダイレクトされるため, 404ページをNotFountComponentとする.

// app/javascript/packs/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: '/', components: Index },
    { path: '/about', components: About },
    { path: '/contact', components: Contact },
    { path: '*', component: NotFoundComponent }
  ]
})

# Vue-Routerに応じたタグの修正

Vue-Routerを使用すると,以下のようなタグが提供される.

Name Effect
<router-link> <a>タグとして変換されるが,
'' Vue-Routerに登録されたパスからコンポーネントを探す
<router-view> <router-link>で見つけたコンポーネントを表示

要するに, <a>タグを<router-link>に修正する.

その後, app/views/home/index.html.erb<router-view></router-view>を追加.

# Axiosを用いたAPI通信(Ajax)

# What's Axios?

Ajax通信ライブラリらしい.
axios (opens new window)

# Axiosのインストール

$ yarn add axios

# 実践(Axios)

checkboxの書き方は, Materialize 1.0.0から変わったので注意.
詳しくは, 公式 (opens new window)を見よ.

また, 一覧を表示するために, JSを書く.
仕組みは, インスタンスにプロパティとしてTask(array)と, newTask(string)を与え, メソッドでAPIで取得してきた値をループさせてtasksに格納するというもの.

  • methodは, Vueインスタンスがマウントされたタイミングで実行されるライフサイクルフック(詳しくは, ライフサイクルダイアグラム (opens new window)をみるとよい)
  • 一覧表示は, v-for (opens new window)v-if (opens new window)を使ってやればよい(汚くなるけども, まぁ動くので)
  • v-bindで置換
  • 単一コンポーネントにするので, コンパイル時にCSSを出さないようにする
  • 具体的には, config/webpack/environment.jsenvironment.loaders.get('vue').options.extractCSS=falseとすれば良い
  • accomplished tasksは基本的に非表示, 必要な時に表示すればおk.
  • ボタンが押されたとき(v-on (opens new window))に表示すればおk.

# 新規作成フォームの作成

双方向バインディングが可能なv-model (opens new window)を用いる.
<input>タグで入力された値と, インスタンスのnewTaskプロパティとをバインドさせる.

基本的には, scriptmethodを定義して, イベントに対してそれを呼び出すという様式になっている様子.
JSと同じ.

Last Updated: 9ヶ月前