Vue.js チュートリアル for Rails エンジニア(Vue3 version)

この記事は クラウドワークス Advent Calendar 2020 の9日目の記事です。

## はじめに

この記事は、普段 Rails を使用して開発を行なっているエンジニアが、 [Vue.js (Vue3)](https://v3.ja.vuejs.org/) を触り始めようとする時に見て役に立ったら嬉しいものです。

今回は Vue3 での開発を想定したものになります。 Vue2 での開発に関して知りたい方は下記の記事をご覧ください。
[Vue.js チュートリアル for Rails エンジニア(Vue2 version)](https://qiita.com/t0yohei/items/d516fefaaad69b4022ec)

また、 Vue.js 自体を触るのは今回が初めてという方は、先に下記のようなチュートリアル記事を参照されることをお勧めします。
[Vue.js を vue-cli を使ってシンプルにはじめてみる](https://qiita.com/567000/items/dde495d6a8ad1c25fa43)

## 対象読者

- Rails に関しての知識がある程度ある方(Rails チュートリアルをやっていれば OK)
- Vue.js に関しての知識がある程度ある方(なんらかのチュートリアルなどをやっていれば OK。Vue3 の知識は必須でないです)
- Rails + Vue3 のアプリケーションをどうやって開発すればいいかを知りたい方

## 作るもの

今回は Webpacker を使用して Rails + Vue3 の簡単なアプリケーションを作っていきます。
そもそも Webpacker を使用しない手法だったり、Docker を使ったり、 TypeScript を使ったりといったことは本稿では扱わないのでご了承ください。
(Vue.js を使った開発のとっかかりとしてのチュートリアルという位置付けにしたいため)

作成予定の画面は ↓ にあるような、簡単なテーブル構造を持つ画面になります。

<div>
  <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152779/5996c21b-ad88-e217-9968-1496df8b7b58.png" width="300">
  <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152779/147cca55-d74c-2ed8-0111-97162a618fc4.png" width="300">
</div>

サンプルコードこちらに置いています: https://github.com/t0yohei/rails-vue3-app

では、早速環境構築からチュートリアルに入っていきましょう。

## 環境構築

環境構築には、 `homebrew` , `rbenv` を使用します。インストールがまだな方は、各自インストールをお願いします。

- rbenv: https://github.com/rbenv/rbenv#installation
- homebrew: https://brew.sh/index_ja

### 使用する環境

- macOS: 10.15.7 (Catalina)
- Ruby: 2.7.2
- Rails: 6.0.3
- Webpacker: 4.3.0
- yarn: 1.22.10
- Vue: 3.0.3

※ macOS を除いて、記事作成時点で最新のバージョンになります。

### アプリケーションを作成するディレクトリを作成

はじめに、これからアプリケーションを実装していくディレクトリを `rails-vue3-app` という名前で作成します。今回はこの `rails-vue3-app` がアプリケーションの名前になります。

```shell
$ mkdir rails-vue3-app
$ cd rails-vue3-app
```

### rbenv を使用して Ruby 2.7.2 をインストール

Ruby 2.7.2 をインストールします。

```shell
$ brew update && brew install ruby-build // または brew update && brew upgrade ruby-build
$ rbenv install 2.7.2
$ rbenv local 2.7.2
$ rbenv rehash
$ ruby -v
ruby 2.7.2p137 (2020-10-01 revision 5445e04352) [x86_64-darwin19]
```

### yarn 1.22.10 のインストール

yarn をインストールします。
今回は 1.22.10 を前提に進めていきますが、 1.22.10 以上であれば基本問題ないはずです。

```shell
$ brew install yarn // または brew upgrade yarn
$ yarn -v
1.22.10
```

### Vue.js devtools (Vue3 用)のインストール

[Installation \| Vue Devtools](https://v3.vuejs.org/guide/installation.html#vue-devtools) から devtools をインストールします。
以前に Vue.js devtools をインストールしている場合(Vue2 の Vue.js devtools を使っていた場合)、Vue3 の Vue.js devtools とは併用できなくなっています(2020 年 12 月時点)。
そのため、拡張機能の設定から既存の Vue.js devtools (Vue2 用) を OFF にする必要があります。
Chrome の場合は `chrome://extensions/` を開き、 Vue2 の Vue.js devtools を OFF にしてください。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152779/0585560a-b781-99cb-3b17-330cc493e8db.png" width="500">

以上で環境構築は終わりです。次からはプロジェクトの作成を進めていきます。

## プロジェクトの作成

カレントディレクトリ(rails-vue3-app)を起点に Rails プロジェクトを作成します。

```shell
$ bundle init
$ echo 'gem "rails", "~> 6.0.3"' >> Gemfile
$ bundle install --path vendor/bundle
$ bundle exec rails new . --skip-turbolinks --skip-coffee
Overwrite (略)/rails-vue3-app/Gemfile? (enter "h" for help) [Ynaqdhm] y
```

※ `--skip-turbolinks --skip-coffee` のオプションをつけて、今回は不要となるものを削ぎ落としています。 [^1]

[Vue2 では、 `rails new . --webpack=vue` として実行していたのですが](https://qiita.com/t0yohei/items/d516fefaaad69b4022ec#%E3%83%97%E3%83%AD%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%81%AE%E4%BD%9C%E6%88%90)、Vue3 ではオプションが提供されていないため `--webpack=vue` のオプションを付けずに実行します。 [^2]

## Vue3 のセットアップ

`--webpack=vue3` といった形での Vue3 のセットアップができないため、独自で Vue3 のセットアップを行っていきます。

### Vue3 用の各種パッケージをインストール

```shell
$ yarn add vue@next
$ yarn add --dev vue-loader@next @vue/compiler-sfc
```

インストール完了後、 `package.json` は以下のようになっているはずです(細かいバージョンはインストールのタイミングで違ってきます)

```package.json
{
  "name": "rails_vue3_app",
  "private": true,
  "dependencies": {
    "@rails/actioncable": "^6.0.0",
    "@rails/activestorage": "^6.0.0",
    "@rails/ujs": "^6.0.0",
    "@rails/webpacker": "4.3.0",
    "vue": "^3.0.3"
  },
  "version": "0.1.0",
  "devDependencies": {
    "@vue/compiler-sfc": "^3.0.3",
    "vue-loader": "^16.0.0"
    "webpack-dev-server": "^3.11.0"
  }
}
```

### Webpack の設定

次に `.vue` の拡張子がついた Vue ファイルを、 Webpack でコンパイルするための設定を追加します。
`config/webpack/environment.js` を開いてみると、現状は次のような状態になっているはずです。

```config/webpack/environment.js
const { environment } = require('@rails/webpacker')

module.exports = environment
```

以下のように書き変えて、 Vue ファイルに対して `vue-loader` を使用するように指定します。

```diff:config/webpack/environment.js
  const { environment } = require('@rails/webpacker')
+ const { VueLoaderPlugin } = require('vue-loader')
+
+ environment.plugins.prepend(
+     'VueLoaderPlugin',
+     new VueLoaderPlugin()
+ )
+
+ environment.loaders.prepend('vue', {
+     test: /\.vue$/,
+     use: [{
+         loader: 'vue-loader'
+     }]
+ })

  module.exports = environment
```

<details>
  <summary>補足: ファイルを分けて定義することも可能</summary>
  <div>

`config/webpack/loaders/vue.js` を作成して、 loader の定義ファイルを切り出すことも可能です。

```config/webpack/loaders/vue.js
module.exports = {
  test: /\.vue(\.erb)?$/,
  use: [{
    loader: 'vue-loader'
  }]
}
```

```diff:config/webpack/environment.js
  const { environment } = require('@rails/webpacker')
+ const { VueLoaderPlugin } = require('vue-loader')
+ const vue = require('./loaders/vue')

+ environment.plugins.prepend('VueLoaderPlugin', new VueLoaderPlugin())
+ environment.loaders.prepend('vue', vue)
  module.exports = environment
```

  </div>
</details>

`config/webpacker.yml` の `extensions` に `.vue` を追加します。

```diff:config/webpacker.yml
default: &default
  # ...


  extensions:
    # ...
+    - .vue
```

以上で Webpack の設定は終わりです。

## 一番シンプルな Rails + Vue を使った画面の実装

環境構築や設定周りの変更が完了したので、ここからは実際にアプリケーションの実装に入っていきます。
最初に、 Rails + Vue を使った一番シンプルな画面を実装をしていきます。

### コントローラーの作成

まずは `rails g controller` コマンドでコントローラーやルーティングを作成します。

```shell
$ bundle exec rails g controller HelloVue index --no-helper --no-assets
```

※ 今回は、ヘルパーやアセットファイルなど不要なものを生成しないオプションを指定しています。 [^3]

一旦サーバーを起動して、画面が表示するか見てみましょう。

```shell
$ bundle exec rails s
```

http://localhost:3000/hello_vue/index にアクセスして、下記のページが表示されると ok です。

<img src='https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152779/14345242-093b-6193-f3da-81cd901d42c2.png' width=300>

### javascript_pack_tag を使用して、 js ファイルを読み込めるようにする

`javascript_pack_tag` を使用することで `app/javascript/packs` 配下にあるファイルの読み込みが可能になります(この辺は [Webpacker の設定](https://github.com/rails/webpacker#usage)によるもの)。
今回は、読み込む対象としてこの後作成する `hello_vue.js` を指定します。

```diff:app/views/hello_vue/index.html.erb
  <h1>HelloVue#index</h1>
  <p>Find me in app/views/hello_vue/index.html.erb</p>
+ <%= javascript_pack_tag 'hello_vue.js' %>
```

### index.html.erb から読み込む js ファイルを作成

先ほど作成した `index.html.erb` から読み込む js ファイルを作成します。

```app/javascript/packs/hello_vue.js
document.addEventListener("DOMContentLoaded", () => {
  console.log('this is hello_vue.js')
});
```

再度 http://localhost:3000/hello_vue/index にアクセスして開発者ツールを開くと、 Console に `this is hello_vue.js` と出力されます。

<img src='https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152779/13987632-5d8a-c201-f7ba-34919ce7c6d1.png' width=500>

### Vue ファイルを作成

次に画面表示に使用するシンプルな Vue ファイル(Vue コンポーネント)を作成します。

```app/javascript/app.vue
<template>
  <p>
    {{ message }}
  </p>
</template>

<script>
export default {
  data() {
    return {
      message: "Hello Vue!"
    }
  }
}
</script>

<style scoped>
p {
  font-size: 2em;
  text-align: left;
}
</style>
```

<details>
  <summary>補足: Composition API を使って書く場合</summary>
  <div>
[Composition API](https://v3.ja.vuejs.org/guide/composition-api-introduction.html)は、 Vue3 で新たに提供された API です。 Composition API を使うことで、大規模な Vue アプリケーションにおけるコンポーネントの実装をシンプルに保つことができます。

今回はそもそもコンポーネントが大きくないので、Composition API を使うメリットはそんなにありませんが、 Composition API を使用した実装をする場合は下記のようになります。

```app/javascript/app.vue
<template>
  <p>
    {{ message }}
  </p>
</template>

<script>
import { ref } from 'vue'

export default {
  setup() {
    const message = ref('Hello World')
    return { message }
  }
}
</script>

<style scoped>
p {
  font-size: 2em;
  text-align: left;
}
</style>

```

余談ですが、 `data` 関数など使用した従来の Vue コンポーネントの実装方法は [Options API](https://v3.vuejs.org/api/options-api.html) を使用した実装と呼ばれています。

  </div>
</details>

### app コンポーネントを `index.html.erb` にマウントする

最後に、先ほど作成した `app.vue` のコンポーネントを `index.html.erb` にマウントします。

まずはマウント対象の要素を `index.html.erb` に作成します。

```diff:app/views/hello_vue/index.html.erb
  <h1>HelloVue#index</h1>
  <p>Find me in app/views/hello_vue/index.html.erb</p>
  <%= javascript_pack_tag 'hello_vue.js' %>
+ <div id='vue-app'></div>
```

この要素に対して、 App コンポーネントをマウントする実装を `hello_vue.js` に追加します。

```diff:app/javascript/packs/hello_vue.js
+ import { createApp } from "vue";
+ import App from "../app.vue";

  document.addEventListener("DOMContentLoaded", () => {
-    console.log('this is hello_vue.js')
+    const app = createApp(App);
+    app.mount("#vue-app");
  });
```

http://localhost:3000/hello_vue/index にアクセスしてみると、下記のような画面が表示されるはずです。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152779/99229de5-d8a5-1f63-f11d-d6543cc18022.png" width='500'>

この時点で、一番の基本となる Rails + Vue のアプリケーション実装が完了しました :tada: 。

### bin/webpack を使用した js, vue ファイルの事前ビルド

今までのやり方では、 http://localhost:3000/hello_vue/index にアクセスがあった時点で、毎回 js, vue ファイルのビルドを行うため、画面が表示されるのに少し時間がかかってしまいます。
下記のコマンドを実行することで、 `app/JavaScript` 配下の js, vue ファイルを事前にビルドしておくことができます。

```shell
$ bin/webpack
```

rails server を立ち上げ直した後に http://localhost:3000/hello_vue/index に再度アクセスすると、先ほどよりも早く画面が表示されるようになるはずです。
※ 必ず rails server を立ち上げ直してください

`bin/webpack` コマンドの詳細について知りたい方はこちら: https://qiita.com/chimame/items/8d3d6f4afea675cffa7d

### webpack の自動ビルド

先ほどは、 `bin/webpack` のビルドを行いましたが、これでは js, vue ファイルを変更するたびに再度コマンドを叩いてビルドをする必要があります。
それでは面倒なので、開発中は `bin/webpack` の代わりに下記コマンドを実行して、ファイルを保存するたびに自動ビルドが走るようにしておくといいでしょう(`rails server` を実行しているタブとは別のタブで実行してください)。

```shell
$ bin/webpack-dev-server
```

### Vue.js devtools

せっかくなので `Vue.js devtools` の使い方をここで確認しておきます。
使い方は簡単で、 chrome の developer ツールを開いて、 Vue のタブを選択するだけです。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152779/485111cb-2753-d85d-869e-dded0adcd0ad.png" width='500'>

Vue のタブを選択し、コンポーネントを選択してみると、内部の data などを確認できます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152779/02d49f91-aae0-b327-53bf-a905eb04b032.png" width='500'>

※ Vue.js devtools がうまく表示されない場合は、 `chrome://extensions/` で複数の Vue.js devtools が ON になっていないかを確認してください。あるいは、 chrome の developer ツールを開き直してみてください。

## 実践的な実装

先ほど実装では、本番アプリケーションを実装する上で必須になってくるバックエンド(controller など)からのデータの受け渡しを行なっていなかったので、ここからはその点を深掘っていきます。

Webpacker を使用した、 Rails + Vue のアプリケーションを作成する際、データの渡し方には色々あるのですが、今回は個人的にデータフローがわかりやすかった以下の 2 通りの方法でサンプルアプリケーションを作っていきます。

- 1. HTML のデータ属性に値を設定して渡す方法
- 2. API を使用して渡す方法

## 1. HTML のデータ属性に値を設定して渡す方法

まずはページを表示するためのコントローラーを、 `HomeController` という名前で作成します。

```shell
$ bundle exec rails g controller Home index --no-helper --no-assets
```

次にコントローラー内部の実装を進めていきます。 `index` メソッドのインスタンス変数として、 `title` `description` そして Hash 形式の `contents` を用意します。

```diff:app/controllers/home_controller.rb
  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/cd11b15642fbb26f71e2'
+         },
+         {
+           name: 'ソースコード',
+           text: 'GitHub',
+           url: 'https://github.com/t0yohei/rails-vue3-app'
+         }
+       ],
+     }
+   end
  end
```

続いて `view` の実装です。ここでポイントとなるのが、 Rails の `content_tag` ヘルパーを利用して、 `div` タグの `data` 属性に vue 側へ受け渡したいデータを設定している点です。 vue へ受け渡すデータは `json` 形式に変換しておきます。

```diff:app/views/home/index.html.erb
- <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 %>
```

生成される html は下記画像のようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152779/aeca759b-991d-1a2b-0ebb-c48cfb425384.png)

※ `data` 属性に設定した情報は、 developer tool などを使うことで閲覧可能です。そのため API 同様にユーザーのプライペート情報など、秘匿情報は公開しないよう注意してください。

### data 属性に設定した値を読み込む js ファイルを作成

先ほど view で設定したデータを、 js 側から読み取ってみましょう。 `app/javascript/packs/home/index.js` を下記の通り実装します。

```app/javascript/packs/home/index.js
document.addEventListener("DOMContentLoaded", () => {
  const node = document.getElementById("homeIndex");
  const initialData = JSON.parse(node.getAttribute("data"));
  console.log(initialData);
});
```

http://localhost:3000/home/index にアクセスして、 developer tool の console を開けてみます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152779/222d8327-76e2-0276-5601-120c7f96128a.png" width='500'>

このように、 Rails の view ファイルで設定したデータが、 object として取得できていることがわかります。
この object のデータを使用して、実装を進めていきましょう。

### 読み取った値を表示する Vue コンポーネントの実装

props として `initialData` を受け取る Index コンポーネントを作成します。

```app/javascript/components/home/Index.vue
<template>
  <div>
    <h1>{{ initialData.title }}</h1>
    <p>{{ initialData.description }}</p>
    <table class="contents-table">
      <tr>
        <th>名前</th>
        <th>リンク</th>
      </tr>
      <tr v-for="outer_link in initialData.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: {
    initialData: {
      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>
```

<details>
  <summary>補足: data プロパティを設定する場合</summary>
  <div>

今回の実装では、 props で受け取った値をそのまま画面に表示するだけなので今の実装で十分ですが、props の値は immutable (書き換え不可)なので任意のタイミングで値を変更したりできません。
(どこかのボタンを押すと `description` の値を変更させる、といった処理はできません。)

そう言った場合は、 props で受け取った値を初期値として data 属性を設定することで、 mutable な state (書き換え可能な値)を設定することができます。

```diff:app/javascript/components/home/Index.vue
  <template>
    <div>
-     <h1>{{ initialData.title }}</h1>
-     <p>{{ initialData.description }}</p>
+     <h1>{{ title }}</h1>
+     <p>{{ description }}</p>
      <table class="contents-table">
        <tr>
          <th>名前</th>
          <th>リンク</th>
        </tr>
-       <tr v-for="outer_link in initialData.contents.outer_links" v-bind:key="outer_link.name">
+       <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: {
      initialData: {
        type: Object,
        default: () => {}
      }
    },
+   data() {
+     return {
+       title: this.initialData.title,
+       description: this.initialData.description,
+       contents: this.initialData.contents
+     }
+   },
  };
  </script>
```

  </div>
</details>

<details>
  <summary>補足: data プロパティの設定を Composition API を使って書く場合</summary>
  <div>

[toRefs](https://v3.ja.vuejs.org/guide/composition-api-setup.html#%E3%83%95%E3%82%9A%E3%83%AD%E3%83%8F%E3%82%9A%E3%83%86%E3%82%A3) をでの分割代入を使用して実装します。

```app/javascript/components/home/Index.vue
...
<script>
import { toRefs } from 'vue'

export default {
  props: {
    initialData: {
      type: Object,
      default: () => {}
    }
  },
  setup(props) {
    const { title, description, contents } = toRefs(props.initialData)
    return { title, description, contents }
  }
};
</script>
...
```

  </div>
</details>

### Index コンポーネントをマウントする

`index.js` で Index コンポーネントに props として initialData を渡し初期化し[^4]、 `index.html.erb` の div 要素にマウントします。

```diff:app/javascript/packs/home/index.js
+ import { createApp } from "vue";
+ import Index from "../../components/home/Index.vue";

  document.addEventListener("DOMContentLoaded", () => {
    const node = document.getElementById("homeIndex");
    const initialData = JSON.parse(node.getAttribute("data"));
+   const app = createApp(Index, { initialData: initialData })
+   app.mount("#homeIndex");
-   console.log(initialData);
  });
```

### ページの表示

`app/javascript/packs` 配下に追加した js ファイルを読み込ませるためには、 `webpack-dev-server` の再起動が必要です。 `webpack-dev-server` を実行中の場合は一度停止し、再度下記コマンドを実行しましょう。

```shell
 $ bin/webpack-dev-server
```

http://localhost:3000/home/index にアクセスした時、下記のようなページが表示されていると ok です。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152779/f5da4a61-b39c-30df-b328-954f3f718799.png" width='500'>

### 実装のリファクタリング

とりあえず表示させることを優先で、 `Index.vue` に全てを書いていたので、簡単にコンポーネントの分割します。分割のイメージは下記になります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152779/d11f7c27-bd8c-21e1-e8a4-95035d6be25b.png)

ちょっとずつ実践的な実装になってきましたね。

#### HeaderView.vue の作成

Index コンポーネントの `title` , `description` 部分を切り出した HeaderView コンポーネントを作成します。このコンポーネントは、他のコンポーネントでも使いやすいように、1 階層上 (`app/javascript/components` 直下) に配置します。

```app/javascript/components/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></style>
```

#### Contents.vue の作成

次に、 Index コンポーネントの `table` 部分を切り出した Contents コンポーネントを作成します。

```app/javascript/components/home/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 の修正

最後に、 Index コンポーネントで HeaderView コンポーネント、 Contents コンポーネントを使用する形に修正します。

```diff:app/javascript/components/home/Index.vue
  <template>
    <div>
-     <h1>{{ initialData.title }}</h1>
-     <p>{{ initialData.description }}</p>
-     <table class="contents-table">
-       <tr>
-         <th>名前</th>
-         <th>リンク</th>
-       </tr>
-       <tr v-for="outer_link in initialData.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>
+     <HeaderView v-bind:initialData.title="title" v-bind:description="initialData.description" />
+     <Contents v-bind:contents="initialData.contents" />
    </div>
  </template>

  <script>
+ import HeaderView from "../HeaderView.vue";
+ import Contents from "./Contents.vue";

  export default {
+   components: {
+     HeaderView,
+     Contents
+   },
    props: {
      initialData: {
        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>
+ <style scoped></style>
```

Index コンポーネントがだいぶスッキリしましたね。
念の為、再度 http://localhost:3000/home/index にアクセスして、画面がちゃんと表示されることを確認しておきましょう。

## 2. API を使用して渡す方法での実装

次に、API を使用して渡す方法での実装を進めていきましょう。
下記の手順で整数リテラルの分類表を作成してみます。

- 2-1. API を叩くコンポーネントの実装
- 2-2. API エンドポイントの実装
- 2-3. コンポーネントから API を叩いてデータを取得
- 2-4. 取得したデータをコンポーネント内で描画

### 2-1. API を叩くコンポーネントの実装

まずはページを表示するためのコントローラーを作成します。

```shell
$ bundle exec rails g controller IntegerLiteralDescriptions index --no-helper --no-assets
```

#### コントローラーの実装

今回は何もしないです。

#### View の実装

`javascript_pack_tag` と、コンポーネントをマウントするための div 要素を追加します。

```diff:app/views/integer_literal_descriptions/index.html.erb
- <h1>IntegerLiteralDescriptions#index</h1>
- <p>Find me in app/views/integer_literal_descriptions/index.html.erb</p>
+ <%= javascript_pack_tag 'integerLiteralDescriptions/index.js' %>
+ <div id="integerLiteralDescriptionsIndex"></div>
```

#### コンポーネントの実装

`index.html.erb` にマウントする Index コンポーネントと Contents コンポーネントを追加します。

```app/javascript/components/integerLiteralDescriptions/Index.vue
<template>
  <div>
    <HeaderView v-bind:title="title" v-bind:description="description" />
    <Contents v-bind:contents="contents" />
  </div>
</template>

<script>
import HeaderView from "../HeaderView.vue";
import Contents from "./Contents.vue";

export default {
  components: {
    HeaderView,
    Contents
  },
  data() {
    return {
      title: "title",
      description: "description",
      contents: []
    };
  }
};
</script>

<style scoped>
</style>
```

```app/javascript/components/integerLiteralDescriptions/Contents.vue
<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>

<script>
export default {
  props: {
    contents: {
      type: Array,
      default: () => []
    }
  }
};
</script>

<style scoped>
.contents {
  border: 1px solid gray;
}
.contents th,
.contents td {
  border: 1px solid gray;
}
</style>
```

#### Vue コンポーネントをマウントする実装

Index コンポーネントと Contents コンポーネントを `index.html.erb` にマウントします。

```app/javascript/packs/integerLiteralDescriptions/index.js
import { createApp } from "vue";
import Index from "../../components/integerLiteralDescriptions/Index.vue";

document.addEventListener("DOMContentLoaded", () => {
  const app = createApp(Index)
  app.mount("#integerLiteralDescriptionsIndex");
});
```

再び `bin/webpack-dev-server` を実行し直し、 http://localhost:3000/integer_literal_descriptions/index にアクセスすると、下記画像のようなページが表示されます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152779/735f6b90-212f-8fce-826d-028e5f2ac159.png" width='400'>

これで API を叩いて取得したデータを受け取り、表示するためのコンポーネントが完成しました。

次は、先ほど作成したコンポーネントからリクエストを受け、レスポンスを返す処理を実装していきます。

### 2-2. API エンドポイントの実装

コントローラーを、 Api::V1::IntegerLiteralDescriptionsController という名前で作成します。

```shell
$ bundle exec rails g controller api/v1/integer_literal_descriptions index --no-helper --no-assets --no-view-specs
```

#### コントローラーの実装

整数リテラルの分類表の json を返却する index メソッドを実装します。

```diff:app/controllers/api/v1/integer_literal_descriptions_controller.rb
 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 にアクセスしてみると、下記のような画面が表示されるはずです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152779/6841f34d-1bb5-b843-b729-20ac2eefb0fd.png)

一旦これで、 API のエンドポイントが完成しました。

### 2-3. コンポーネントから API を叩いてデータを取得

コンポーネントと API の Ajax 通信は [axios](https://github.com/axios/axios) というライブラリを使用します。[^5]
まずは `axios` をインストールします。

```shell
$ yarn add --dev axios
```

install に成功していると、 `package.json` `axios` の項目が追記されているはずです。

```diff:package.json
   "devDependencies": {
+    "axios": "^0.21.0",
     "webpack-dev-server": "^3.11.0"
   }
```

次に `integerLiteralDescriptions/Index.vue` を書き換えて、 axios でのデータ取得を実装します。

```diff:app/javascript/components/integerLiteralDescriptions/Index.vue
 import Contents from "./Contents.vue";
+import Axios from "axios";

 export default {
   components: {
     HeaderView,
      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);
+        }
+      );
+    }
   }
```

<details>
  <summary>補足: Composition API を使って書く場合</summary>
  <div>

```app/javascript/components/integerLiteralDescriptions/Index.vue
<script>
import HeaderView from "../HeaderView.vue";
import Contents from "./Contents.vue";
import Axios from "axios";
import { ref, onMounted } from "vue";

export default {
  components: {
    HeaderView,
    Contents
  },

  setup() {
    const title = ref('')
    const description = ref('')
    const contents = ref([])

    const updateContents = () => {
      Axios.get("/api/v1/integer_literal_descriptions/index.json").then(
        response => {
          const responseData = response.data;
          console.log(responseData);
        }
      );
    }

    onMounted(updateContents)

    return { title, description, contents }
  }
};
</script>
```

  </div>
</details>

`created` で vue コンポーネントが作成されたタイミングで `axios` によるデータ取得を走らせるようにしています。
`console.log` によって、取得したデータが出力されているはずなので、 http://localhost:3000/integer_literal_descriptions/index を見てみましょう。
↓ のようなログが出て、データ取得に成功しているはずです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152779/53efcc47-4697-e491-bb3c-fbe07cc7ba6a.png)

<details>
  <summary>Tips: JavaScript のデバッグ</summary>
  <div>
ご存知な方も多いと思いますが、 JavaScript では debugger を仕込むことで、デバッグ実行が可能になります。
[debugger ステートメント | MDN](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/debugger)

具体的には下記のように仕込むことができます。

```diff:app/javascript/components/integerLiteralDescriptions/Index.vue
   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 にアクセスしてみると、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152779/d3426fcc-cd8d-15bf-0adb-96d78219f9ce.png)

`debugger` を仕込んだ部分で処理が止まり、 `Console` からその時点の各種データを覗くことができます。(↑ 画像の場合、画像下部で `responseData` の値を確認しています。)

  </div>
</details>

### 2-4. 取得したデータをコンポーネント内で描画

先ほど `axios` で取得したデータを、画面に反映させます。
今回の場合、下記の部分を書き換えるだけです。

```diff:app/javascript/components/integerLiteralDescriptions/Index.vue
   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 にアクセスしてみると、整数リテラル分類表が出てくるはずです。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152779/8c33664a-db72-e8b3-c45d-dc742731fbdb.png" width="500">

<details>
  <summary>Tips: 整数リテラルとは</summary>
  <div>

JavaScript で表現できる [整数リテラル](https://jsprimer.net/basic/data-type/#integer-literal) には 10 進数、2 進数、8 進数、16 進数があります。
JavaScript では 0 で始まる数値の直後に `b, o, x` をつけることで、それぞれ 2 進数、8 進数、16 進数が表現できます。
`b, o, x` は英語訳を見てみると、それぞれ binary digits, octal, hex となっており、それぞれの頭文字から来ていることがわかります。今回の実装で整数リテラル分類表をサンプルに組み込んだ理由はただの思いつきです。

  </div>
</details>

### リンクボタンを追加

せっかくなので、 [home/index](http://localhost:3000/home/index) から [http://localhost:3000/integer_literal_descriptions/index](integer_literal_descriptions/index) に飛べるよう、リンクボタンを追加しておきましょう。

```diff:app/controllers/home_controller.rb
           url: 'https://github.com/t0yohei/rails-vue-app'
         }
       ],
+      inner_links: [{
+        label: '整数リテラル分類表',
+        url: url_for(action: 'index', controller: 'integer_literal_descriptions')
+      }]
     }
```

```diff:app/javascript/components/home/Contents.vue
...
         </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>

 <script>
 export default {
   props: {
     contents: {
       type: Object,
       default: () => {}
     }
+  },
+  methods: {
+    changeLocation(url) {
+      window.location.href = url;
+    }
   }
 };
 </script>

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

<details>
  <summary>補足: Composition API を使って書く場合</summary>
  <div>

```app/javascript/components/home/Contents.vue
<script>
import { ref } from "vue";

export default {
  props: {
    contents: {
      type: Object,
      default: () => {}
    }
  },

  setup(props) {
    const changeLocation = url => {
      window.location.href = url;
    }
    return { changeLocation }
  }
};
</script>
```

  </div>
</details>

http://localhost:3000/home/index を開いて、こんな感じのボタンができていたら ok です。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152779/38a85c48-683d-feaa-5090-830fff894028.png" width=400>

## 最後に

今回のチュートリアルは以上で終了です。
このチュートリアルで学習したのは Webpacker を使用した Vue + Rails アプリケーションの基本となる部分で、大きなアプリケーションに取り組むときにも、これからアプリケーションを作り始める時にも大きな武器になるはずです。
このチュートリアルが、 Rails や Vue を学習される方の助けに少しでもなっていれば幸いです。
では。

## 参考

- [rails/webpacker: Use Webpack to manage app-like JavaScript modules in Rails](https://github.com/rails/webpacker)
- [Vue.js 3 · Issue #2751 · rails/webpacker](https://github.com/rails/webpacker/issues/2751)
- [Vue3 on Rails - DEV](https://dev.to/vannsl/vue3-on-rails-l9d)
- [はじめに | Vue.js](https://v3.ja.vuejs.org/guide/introduction.html)
- [メンテ不能になったフロントエンド環境を立て直す話](https://engineer.crowdworks.jp/entry/2019/05/28/114806)

## 注釈

[^1]: 今回の実装では [CoffeeScript](http://coffeescript.org/) と [turbolinks](https://github.com/turbolinks/turbolinks) のセットアップを省いています。既存の rails プロジェクトで Vue.js を使用する場合は、 turbolinks と[戦う必要がありそう](https://tech.misoca.jp/entry/2018/02/02/110000)です。
[^2]: [Webpacker 6.0 でそもそも `--webpack=vue` といったオプション自体が廃止されるとのこと。](https://github.com/rails/webpacker/issues/2757#issuecomment-710784864)ちなみの --webpack のオプションを指定しない場合でも、 rails new 時に [default で webpacker:install が実行されます。](https://github.com/rails/rails/blob/6-1-stable/railties/lib/rails/generators/app_base.rb#L429)
[^3]: オプションの詳細は、 [こちら](<http://railsdoc.com/rails#%E3%82%B3%E3%83%B3%E3%83%88%E3%83%AD%E3%83%BC%E3%83%A9%E3%81%AE%E7%94%9F%E6%88%90(rails%20generate%20controller)>) や `rails g controller -h` コマンドで確認できます。
[^4]: [createApp の第二引数に root props を渡せる機能](https://v3.vuejs.org/api/global-api.html#arguments)を活用しています。
[^5]: [axios を利用した API の使用](https://jp.vuejs.org/v2/cookbook/using-axios-to-consume-apis.html)。 Ajax 通信をする手段としては、他のも [fetch API](https://developer.mozilla.org/ja/docs/Web/API/Fetch_API/Using_Fetch) などを使用する方法が有名です。