【コロナ】DockerとVue.jsで30万円給付の対象になるか判定するYes/No診断を作った

# はじめに
新型コロナウイルスの感染拡大の影響を受けて、政府は収入が減少した世帯への30万円の現金給付を決めました。
しかし国が決めた政策というだけあって、内容が簡易だとしてもなぜか色々と面倒、難しそうというイメージが湧いてしまいますよね（僕だけ…？）。

そこで30万円給付の対象になるか判定するYes/No診断を作ってみました。
厳密なものではなく、簡易（本当に簡易です）な診断なので「自分が給付対象に該当する可能性あるのかな？」くらいの気持ちでみてもらえればと思います。

**作ったサイト↓**
https://corona-benefit-target.netlify.com/

# 概要
今回はサイトの簡易さとは裏腹に、無駄に豪華な技術スタックで作成してみます（今回のケースだと生のHTML, CSS, JSで十分です）。
フレームワークはVue.js（TypeScript）、デザインにはVuetifyを採用し、ローカルを汚さないように開発環境はDocker上に構築します。適用はNetrifyに任せます。

この記事では、以下の手順に沿ってサイト構築にあたって実施したことを説明します。

- Docker上にVueプロジェクトを作成
- プロジェクトの設計と開発
- Netrifyを使ってdeploy及び公開

# 前提
- Dockerをインストールして起動しておく

# Docker上にVueプロジェクトを作成する
## プロジェクト用ディレクトリの作成
```bash
$ mkdir corona-benefit-target
$ cd corona-benefit-target/
```
## Docker設定ファイルの作成
```bash
$ vim Dockerfile
```
```dockerfile
FROM node:13.12.0-alpine

WORKDIR /app

RUN apk update && \
    npm install -g npm @vue/cli
```
```bash
$ vim docker-compose.yml
```
```docker-compose.yml
version: '3'
services:
  app: # Dockerが構築するNetworkにおけるホスト名
    build: . # Dockerfileがあるディレクトリ
    ports: # ホストのポートとコンテナのポートを繋げる(http://localhost:5555 でコンテナの8080番に繋がる)
      - 5555:8080
    volumes:
      - .:/app # コンテナのディレクトリをホストのディレクトリにマウント（ホスト：コンテナ）
    stdin_open: true # -iに相当
    tty: true # -tに相当
    command: /bin/sh
```

## Dockerコンテナイメージのビルド・起動・実行
```bash
$ docker-compose build
$ docker-compose up -d
$ docker-compose exec app sh
```
## コンテナ上でVueプロジェクトの作成（with TypeScript & VueRouter）
TypeScriptでコーディングしたいので選択肢は下記のようにしました。

```bash
/app # vue create .

Vue CLI v4.3.1
? Generate project in current directory? Yes

Vue CLI v4.3.1
? Please pick a preset: Manually select features
? Check the features needed for your project: Babel, TS, Router, Linter
? Use class-style component syntax? Yes
? Use Babel alongside TypeScript (required for modern mode, auto-detected polyfills, transpiling JSX)? Yes
? Use history mode for router? (Requires proper server setup for index fallback in production) Yes
? Pick a linter / formatter config: Basic
? Pick additional lint features: Lint on save
? Where do you prefer placing config for Babel, ESLint, etc.? In dedicated config files
? Save this as a preset for future projects? No
? Pick the package manager to use when installing dependencies: NPM
```

## Vuetifyの追加
`vue add vuetify`で、Vue CLIがいい感じにVuetifyをプロジェクトに適用してくれます。

```bash
/app # vue add vuetify

? Choose a preset: Default (recommended)
```

TypeScript用の設定として`tsconfig.json`のtypesにvuetifyを追加します。

```tsconfig.json
"types": [
    "webpack-env",
    "vuetify"
]
```

## ローカルサーバーを起動
```bash
/app npm run serve
```
デフォルトで下記ポートになっています、これはDocker内での話です。
`http://localhost:8080`
アクセスするには、docker-compose.ymlで8080ポートにマッピングした5555ポートを使用します。
`http://localhost:5555`

ブラウザで上記にアクセスしたらVuetifyの綺麗な画面が現れます。`npm run serve`コマンドはホットリローディングしてくれるため、開発にはもってこいです。

以上で開発環境の構築は終わりです。次から設計と開発に入ります。


# プロジェクトの設計と開発
## 設計
### プロジェクト全体
結論からいうと、このようなディレクトリ構成にしました（説明しない部分は割愛）。

```bash
$ cd src
$ tree
.
├── App.vue
├── components
│   ├── Dyagnosis.vue
│   ├── Result.vue
│   └── Table.vue
├── router
│   └── index.ts
└── views
    ├── Home.vue
    ├── NotTarget.vue
    └── Target.vue
```

### App.vue
どのページでも共通のヘッダーを記述します。

### viewsディレクトリ
ここにはVueRouterで振り分けるページを用意します。
ページとしては、単純な診断サイトなので、シンプルに３画面を準備しました。

- Home.vue：診断画面
- Target.vue：対象であったときの画面
- NotTarget.vue：対象でなかったときの画面

**診断画面**
![dyagnosis.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/310081/7822d270-b688-dcba-a86b-bc33cad4ad70.png)

**対象であったときの画面**
![target.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/310081/ec499bfc-5cc4-edb4-d51e-bbdf4dd8acc0.png)

**対象でなかったときの画面**
![not-target.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/310081/e7ea1d95-d9f5-f971-2cef-713542f4b8cc.png)

URLとのマッピングはこんな感じ。

```router/index.ts
const routes: Array<RouteConfig> = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/target',
    name: 'Target',
    component: () => import('../views/Target.vue')
  },
  {
    path: '/not-target',
    name: 'NotTarget',
    component: () => import('../views/NotTarget.vue')
  }
]
```

### componentsディレクトリ
再利用性や分かりやすさを考えて、3つのコンポーネントを用意しました。

- Dyagnosis.vue
    - 診断画面のメインコンポーネント。質問データや、診断ロジックを記述。Home.vueが表示する。
- Table.vue
    - 診断中質問によって表示するテーブルコンポーネント。Dyagnosis.vueが質問によって表示を制御する。
- Result.vue
    - 診断結果コンポーネント。Target.vue、または、NotTarget.vueが表示を制御する。

## 開発
ここは細かい部分なのであまり説明しませんが、Yes/No診断を書くのが初めてだったので、正直気持ち悪いロジックになってると思います。
もしこう書くと綺麗になるよ等あればコメントで教えていただきたいです。

僕のコードでは、下記のように配列でデータを持っておき、質問はインデックス0番からスタートします。
yesかnoかの回答を得たら、そのプロパティが示すインデックス番号の質問に移る、といったロジックになっています。
苦肉の策ですが、質問の終了は-1と-2というインデックスを示しておき、その場合は診断結果のページに切り替えます。

```Dyagnosis.vue
const TARGET_INDEX = -1
const NOT_TARGET_INDEX = -2;
const questions = [
  { question: '世帯主の2月〜6月の月収が減った', yes: 1, no: NOT_TARGET_INDEX },
  { question: '世帯主の2月〜6月のいずれかの月収が下表の額以下である', yes: TARGET_INDEX, no: 2 },
  { question: '世帯主の2月〜6月の月収が半分以下になった', yes: 3, no: NOT_TARGET_INDEX },
  { question: '世帯主の2月〜6月のいずれかの月収が下表の額以下である', yes: TARGET_INDEX, no: NOT_TARGET_INDEX },
];
```

設計と開発に関しては以上になります。


# Netrifyを使ってdeploy及び公開
適用ですが、静的ホスティングが優秀と聞いていたので、今回初めてNetrifyを使ってみました。
簡単すぎて驚愕したので次からも使ってみようと思います。

## どんなURLでもindex.htmlを返すようにする
簡単といっておいてあれですが、Netrifyに単純にシングルアプリケーションをデプロイするだけだと問題がおきてしまいます。
例えば`https://corona-benefit-target.netlify.com/target`というURLでリロードするとPage Not Foundになってしまうのですが、これは
`/target`に対応したHTMLをブラウザが要求してしまうからです。

SPAでは常に`index.html`を返す必要があるので、それをNetrifyに教えてあげる必要があります。
やり方は簡単で、publicディレクトリ以下に`_redirects`というファイルを作成し、下記のように記述するだけです。

```
/* /index.html 200
```
これでどんなパスでも`index.html`を返してくれるようになります。

## ビルド
プロジェクトをビルドします。このコマンドが完了するとdistディレクトリにデプロイ用の圧縮されたファイルが置かれます。

```bash
/app # npm run build
```

## Netrifyにdeploy
[Netrify](https://app.netlify.com/)にサインアップして、先ほど作成したdistディレクトリをドラッグ＆ドロップすると、デプロイが完了します。
あり得ないほど簡単ですよね^^;

そこで生成されたURLをブラウザで開けば開発完了です！お疲れ様でした！
https://corona-benefit-target.netlify.com/


# おわりに
30万円給付対象かを判定するサイトを作ってみましたが、無駄に豪華（？）な技術を使ったことにより思ったより疲れてしまいました。

ただ、もし国のサイトとかみたくないよーって人で、この記事に偶然出逢い、偶然給付対象だとわかって、苦しい状態から少しでも脱出していただけたら幸せだなと思いながら作るのは楽しかったです。

コロナの痛みから世界が少しでも早く回復しますように。