Vue.js / Vue.js + TS / Nuxt.js / Nuxt.js + TSでポケモンのタイプ相性表を実装してみる

JSフレームワークである[Vue.js](https://jp.vuejs.org/)と、そのVue.jsのフレームワークである[Nuxt.js](https://ja.nuxtjs.org/)のどちらを使うべきか？といった議論は時々見かけます。

が、結局は使う場面による、という結論だと思うので、Vue([vue-cli](https://cli.vuejs.org/))と、Nuxtで同じアプリケーションを作って、「始め方」や「書き方」を整理しておきたいと思います。

> **2019/10/16**
> 続編として、これらをサーバ上で動かすための「[Vue.js / Nuxt.jsのアプリをnginxで動かす（サブディレクトリ対応）](https://qiita.com/ktkiyoshi/items/c6b7decfa368cd7ce579)」を書きました。

# 前提

## 題材と完成品

### ポケモンのタイプ相性表

私自身がポケモンシリーズ好きということで、18種類あるタイプの相性表（同タイプ同士もある総当たり戦）を題材としました。

- ●) 効果バツグン(2倍)
- ▲) 効果いまひとつ(0.5倍)
- ×) 効果なし(0倍)

完成したアプリの見た目がこちら。（縦軸が攻撃側、横軸が防御側）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59926/7edec443-70b3-583d-a95d-e30f2f9cff21.png)

### 完成したアプリとリポジトリ

※リポジトリは執筆当時なので今後更新がある・・・かも。

#### [Vue.js(vue-cli)](https://kt-kiyoshi.com/vue-cli-app/type) ver.

- https://github.com/ktkiyoshi/vue-cli-app

#### [Vue.js(vue-cli) + TypeScript](https://kt-kiyoshi.com/vue-cli-ts-app/type) ver.
- https://github.com/ktkiyoshi/vue-cli-ts-app

#### [Nuxt.js](https://kt-kiyoshi.com/nuxt-app/type) ver.
- https://github.com/ktkiyoshi/nuxt-app

#### [Nuxt.js + TypeScript](https://kt-kiyoshi.com/nuxt-ts-app/type) ver.
- https://github.com/ktkiyoshi/nuxt-ts-app

`Vue.js`であれば`vue-cli`の初期状態から`views`に1ページ足しただけ、`Nuxt.js`も`pages`に1ページ足しただけ、という超簡易実装です。

## 使用したWeb APIについて

そもそもポケモンのタイプ相性表に変動する余地はないのですが、`Vue`でアプリを作るとなったらほぼ必須となる[axios](https://github.com/axios/axios)も使うべく、無理やりWeb APIを使うことにしました。

### ポケモンのタイプ情報を返却するAPI

自作しました。（こちらはGithubには上げてません）

#### レスポンス

```json
[
  {
    "id": 1,
    "name": "ノーマル",
    "short_name": "ノ",
    "color": "b1b1b1",
    "attack_relations": [
      {
        "attack_id": 1,
        "defense_id": 1,
        "effect_rate": 1.0
      },
      {
        "attack_id": 1,
        "defense_id": 2,
        "effect_rate": 1.0
      },
// 略
```
# 環境構築と実装

ここから上に挙げた各アプリの環境構築と実装について書いていきます。なお、`node.js`はインストール済み(`npm`が使える状態)という前提です。

## Vue.js(vue-cli)

### vue-cliインストール
グローバル(-g)にインストールします。

```
$ npm install -g @vue/cli
```
参考）[Vue CLI Installation](https://cli.vuejs.org/guide/installation.html)

### 初期設定
アプリ名は`vue-cli-app`で、`Babel`。`Router`にチェックを入れます。(`Linter / Formatter`は任意で、`Vuex`は今回使ってないのでなくてもOK)

```
$ vue create vue-cli-app

Vue CLI v3.11.0
? Please pick a preset: Manually select features
? Check the features needed for your project:
 ◉ Babel
 ◯ TypeScript
 ◯ Progressive Web App (PWA) Support
 ◉ Router
❯◉ Vuex
 ◯ CSS Pre-processors
 ◉ Linter / Formatter
 ◯ Unit Testing
 ◯ E2E Testing
```

### ローカル起動確認
```
 $ cd vue-cli-app
 $ npm run serve
```
ここまではいわゆる`Hello World`的な部分なので問題はないかと思います。

### Axiosインストール
```
$ npm install axios
+ axios@0.19.0
```

### 実装(コーディング)

全部載せると長くなりそうなので、ポイントを抜粋していきます。

#### Type.vueの`template`

基本は`<table>`タグを使い、APIから取得する`types`という連想配列をループしていくだけです。背景色のRGBもAPIのレスポンスに入れてます。

```html:views/Type.vue
<template>
  <div>
    <table>
      <tr>
        <td class="none">&nbsp;</td>
        <th v-for="type in types" :key="type.id"
          :style="{ 'background-color' : '#' + type.color }">
          {{ type.short_name }}
        </th>
      </tr>
      <tr v-for="type in types" :key="type.id">
        <th :style="{ 'background-color' : '#' + type.color }">
          {{ type.short_name }}
        </th>
        <td v-for="(relation, idx) in type.attack_relations" :key="idx"
          :class="[
            relation.effect_rate == 2? 'red': '',
            relation.effect_rate == 0.5? 'blue': '',
            relation.effect_rate == 0? 'bold': ''
          ]"
        >
          {{ toSymbol(relation.effect_rate) }}
        </td>
      </tr>
    </table>
  </div>
</template>
```

#### Type.vueの`script`

ライフサイクルフックである`created`でAPIを叩いて、レスポンスを`types`に入れています。（あれ、`types: []`でいいんだっけ...`types: {}`...？

```vue:views/Type.vue
<script>
import axios from 'axios'
export default {
  name: 'type',
  data: function() {
    return {
      types: []
    }
  },
  created: function () {
    axios.get(process.env.VUE_APP_DOMAIN + '/v1/types')
      .then((response) => {
        this.types = response.data;
      })
  },
  methods: {
    toSymbol(rate) {
      switch(rate) {
        case 2:   rate = "●";
                  break;
        case 0.5: rate = "▲";
                  break;
        case 0:   rate = "×";
                  break;
        default:  rate = "";
      }
      return rate;
    }
  },
}
</script>
```

### ルーティング

追加した箇所だけの抜粋ですが、非常にシンプルです。

```javascript:router.js
{
  path: '/type',
  name: 'type',
  component: Type
},
```

### 環境変数ファイル（.env）

`vue-cli 3.0`からはルートディレクトリに`.env`ファイルを配置すると、`process.env.hogehoge`として環境変数を利用できるようです。

ただし、変数の定義方法にルールがあり、`VUE_APP_`から始めること、です。

```:.env
VUE_APP_DOMAIN=http://localhost:8080
```
アプリ側からの参照方法は以下の通りです。

```vue:views/Type.vue
axios.get(process.env.VUE_APP_DOMAIN + '/v1/types')
```

#### 環境別の環境変数

上記はAPIのドメインを設定していますが、ローカルでの開発時と、サーバでの運用時はAPIドメインが変わる、といった場合には以下のように設定が可能です。

```:.env.development
VUE_APP_DOMAIN=http://localhost:8080
```
```:.env.production
VUE_APP_DOMAIN=https://hogehoge.com
```
ローカルで`npm run serve`した時は`.env.development`、サーバで`npm run build` => `npm run start`した時は`.env.production`を自動でを読みにいくようです。（やってないけど起動時に設定することも可能らしい）


## Vue(vue-cli) + TypeScript

### vue-cliインストール
※ Vue(vue-cli)と同じ

### 初期設定
アプリ名は`vue-cli-ts-app`で、`Babel`。`Router`と、今回は`TypeScript`にもチェックを入れます。(`Linter / Formatter`は任意で、`Vuex`は今回使ってないのでなくてもOK)

```
$ vue create vue-cli-app

Vue CLI v3.11.0
? Please pick a preset: Manually select features
? Check the features needed for your project:
 ◉ Babel
 ◉ TypeScript
 ◯ Progressive Web App (PWA) Support
 ◉ Router
❯◉ Vuex
 ◯ CSS Pre-processors
 ◉ Linter / Formatter
 ◯ Unit Testing
 ◯ E2E Testing
```

### ローカル起動確認
```
$ cd vue-cli-ts-app
$ npm run serve
```

### Axiosインストール
※ Vue(vue-cli)と同じ

### 実装(コーディング)

構成などはVue.js(vue-cli)と変わらないので、メインは`script`部分だけと言っても過言ではないです。

#### Type.vueの`template`
※ Vue(vue-cli)と同じ

#### Type.vueの`script`
ポイントは`data`の`types`に型定義が書かれていることと、`created()`と`toSymbol()`が見た目変わらないこと、でしょうか。もちろん、`created`はライフサイクルフックとして機能します。

```vue:views/Type.vue
<script lang="ts">
import axios from 'axios';
import { Component, Vue } from 'vue-property-decorator';
@Component({
  components: {
  },
})
export default class Type extends Vue {
  // data
  public types: Array<{
    id: number,
    name: string
    short_name: string,
    color: string,
    attack_relations: Array<{
      attack_id: number,
      defense_id: number,
      effect_rate: number;
    }>;
  }> = [];
  // created
  public created(): void {
    axios.get(process.env.VUE_APP_DOMAIN + '/v1/types')
      .then((response) => {
        this.types = response.data;
      });
  }
  // methods
  public toSymbol(rate: number) {
    let symbol: string;
    switch (rate) {
      case 2:   symbol = '●';
                break;
      case 0.5: symbol = '▲';
                break;
      case 0:   symbol = '×';
                break;
      default:  symbol = '';
    }
    return symbol;
  }
}
</script>
```

### ルーティング
※ Vue(vue-cli)と同じ

### 環境変数ファイル（.env）
※ Vue(vue-cli)と同じ

## Nuxt.js

### create-nuxt-app

`npx`というものが必要らしいのですが、`npm 5.2.0`からはデフォルトでバンドルされているそう。

参考）[nuxt-js-を使ったスターターテンプレート](https://ja.nuxtjs.org/guide/installation/#nuxt-js-%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%9F%E3%82%B9%E3%82%BF%E3%83%BC%E3%82%BF%E3%83%BC%E3%83%86%E3%83%B3%E3%83%97%E3%83%AC%E3%83%BC%E3%83%88)

### 初期設定
アプリ名は`nuxt-app`で、`package manager`として`Npm`を選択し。`Nuxt.js modules`は`Axios`を選択しておきます。

```
$ npx create-nuxt-app nuxt-app

create-nuxt-app v2.11.1
✨  Generating Nuxt.js project in nuxt-app
? Project name nuxt-app
? Project description My wicked Nuxt.js project
? Author name hogehoge
? Choose the package manager Npm
? Choose UI framework None
? Choose custom server framework None (Recommended)
? Choose Nuxt.js modules Axios
? Choose linting tools (Press <space> to select, <a> to toggle all, <i> to invert selection)
? Choose test framework None
? Choose rendering mode Universal (SSR)
? Choose development tools (Press <space> to select, <a> to toggle all, <i> to invert selection)
```

### ローカル起動確認

`vue-cli`は`npm run serve`だったので微妙に違う！

```
$ cd nuxt-app
$ npm run dev
```

### Axiosインストール
※`create-nuxt-app`でインストール済み

### 実装(コーディング)

#### type.vueの`template`
※Vue(vue-cli) / Vue(vue-cli) + TSと同じ

#### type.vueの`script`

ここもほとんど`vue-cli`と変わりませんが、APIのドメイン(環境変数)の部分が微妙に異なります。同じにもできるのですが、PREFIXなしでいけるのであえて変えています。（後述）

```vue:views/type.vue
<script>
import axios from 'axios'
export default {
  name: 'type',
  data: function() {
    return {
      types: []
    }
  },
  created: function () {
    axios.get(process.env.DOMAIN + '/v1/types')
      .then((response) => {
        this.types = response.data;
      })
  },
  methods: {
    toSymbol(rate) {
      switch(rate) {
        case 2:   rate = "●";
                  break;
        case 0.5: rate = "▲";
                  break;
        case 0:   rate = "×";
                  break;
        default:  rate = "";
      }
      return rate;
    }
  },
}
</script>
```

### ルーティング

`Nuxt.js`におけるルーティングは、`pages`ディレクトリの配下がそのままルーティングになるので、ファイルとしては不要です。

今回の場合、`pages/type.vue`なので、`/type`というパスでアクセスすることになります。

### 環境変数ファイル（.env）

`vue-cli`では、ルートディレクトリに`.env`を置くだけでしたが、`Nuxt.js`はそれが使えないのでモジュールをインストールします。

#### [dotenv](https://github.com/motdotla/dotenv)
```
$ npm install @nuxtjs/dotenv
```
インストール後、モジュールを使うための設定と、環境別に読み込むための設定を`nuxt.congfig.js`に入れます。

```javascript
modules: [
  // Doc: https://axios.nuxtjs.org/usage
  '@nuxtjs/axios',
  [
    '@nuxtjs/dotenv',
    { filename: process.env.NODE_ENV !== 'production' ? ".env.dev" : ".env.prod" }
  ]
],
```

ちなみに、特に意味はありませんが、開発用を`.env.dev`、本番用を`.env.pro`というファイル名にしました。（`development`、`production`が長いと思ったので）

## Nuxt.js + TypeScript

### create-nuxt-app

※ Nuxt.jsと同じ


### 初期設定
アプリ名は`nuxt-ts-app`で、あとはNuxt.jsの時と同じです。
なお、`vue-cli`の時のように、`TypeScript`をプリインストールすることはできません。

### `TypeScript`を使うための準備

ここがメインと言ってもいいでしょう。

#### インストール

```
$ npm install --save-dev @nuxt/typescript-build
+ @nuxt/typescript-build@0.2.9
```

#### 設定

```javascript:nuxt.config.js
buildModules: [
  '@nuxt/typescript-build'
],
```
以下は新規ファイルです。

```json:tsconfig.json
{
  "compilerOptions": {
    "target": "esnext",
    "module": "esnext",
    "moduleResolution": "node",
    "lib": [
      "esnext",
      "esnext.asynciterable",
      "dom"
    ],
    "esModuleInterop": true,
    "allowJs": true,
    "sourceMap": true,
    "strict": true,
    "noEmit": true,
    "baseUrl": ".",
    "paths": {
      "~/*": [
        "./*"
      ],
      "@/*": [
        "./*"
      ]
    },
    "types": [
      "@types/node",
      "@nuxt/types"
    ]
  },
  "exclude": [
    "node_modules"
  ]
}
```

参考） [Nuxt TypeScript Setup](https://typescript.nuxtjs.org/guide/setup.html#installation)

### Class-basedで書きたい
上記の準備だけでTypeScript自体は使えるようですが、Class-basedで書くならいかが必要です。

```
$ npm install --save vue-property-decorator
```

先ほど作成した`tsconfig.json`に一行追記します。

```json:tsconfig.json
"experimentalDecorators": true,
```

### ローカル起動確認
※ Nuxt.jsと同じ

### Axiosインストール
※`create-nuxt-app`でインストール済み

### 実装(コーディング)

#### type.vueの`template`
※ ※Vue(vue-cli) / Vue(vue-cli) + TS / Nuxt.jsと同じ

#### type.vueの`script`
※ vue-cli + TypeScriptと同じ

### ルーティング
※ Nuxt.jsと同じ

### 環境変数ファイル（.env）
※ Nuxt.jsと同じ

---

4つを並行して作っていたから少し混乱気味だったけど、こうして書いてみると「〜〜〜と同じ」が多いので、意外と異なる箇所はシンプルなのかな、と思いました。

ここから状態管理の`Vuex`を使うと更に複雑になりそうですが、それはもう少し複雑なアプリになってから考えるとして、これくらいなら`Nuxt.js`なしの`Vue.js`単体でも十分そうです。