Docker Composeで作った初期装備なRails6にVue.jsを導入する

この記事は

- [Docker Composeを使った初期装備なRails6/PostgreSQL環境の作り方 \- Qiita](https://qiita.com/tanaken0515/items/0e207160c25a6b815561)
- [Docker Composeのvolumesを使ってもっと効率的に \- Qiita](https://qiita.com/tanaken0515/items/a564d2c46b2942b83ebb)

の続編です。

今回の記事では、これまで環境構築をしてきたRailsアプリケーションにVue.jsを導入する方法について記載します。

Rails6の初期装備に従って進めていますので、**turbolinksを使います**し、**Webpackerを使います**。

:warning: turbolinksを使いたくない、または、Webpackerを使いたくない、という方にとっては参考にならない情報である可能性が高いです。また、この一連の記事の「想定読者、方針、前提」については[1つ目の記事](https://qiita.com/tanaken0515/items/0e207160c25a6b815561)の冒頭に記載しているのでご確認ください:pray:

ではやっていきましょう。

## 事前準備

これまでの記事で作ってきたRailsアプリケーションは、アプリケーションのロジック自体にはまだ手を加えていないので、「Yay! You're on Rails!」の画面（以後、Welcome画面と呼びます）しか表示されません。これではこのあとVue.jsを導入するにあたって説明しにくいので、準備として適当なリソースをscaffoldで作っておくことにします。

（すでに何らかの手を加えていてWelcome画面以外が表示されるようになっている場合は、この事前準備はskipしてOKです）

### 現状確認
`docker-compose up`で起動して、現状を確認してみましょう。

```sh
$ docker-compose up
docker-compose-rails-sample_db_1 is up-to-date
Creating docker-compose-rails-sample_web_1 ... done
Attaching to docker-compose-rails-sample_db_1, docker-compose-rails-sample_web_1
db_1   | 
db_1   | PostgreSQL Database directory appears to contain a database; Skipping initialization
db_1   | 
...(略)...
web_1  | => Booting Puma
web_1  | => Rails 6.0.3.1 application starting in development 
web_1  | => Run `rails server --help` for more startup options
web_1  | Puma starting in single mode...
web_1  | * Version 4.3.5 (ruby 2.7.1-p83), codename: Mysterious Traveller
web_1  | * Min threads: 5, max threads: 5
web_1  | * Environment: development
web_1  | * Listening on tcp://0.0.0.0:3000
web_1  | Use Ctrl-C to stop

```

localhost:3000 にアクセスするとWelcome画面が表示されるはずです。

### Usersリソースを作る
今後の説明の準備として、適当なリソースをscaffoldで作っていきます。

まずはWebコンテナの中に入りましょう。

```sh
$ docker-compose exec web bash
root@2a2467ff3a66:/myapp# 
```

コンテナの中でコマンドを実行していきます。
ここでは[第2章 Toyアプリケーション \- Railsチュートリアル](https://railstutorial.jp/chapters/toy_app?version=5.1#cha-a_toy_app)に倣ってUsersリソースを作ることにします。（ほかに作りたいリソースがあればそれでも構いません）

```sh
root@2a2467ff3a66:/myapp# bundle exec rails generate scaffold User name:string email:string
      invoke  active_record
      create    db/migrate/20200613085052_create_users.rb
      create    app/models/user.rb
      invoke    test_unit
      create      test/models/user_test.rb
      create      test/fixtures/users.yml
      invoke  resource_route
       route    resources :users
      invoke  scaffold_controller
      create    app/controllers/users_controller.rb
      invoke    erb
      create      app/views/users
      create      app/views/users/index.html.erb
      create      app/views/users/edit.html.erb
      create      app/views/users/show.html.erb
      create      app/views/users/new.html.erb
      create      app/views/users/_form.html.erb
      invoke    test_unit
      create      test/controllers/users_controller_test.rb
      create      test/system/users_test.rb
      invoke    helper
      create      app/helpers/users_helper.rb
      invoke      test_unit
      invoke    jbuilder
      create      app/views/users/index.json.jbuilder
      create      app/views/users/show.json.jbuilder
      create      app/views/users/_user.json.jbuilder
      invoke  assets
      invoke    scss
      create      app/assets/stylesheets/users.scss
      invoke  scss
      create    app/assets/stylesheets/scaffolds.scss
```

migrationファイルが作られてますね、migrationしましょう。

```sh
root@2a2467ff3a66:/myapp# bundle exec rails db:migrate
== 20200613085052 CreateUsers: migrating ======================================
-- create_table(:users)
   -> 0.0613s
== 20200613085052 CreateUsers: migrated (0.0614s) =============================
```

ついでに、root pathにアクセスが来たらUser一覧画面を表示するようにルーティングを書き換えておきます。

```diff:config/routes.rb
 Rails.application.routes.draw do
   resources :users
-  # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
+
+  root to: 'users#index'
 end
```

### 動作確認

localhost:3000 にアクセスするとUser一覧画面が表示されるはずです。

<img alt="users-index" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/309992/2e1520db-9f92-5ccf-a42f-16cd8fb2b625.png" />


## Vue.jsを動かす

RailsアプリケーションにVue.jsをインストールして動かしてみましょう

### Vue.jsのインストール
`bundle exec rails webpacker:install:vue` で簡単にインストールできます。
（ref: [webpacker/integrations\.md at master · rails/webpacker](https://github.com/rails/webpacker/blob/master/docs/integrations.md#vue)）

```sh
root@2a2467ff3a66:/myapp# bundle exec rails webpacker:install:vue
Copying vue loader to config/webpack/loaders
      create  config/webpack/loaders/vue.js
Adding vue loader plugin to config/webpack/environment.js
      insert  config/webpack/environment.js
      insert  config/webpack/environment.js
Adding vue loader to config/webpack/environment.js
      insert  config/webpack/environment.js
      insert  config/webpack/environment.js
Updating webpack paths to include .vue file extension
      insert  config/webpacker.yml
Copying the example entry file to /myapp/app/javascript/packs
      create  app/javascript/packs/hello_vue.js
Copying Vue app file to /myapp/app/javascript/packs
      create  app/javascript/app.vue
Installing all Vue dependencies
         run  yarn add vue vue-loader vue-template-compiler from "."
yarn add v1.13.0
[1/4] Resolving packages...
[2/4] Fetching packages...
info fsevents@2.1.3: The platform "linux" is incompatible with this module.
info "fsevents@2.1.3" is an optional dependency and failed compatibility check. Excluding it from installation.
info fsevents@1.2.13: The platform "linux" is incompatible with this module.
info "fsevents@1.2.13" is an optional dependency and failed compatibility check. Excluding it from installation.
[3/4] Linking dependencies...
warning " > webpack-dev-server@3.11.0" has unmet peer dependency "webpack@^4.0.0 || ^5.0.0".
warning "webpack-dev-server > webpack-dev-middleware@3.7.2" has unmet peer dependency "webpack@^4.0.0".
warning " > vue-loader@15.9.2" has unmet peer dependency "css-loader@*".
warning " > vue-loader@15.9.2" has unmet peer dependency "webpack@^3.0.0 || ^4.1.0 || ^5.0.0-0".
[4/4] Building fresh packages...
success Saved lockfile.
warning Your current version of Yarn is out of date. The latest version is "1.22.4", while you're on "1.13.0".
success Saved 12 new dependencies.
info Direct dependencies
├─ vue-loader@15.9.2
├─ vue-template-compiler@2.6.11
└─ vue@2.6.11
info All dependencies
├─ @vue/component-compiler-utils@3.1.2
├─ consolidate@0.15.1
├─ de-indent@1.0.2
├─ he@1.2.0
├─ merge-source-map@1.1.0
├─ prettier@1.19.1
├─ vue-hot-reload-api@2.3.4
├─ vue-loader@15.9.2
├─ vue-style-loader@4.1.2
├─ vue-template-compiler@2.6.11
├─ vue-template-es2015-compiler@1.9.1
└─ vue@2.6.11
Done in 25.23s.
Webpacker now supports Vue.js 🎉
```

### hello_vue.jsを動かす

先ほどのコマンドで`app/javascript/packs/hello_vue.js`が作られています。
これはサンプルファイルで、こんな感じでVue.jsを動かせるよ〜的なことが書いてあります。

```app/javascript/packs/hello_vue.js
/* eslint no-console: 0 */
// Run this example by adding <%= javascript_pack_tag 'hello_vue' %> (and
// <%= stylesheet_pack_tag 'hello_vue' %> if you have styles in your component)
// to the head of your layout file,
// like app/views/layouts/application.html.erb.
// All it does is render <div>Hello Vue</div> at the bottom of the page.

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)
})
```

コードコメントを訳すと

「このサンプルはレイアウトファイル（例えば`app/views/layouts/application.html.erb`）のheadに`<%= javascript_pack_tag 'hello_vue' %>`を追記することで実行できるよ。（コンポーネントに独自のstyleを適用したい場合は`<%= stylesheet_pack_tag 'hello_vue' %>`も追記してね）」

ということですね。

コード自体は「画面の読み込み時に、Appコンポーネントの中身をbodyタグに追記する」という処理になっています。

Appコンポーネントの中身はこれで、`id="app"`なdivタグの中に`"Hello Vue!!"`を表示するだけの単純なやつです。

```app/javascript/app.vue
<template>
  <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>
```

では`hello_vue.js`のコードコメントに書かれている通り、レイアウトファイルに`<%= javascript_pack_tag 'hello_vue' %>`を追記してみましょう。

```diff:app/views/layouts/application.html.erb
     <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
     <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
+    <%= javascript_pack_tag 'hello_vue' %>
   </head>
 
   <body>
```

その状態で localhost:3000 にアクセスするとこうなります。
![スクリーンショット 2020-06-13 22.57.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/309992/f8c4459b-5682-5b12-8bbc-cdabc24ac17c.png)

お〜、よさそう？

### 問題発生

一見よさそうに見えるのですが、「New User」のリンクをクリックして**画面遷移すると`"Hello Vue!!"`の表示が消えてしまいます**。
その状態で**画面をリロードすると`"Hello Vue!!"`の表示が出てきます**。
不可解な挙動になっていますね。

それもそのはず、Rails6の初期装備では`turbolinks`というライブラリが有効になっており、無駄な画面の読み込みをなくし、高速に表示できるようになっているのです。
（ref: [turbolinks/turbolinks: Turbolinks makes navigating your web application faster](https://github.com/turbolinks/turbolinks)）

`hello_vue.js`の実装に戻ってみると

```app/javascript/packs/hello_vue.js
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)
})
```
`document.addEventListener('DOMContentLoaded', ...`を使っていますね。
これは画面を読み込んだ時だけ走るので、画面をリロードすると`"Hello Vue!!"`の表示が出てくる、という挙動になっています。

この問題を解消するには、turbolinksの仕組みに合わせた実装にする必要がありそうです。

### vue-turbolinksを使う

実は`hello_vue.js`の下の方にこのような記載があります。

```app/javascript/packs/hello_vue.js
// If the project is using turbolinks, install 'vue-turbolinks':
//
// yarn add vue-turbolinks
//
// Then uncomment the code block below:
//
// import TurbolinksAdapter from 'vue-turbolinks'
// import Vue from 'vue/dist/vue.esm'
// import App from '../app.vue'
//
// Vue.use(TurbolinksAdapter)
//
// document.addEventListener('turbolinks:load', () => {
//   const app = new Vue({
//     el: '#hello',
//     data: () => {
//       return {
//         message: "Can you say hello?"
//       }
//     },
//     components: { App }
//   })
// })
```

「もしturbolinksを使ってるなら、`vue-turbolinks`をインストールしよう。`yarn add vue-turbolinks`だ。そしてこの下のコードを有効化してくれい」

なるほど。よし、`vue-turbolinks`を入れましょう。

```sh
root@2a2467ff3a66:/myapp# yarn add vue-turbolinks
```

コメントアウトされているサンプルコードを参考に、`hello_vue.js`を書き換えてみます。

```app/javascript/packs/hello_vue.js
import TurbolinksAdapter from 'vue-turbolinks'
import Vue from 'vue'
import App from '../app.vue'

Vue.use(TurbolinksAdapter)

document.addEventListener('turbolinks:load', () => {
  const app = new Vue({
    render: h => h(App)
  }).$mount()
  document.body.appendChild(app.$el)

  console.log(app)
})
```

`TurbolinksAdapter`をimportして`Vue.use`し、イベントリスナーを`'turbolinks:load'`に書き換えただけです。

この状態で localhost:3000 にアクセスして、ポチポチと動作確認をしてみましょう。
画面をリロードしなくても、常に`"Hello Vue!!"`が表示されるようになっていますね！:tada::tada:

## User画面をVue.jsで書き換えてみる

ここまででVue.jsの動作確認をすることができました。

あとは[Vue.jsのガイド](https://jp.vuejs.org/v2/guide/)等を読みながら、必要に応じてVue.jsを使っていただければOKです。

この記事では一例として`users#show`で表示される画面をVue.jsに書き換えてみます。
＊あくまで一連の流れを示すための例で、コンポーネントの切り方やディレクトリ構成は雑です。というかあまりフロントエンドに詳しくないので正直なところ雰囲気でやっています。「こうしたほうがいいよ〜」等あればコメントください！

### 既存の画面の確認

現時点で`users#show`の画面はこうなっており、
![スクリーンショット 2020-06-18 10.19.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/309992/590fb9b3-6888-c2f5-ab88-6fb47517ee83.png)

view_templateはこうなっています。

```app/views/users/show.html.erb
<p id="notice"><%= notice %></p>

<p>
  <strong>Name:</strong>
  <%= @user.name %>
</p>

<p>
  <strong>Email:</strong>
  <%= @user.email %>
</p>

<%= link_to 'Edit', edit_user_path(@user) %> |
<%= link_to 'Back', users_path %>
```

5つのデータ（`notice`, `@user.name`, `@user.email`, `edit_user_path(@user)`, `users_path`）を表示に使っていますね。
これらのデータをjsで処理してVueコンポーネントに渡して表示する、という一連の流れを実装していきます。

### 書き換えていく

まずはview_templateをこのように書き換えましょう。

```app/views/users/show.html.erb
<div
  class="js-users-show"
  data-notice="<%= notice %>"
  data-name="<%= @user.name %>"
  data-email="<%= @user.email %>"
  data-edit-user-path="<%= edit_user_path(@user) %>"
  data-users-path="<%= users_path %>"
></div>
```

`js-users-show`クラスのdivタグに、data属性として先ほどの5つのデータを持たせました。

続いてVueコンポーネントを実装します。
今回は`app/javascript`の下に`components/users`ディレクトリを作り、`UsersShow.vue`というコンポーネントを作ることにします。

```app/javascript/components/users/UsersShow.vue
<template>
  <div>
    <p id="notice">{{ notice }}</p>

    <p>
      <strong>Name:</strong>
      {{ name }}
    </p>

    <p>
      <strong>Email:</strong>
      {{ email }}
    </p>

    <a :href="editUserPath">Edit</a> |
    <a :href="usersPath">Back</a>
  </div>
</template>

<script>
export default {
  name: 'UsersShow',
  props: {
    notice: {
      type: String,
      required: true
    },
    name: {
      type: String,
      required: true
    },
    email: {
      type: String,
      required: true
    },
    editUserPath: {
      type: String,
      required: true
    },
    usersPath: {
      type: String,
      required: true
    }
  }
}
</script>

<style scoped>
</style>
```

5つのデータをプロパティとして受け取り、`<templete>`にはもともとのview_templateにあった内容をコピーしてきて、それぞれのプロパティをそれぞれの位置に配置しているだけです。

続いて、view_templateからVueコンポーネントにデータを受け渡す処理を書いていきます。`packs`配下に`users.js`を実装します。

```app/javascript/packs/users.js
/* eslint no-console: 0 */

import TurbolinksAdapter from 'vue-turbolinks'
import Vue from 'vue'
import UsersShow from '../components/users/UsersShow.vue'

Vue.use(TurbolinksAdapter)

const mountUsersShow = () => {
  const el = document.querySelector('.js-users-show')

  if (el == null) {
    return
  }

  const { notice, name, email, editUserPath, usersPath } = el.dataset

  const props = {
    notice: notice,
    name: name,
    email: email,
    editUserPath: editUserPath,
    usersPath: usersPath
  }

  new Vue({
    el: el,
    render: h => h(UsersShow, { props })
  }).$mount()
}

document.addEventListener('turbolinks:load', () => {
  mountUsersShow()
})
```

`js-users-show`クラスの要素を取得して、その要素のdata属性を`props`としてUsersShowコンポーネントに渡し、`js-users-show`クラスの要素をマウントする、という実装になっています。


最後に、レイアウトファイルに`<%= javascript_pack_tag 'users' %>`を追記したら完了です。

```diff:app/views/layouts/application.html.erb 
     <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
     <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
     <%= javascript_pack_tag 'hello_vue' %>
+    <%= javascript_pack_tag 'users' %>
   </head>
 
   <body>
```

画面の見た目は最初とまったく変わらないはずです。

### 余談

この例では`users#show`の画面を書き換えてみましたが、実際にはこの画面のように「動的なDOMの切り替えがない画面」をVue.jsに書き換えるメリットはあまりないかもしれません。（Vue.js用のコンポーネントライブラリを使ってデザインをあてたい、とか、静的な画面についてもjsでテスト（コンポーネントの単体テストなど）を書いておきたい、などのシーンではメリットがあるかもですが）

やはりメリットがあるのは、例えばユーザの画面クリックに応じて表示するコンポーネントを切り替えたり、フォームへの入力内容に応じてテキストを表示したりとか、そういった動的なDOMの切り替えが必要な画面かなぁと思います。

今回、はじめは入力フォーム`app/views/users/_form.html.erb`を書き換えようかと思ったのですが、これをやるとCSRFトークンなどの話も含まないといけなくなり、全体の流れが伝わりにくくなりそうな予感がしたのでやめました。また改めて書こうかと思います。

## まとめ

今回の記事では、これまでの記事で環境構築をしてきたRailsアプリケーションに、Vue.jsを導入する方法について記載しました。参考になれば幸いです。

次の記事はどうしようか迷っていますが、開発中にjsをいじった後に画面にアクセスすると、コンパイルが走って読み込みに時間が掛かるのがつらいので、webpack-dev-serverの設定について書こうかなと思います。予定は未定です。

- [Docker Composeを使った初期装備なRails6/PostgreSQL環境の作り方 \- Qiita](https://qiita.com/tanaken0515/items/0e207160c25a6b815561)
- [Docker Composeのvolumesを使ってもっと効率的に \- Qiita](https://qiita.com/tanaken0515/items/a564d2c46b2942b83ebb)
- Docker Composeで作った初期装備なRails6にVue.jsを導入する **（今回の記事）**
- Docker Composeでwebpack-dev-serverを動かしてもっと快適に **（未定）**

ではまた〜。