RailsとVue.js(SPA)を「いいとこ取り」。API連携で開発するハンズオン。（その2：Vue編）

この記事は [その1：Rails編](https://qiita.com/shozzy/items/acd6eb915fdd87b5e689) の続きです。

# 何を作っているか
DBの操作と管理画面はRailsで作成しつつ、一般ユーザ向けの画面はSPAとかPWAにしたいというシーンは結構あるかと思います。既存アプリがRailsで動いているのを活かしながら、フロント側はSPA化するとか。基本はSPAなんだけど、管理画面はscaffoldでサクッと作って済ませたいとか。

そんなことを考えながら、このシリーズでは`Rails（管理画面＆API）`＋`Vue.js＆Nuxt.js(SPA）`という構成のアプリを、以下の役割分担で作っています。

|                       | Rails(View) | Rails(API) | Vue＆Nuxt |
|:---------------------:|:-----------:|:----------:|:--------:|
| コンテンツの表示（一般公開）  |    ○       |   ○        |    ○     |
| コンテンツの編集（管理者限定） |   ○       |   －        |    －     |


この記事では、Vue.jsとNuxt.jsでSPAの画面を作成し、Railsで作成したAPIと連携するところを掲載します。

アプリの題材はポートフォリオ。実は[もうリリース済みです](https://shozzy.netlify.com/)。

# Vue.js＆Nuxt.js側の準備
SPAモードで作ります。
言語はJavaScriptを選択しました。


~~Vuetifyは最初は1.5系が入ってしまうので、2.0系に後から上げます。~~

###2020.3.14追記
1.5系が入ってしまうというのは勘違いだった可能性があります。

[nuxt-community/vuetify-moduleのCHANGELOG](https://github.com/nuxt-community/vuetify-module/blob/master/CHANGELOG.md)によれば、2019−07−23時点でVuetify2に対応したと書いてありました。

追記時点ではすでにVuetify2系にアップデート済みなので確認できませんが、`yarn list`で確認を取っていればどのバージョンが入っているか確実に確認できたはずです。
ちなみに、追記時点はで`vuetify@2.1.4`でした。

###追記おわり

---

##環境
- Vue.js 2.6.11
- Nuxt.js 2.11.0
- Vuetify 2.1.0
- 開発環境はmacOS Mojaveです。[^adobe]

[^adobe]: 大枚はたいて買ったAdobeCS6を使い続けたいから上げられない、、、（どこかで諦めるしかありませんが）

## 前提
- Nuxt.jsはローカルにインストール済み
- Githubはアカウント取得済み


## プロジェクト生成
「portfolio-vue」と言うプロジェクト名で進めます。

Nuxt公式に従います。
https://ja.nuxtjs.org/guide/installation/

```console:console
$ yarn create nuxt-app portfolio-vue
```

この後の選択肢は、以下のようにしました。

```console:console
? Project name portfolio-vue
? Project description shozzy's portfolio
? Author name shozzy
? Choose the package manager Yarn
? Choose UI framework Vuetify.js
? Choose custom server framework None (Recommended)
? Choose Nuxt.js modules Axios
? Choose linting tools Prettier
? Choose test framework Jest
? Choose rendering mode Single Page App
? Choose development tools jsconfig.json (Recommended for VS Code)
```
しばらく待ったのち、、、

```console
🎉  Successfully created project portfolio-vue

  To get started:

	cd portfolio-vue
	yarn dev

  To build & start for production:

	cd portfolio-vue
	yarn build
	yarn start

  To test:

	cd portfolio-vue
	yarn test

✨  Done in 72.01s.
```

うまく行きました！

```console
$ cd portfolio-vue
$ yarn dev
```

ブラウザから http://localhost:3000/ にアクセスします。

このような画面になれば成功です:tada: [^2]
<img width="807" alt="スクリーンショット 2019-09-08 21.34.37.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120785/58e468de-9d36-9c56-e86d-7f63ef0c0fdc.png">


[^2]: 今回はVuetifyを採用したのでこの画面ですが、他のCSSフレームワークを選択した場合は、異なる見た目の画面になると思われます。

VSCodeで「Add Folder to Workspace...」から「portfolio-vue」を選択します。

## （不要なはず）Vuetifyを2.0系にする
2020.03.14追記：前述の通り、この手順は不要と思われます。そのため、記載内容を折りたたみました。
`yarn list`で表示されるvuetifyのバージョンを確認しておきましょう。

<details><summary>以前の記載内容</summary><div>
Vuetifyのバージョンが1.5系なので、2.0系に先に上げておきます。

変更前

```nuxt.config.js
   modules: [
     // Doc: https://axios.nuxtjs.org/usage
     '@nuxtjs/axios',
   ],
```

変更後

```nuxt.config.js
  modules: [
    // Doc: https://axios.nuxtjs.org/usage
    '@nuxtjs/vuetify',
    '@nuxtjs/axios',
  ],
```

Vuetify公式によれば、執筆時点[^create-date]での最新はv2.1.4のようなので、そこまで上げておきます。
https://github.com/vuetifyjs/vuetify/releases

[^create-date]: この部分を書いたのは2019/10/16。公開までだいぶ時間があいてしまいました。。。

```console
$ yarn add vuetify@2.1.4
```

念のため、Vuetify2.0で動作しているか確認しておきます。
方法は、デフォルトで生成されるindex.vueの中身をVuetify2.0から導入された「v-row」「v-col」を用いるように変更し、それがブラウザで正しく表示されるかを確認します。[^4]

[^4]: もっとスマートな方法があるような気がしています。ご存知のかたいらっしゃいましたら、教えていただけると幸いです。


変更前

```vue:pages/index.vue
<template>
  <v-layout
    column
    justify-center
    align-center
  >
    <v-flex
      xs12
      sm8
      md6
    >
<!-- 中略 -->
        <v-card-title class="headline">
          Welcome to the Vuetify + Nuxt.js template
        </v-card-title>
<!-- 中略 -->
    </v-flex>
  </v-layout>
</template>
```

変更後

```vue:pages/index.vue
<template>
  <v-row
    column
    justify-center
    align-center
  >
    <v-col
      xs12
      sm8
      md6
    >
<!-- 中略 -->
        <v-card-title class="headline">
          Welcome to the Vuetify2.0 + Nuxt.js template
        </v-card-title>
<!-- 中略 -->
    </v-col>
  </v-row>
</template>
```

表示されたので、問題なさそうです。
<img width="807" alt="スクリーンショット 2019-09-08 23.57.18.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120785/938cf7e5-807a-607f-6f36-fd3869fe3cad.png">

</div></details>

## GitHubへ初期コミット
ここらでGithubにfirst commitしておきます。


```console
$ git init
Reinitialized existing Git repository in /Users/shozzy/portfolio-vue/.git/
```
どうやら最初からgitもセットアップされていたようです。
git initしても害はなさそうなので、そのまま進めました。

もしGithubを使うのが初めてであれば、メールアドレスの設定が必要です。

<details><div>
設定済みのかたは飛ばしてください。
your-github-mailaddress の部分は、Githubから提供される「noreply」のメールアドレスか、外部公開されても問題のない（可能ならGithub用に作成した）メールアドレスを設定してください。外部公開されるので、くれぐれも通常使用しているメールアドレスは指定しないように。

参考：
[過去記事1](https://qiita.com/shozzy/items/7bc4ab4d3b994996c08a)
[過去記事2](https://qiita.com/shozzy/items/b21f97dd624700663a1f)

```console
$ git config --global user.email your-github-mailaddress
```

</div></details>


Githubのサイトへログインし、新しいリポジトリを作成します。
プライベートリポジトリでも問題ありません。

.gitignore はnuxtがいい感じに設定してくれているので、そのままでOKです。

```console
$ git add .
$ git commit -m "first commit"
$ git remote add origin https://github.com/shozzy/portfolio-vue.git
$ git push -u origin master
```

ソース管理を開始できたので、ここから開発を進めていきます。
ブランチを切りつつチェックアウトしておきます。

```console
$ git checkout -b create-pages
```


## Nuxtのポートを変更
RailsもNuxtも、開発環境のポートがデフォルトでは3000です。
これでは同時に開発できないので、片方を変更する必要があります。
ここでは、Nuxtの方を8000番に変更しました。（http://localhost:8000/）

変更前

```nuxt.config.js
export default {
  mode: 'spa',
```

変更後

```nuxt.config.js
export default {
  server: {
    port: 8000,
  },
  mode: 'spa',
```



## 中身を作る
ここから中身を作っていきます。

※説明を最小限にするため、解説している内容と、キャプチャ画像の見た目が異なる場合があります。


### productsページ
先に、実際にAPIを使うページから作っていきます。
productsはこれまでの作品を紹介するページです。
コンテンツはAPIから取得したものを表示します。

productsページは`http://localhost:8000/products`
コンテンツを一覧で取得するAPIは`http://localhost:3000/api/contents`で用意してあります。バックエンドはRails。

以下のファイルを新規作成します。
最初は単純に、APIで取得したJSONをそのまま表示するだけの内容です。

```vue:pages/products.vue
<template>
  <div>
    {{ contents }}
  </div>
</template>

<script>
export default {
  async asyncData({ app }) {
    const contents = await app.$axios.$get('https://localhost：3000/api/contents')
    return { contents }
  },
}
</script>
```

、、、実は、これだとCORSエラーになります。
Nuxt側のOriginは`localhost:8000`ですが、Rails側のOriginは`localhost:3000`だからですね。
<img width="334" alt="スクリーンショット 2019-10-18 12.19.31.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120785/a77809ef-9745-ff24-5781-3ced1993ae8d.png">

### CORS (Cross-Origin Resource Sharing) を出来るようにする

クロスオリジンでのアクセスには制限がかかっています。CSRFを防ぐために。

そこで、Rails側で`gem 'rack-cors'`を使ってNetlify側からのアクセスを許可します。


###Rails側
Rails側に`rack-cors`の設定を追加します。

```file:Gemfile
gem 'rack-cors'
```

```
$ bundle install
```

ここで`config/initializers/cors.rb`の設定が必要になりますが、今回のRails環境はAPIモードではないので、このファイルは自動生成されません。なので、手動で作リます。

```ruby:config/initializers/cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins Rails.application.config.permitted_origin
    resource '*',
      headers: :any,
      methods: [:get, :post, :put, :patch, :delete, :options, :head]
  end
end
```

許可する呼び出し元は、ローカルと本番環境で当然異なるので、これも環境変数で定義します。[^api]

[^api]: 今回は完全にクローズドなAPIなのでアクセス元の制限をかけています。オープンなAPIなら制限なしもありえますし、特定のユーザだけに解放するようなAPIであればAPIキーでの認証になるはずです。

開発環境ではdotenv-railsを利用します。

```file:Gemfile
group :development do
  #中略
  gem 'dotenv-rails'
end
```

```file:.env
PERMITTED_ORIGIN = 'localhost:8000'
```

```ruby:application.rb
    config.permitted_origin = ENV.fetch('PERMITTED_ORIGIN', 'http://localhost:8000')
```

これで、Vue側（localhost:8000）からのアクセスが許可されました。


###Vue側
ついでに、Vue側にも環境変数「API_BASE」を作成し、APIが存在するドメインを設定できるようにしておきます。
ローカルの開発環境と本番環境ではドメインが異なるためです。

ローカルではdotenvを用います。[^honban]

[^honban]: 本番環境（Netlify）では "Environment variables" で定義しました。

```
$ yarn add @nuxtjs/dotenv
```


```file:.env
API_BASE=http://localhost:3000
```

```javascript:nuxt.config.js
  env: {
    API_BASE: process.env.API_BASE,
  },

  buildModules: [
    '@nuxtjs/dotenv',
  ],
```

これで、コード中で`process.env.API_BASE`を用いて環境変数の内容を読み出せるようになりました。

```vue:pages/products.vue
<script>
export default {
  async asyncData({ app }) {
    const contents = await app.$axios.$get(process.env.API_BASE+'/api/contents')
    return { contents }
  },
}
</script>
```

###JSONをAPI経由で取得する
ようやく、JSONでデータを取得できるようになりました。:tada:

<img width="894" alt="スクリーンショット 2019-10-18 12.30.12.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120785/1c9c4bc6-6284-cc42-682a-b3f97ce143e3.png">

## 見た目を整える
JSONのままではサイトとして公開できませんので、見た目を調整していきます。
各アイテムを表示するカード部分は繰り返し登場するので、コンポーネント化しました。

<img width="803" alt="スクリーンショット 2019-12-01 19.21.58.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120785/d9d63ea9-768a-df7e-1f54-620ee383dffe.png">

[^contents-capture]

[^contents-capture]: コンテンツ自体も編集しながら進めているので、JSONのキャプチャと見た目を整えた後のキャプチャでは内容が異なっています。


```vue:pages/products.vue
<template>
  <div>
      <v-container fluid>
        <v-row row wrap>
          <v-col v-for="item in contents" v-bind:key="item.id" xs12 sm6 md4>
            <ProductCard v-bind:title="item.title" v-bind:content="item.detail"/>
          </v-col>
        </v-row>
      </v-container>
  </div>
</template>

<script>
import ProductCard from '~/components/ProductCard.vue'
export default {
  components: {
    ProductCard
  },
  async asyncData({ app }) {
    const contents = await app.$axios.$get('/api/contents')
    return { contents }
  },
}
</script>
```

コードを全て掲載すると長くなりすぎるので、詳細は[GitHub](https://github.com/shozzy/portfolio-vue)にて。



# まとめ
この記事では、コンテンツをREST API経由で取得し、SPAのサイト内で表示させるところまで作成しました。

次の記事（鋭意執筆中）では、本番環境へのデプロイを実施します。本番環境はNetlify＋Herokuの組み合わせを想定しています。

推敲にはかなり時間をかけましたが、おかしいところがありましたら教えて頂けますと幸いです。




# 参考にしたWebサイト
執筆者の皆様、本当にありがとうございます！

- https://developers.google.com/web/updates/2019/02/rendering-on-the-web
- https://jp.vuejs.org/v2/cookbook/using-axios-to-consume-apis.html
- https://public-constructor.com/nuxtjs-with-axios/
- https://qiita.com/sygnas/items/7eac9491b37a1bcba0cb
- https://qiita.com/nishinoshake/items/eae57372e2fd360438e0
- https://qiita.com/IzumiSy/items/c10949e9a00d1c61613c