61
50

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

クラウドワークスAdvent Calendar 2020

Day 9

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

Last updated at Posted at 2020-12-08

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

はじめに

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

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

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

対象読者

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

作るもの

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

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

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

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

環境構築

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

使用する環境

  • 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 がアプリケーションの名前になります。

$ mkdir rails-vue3-app
$ cd rails-vue3-app

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

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

$ 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 以上であれば基本問題ないはずです。

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

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

Installation | 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 にしてください。

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

プロジェクトの作成

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

$ 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 として実行していたのですが、Vue3 ではオプションが提供されていないため --webpack=vue のオプションを付けずに実行します。 2

Vue3 のセットアップ

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

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

$ 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 を使用するように指定します。

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
補足: ファイルを分けて定義することも可能

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

config/webpack/loaders/vue.js
module.exports = {
  test: /\.vue(\.erb)?$/,
  use: [{
    loader: 'vue-loader'
  }]
}
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

config/webpacker.ymlextensions.vue を追加します。

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


  extensions:
    # ...
+    - .vue

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

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

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

コントローラーの作成

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

$ 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 を使用して、 js ファイルを読み込めるようにする

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

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 と出力されます。

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>
補足: Composition API を使って書く場合
[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 を使用した実装と呼ばれています。

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

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

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

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 に追加します。

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 にアクセスしてみると、下記のような画面が表示されるはずです。

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

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

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

$ 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 を実行しているタブとは別のタブで実行してください)。

$ bin/webpack-dev-server

Vue.js devtools

せっかくなので Vue.js devtools の使い方をここで確認しておきます。
使い方は簡単で、 chrome の developer ツールを開いて、 Vue のタブを選択するだけです。

Vue のタブを選択し、コンポーネントを選択してみると、内部の data などを確認できます。

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

実践的な実装

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

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

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

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

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

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

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

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 形式に変換しておきます。

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

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 を開けてみます。

このように、 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>
補足: data プロパティを設定する場合

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

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

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>
補足: data プロパティの設定を Composition API を使って書く場合

toRefs をでの分割代入を使用して実装します。

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

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

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

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 を実行中の場合は一度停止し、再度下記コマンドを実行しましょう。

 $ bin/webpack-dev-server

http://localhost:3000/home/index にアクセスした時、下記のようなページが表示されていると ok です。

実装のリファクタリング

とりあえず表示させることを優先で、 Index.vue に全てを書いていたので、簡単にコンポーネントの分割します。分割のイメージは下記になります。
image.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 コンポーネントを使用する形に修正します。

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 を叩くコンポーネントの実装

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

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

コントローラーの実装

今回は何もしないです。

View の実装

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

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 にアクセスすると、下記画像のようなページが表示されます。

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

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

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

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

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

コントローラーの実装

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

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

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

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

コンポーネントと API の Ajax 通信は axios というライブラリを使用します。5
まずは axios をインストールします。

$ yarn add --dev axios

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

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

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

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);
+        }
+      );
+    }
   }
補足: Composition API を使って書く場合
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>

created で vue コンポーネントが作成されたタイミングで axios によるデータ取得を走らせるようにしています。
console.log によって、取得したデータが出力されているはずなので、 http://localhost:3000/integer_literal_descriptions/index を見てみましょう。
↓ のようなログが出て、データ取得に成功しているはずです。
image.png

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

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

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

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

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

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

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 にアクセスしてみると、整数リテラル分類表が出てくるはずです。

Tips: 整数リテラルとは

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

リンクボタンを追加

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

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')
+      }]
     }
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>
補足: Composition API を使って書く場合
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>

http://localhost:3000/home/index を開いて、こんな感じのボタンができていたら ok です。

最後に

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

参考

注釈

  1. 今回の実装では CoffeeScriptturbolinks のセットアップを省いています。既存の rails プロジェクトで Vue.js を使用する場合は、 turbolinks と戦う必要がありそうです。

  2. Webpacker 6.0 でそもそも --webpack=vue といったオプション自体が廃止されるとのこと。ちなみの --webpack のオプションを指定しない場合でも、 rails new 時に default で webpacker:install が実行されます。

  3. オプションの詳細は、 こちらrails g controller -h コマンドで確認できます。

  4. createApp の第二引数に root props を渡せる機能を活用しています。

  5. axios を利用した API の使用。 Ajax 通信をする手段としては、他のも fetch API などを使用する方法が有名です。

61
50
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
61
50

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?