Vue&Typescriptで簡易的なCRUDの作成（おまけ：In Docker）

## 概要
VueとTypescriptを使用したCRUDアプリをローカルの環境で試してみた時の記録になります。
また、記事の最後のほうでDocker化してみました。なにかの参考になればと思います。

## 作業環境
```
  OS: Windows 10 Pro
  Docker: 19.03.2
  docker-compose: 1.24.1
  Node: 12.1.0
  npm: 6.9.0 
  Yarn: 1.15.2
```
Windows環境での作業になります。
上記ツール群は、WindowsOS向けパッケージマネージャー`chocolatey`でさくっとインストールしちゃいましょう。

## VueCLIの準備をする
まずは、VueCLI3をグローバル環境にインストールします。
VueCLIはVue.js開発用ツールであり、Vueで構築されたプロジェクトを作成できたりします。
`npm`もしくは`yarn`がインストールされた環境で下記のコマンドでインストールできます。

```
# npmを使用する場合
PS C:\Users\user\Desktop> npm install -g @vue/cli
# yarnを使用する場合
PS C:\Users\user\Desktop> yarn global add @vue/cli
```

<b>※vue-cliと@vue/cliの違いについて</b>
VueCLIを調べていると`vue-cli`と`@vue/cli`の2つが候補に出てくるかと思います。
これらはバージョンが異なり、パッケージ名も異なります。（`vue-cli`が2系で`@vue/cli`が3系です。）
3系のほうがプロジェクトを作成する際に、機能が豊富に取り揃えられた状態で作ってくれたり便利です。
もし2系の`vue-cli`が既にイントールされている場合は一旦アンインストールしてから3系をインストールしましょう。

```
# npmの場合
PS C:\Users\user\Desktop> npm uninstall vue-cli -g
PS C:\Users\user\Desktop> npm install -g @vue/cli
# yarnの場合
PS C:\Users\user\Desktop> yarn global remove vue-cli
PS C:\Users\user\Desktop> yarn global add @vue/cli
```

## Vueプロジェクト作成して起動する
VueCLIをインストールしたら、下記のコマンドを実行してプロジェクトを作成します。

```cmd
PS C:\Users\user\Desktop> mkdir vue-app; cd vue-app;
PS C:\Users\user\Desktop\vue-app> vue create .

Vue CLI v4.0.5
? Please pick a preset: Manually select features
? Check the features needed for your project: Babel, TS, PWA, Router, Vuex, CSS Pre-processors, Linter, Unit
? 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 CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by default): Sass/SCSS (with dart-sass)
? Pick a linter / formatter config: Basic
? Pick additional lint features: (Press <space> to select, <a> to toggle all, <i> to invert selection)Lint on save
? Pick a unit testing solution: Mocha
? Where do you prefer placing config for Babel, PostCSS, ESLint, etc.? In package.json
? Save this as a preset for future projects? No
```

`vue create .`を実行すると設定を色々聞かれます。
ここで選択した機能群はよしなに統合された状態でプロジェクトが作成されます。
※各機能についての解説は省きます。

プロジェクトが作成されたら、`yarn serve`を実行してプロジェクトを起動してみましょう。

```cmd
PS C:\Users\user\Desktop\vue-app> yarn serve
  App running at:
  - Local:   http://localhost:8080/
  - Network: http://192.168.11.10:8080/
```
上記を実行することで、ブラウザで`http://localhost:8080/`にアクセスすると画面を確認できます。
プロジェクトの起動を終了する場合はキーボードの`Ctrl+C`で終了します。

<b>※yarn serveの動き</b>
この`yarn serve`というコマンドですが、コマンドの正体は`package.json`内に記述されています。

```package.json
{
  "name": "vue-app",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "serve": "vue-cli-service serve", ←こいつ
```
実体としては`vue-cli-service serve`コマンドを実行しています。
`vue-cli-service serve`コマンドは、`webpack-dev-server`にもとづいて開発用サーバーを起動するコマンドです。
また、この開発用サーバーには、`Hot Module Replacement(HMR)`が起動しています。
`HMR`はファイルの変更を検知してビルドしなおしてくれる優れものです。
ブラウザをいちいちF5とかで更新しなくても自動更新されてくれるので開発効率がぐんぐん上がります。

## 開発用サーバーを変更してみる
先ほど、`yarn serve`を実行したら`http://localhost:8080/`に誘導されました。
8080番ポートを使用したくない時には、`vue.config.js`をプロジェクトの直下に作成してポートを変更してしまいましょう。
下記のような記述でHostやPortを変更した状態で開発用サーバを起動してくれます。

```vue.config.js
// vue.config.js
module.exports = {
    devServer: {
      host: '0.0.0.0',
      port: '8000',
    }
  }
```
上記のファイルを作成したら先ほどと同じように、`yarn serve`の実行です。

```cmd
PS C:\Users\user\Desktop\vue-app> yarn serve
  App running at:
  - Local:   http://localhost:8000/
  - Network: http://192.168.3.10:8000/
```
これで、無事に`http://localhost:8000/`上に起動させることができました。

<b>※vue.config.jsについて</b>
こちらのファイルは、VueCLIで立ち上げたプロジェクトの設定を変更するファイルです。
webpackに相当する設定はこのファイルに書いていく感じですね。
様々な設定が可能なので詳しくは下記のリファレンスを読んでみましょう。
https://cli.vuejs.org/config/#vue-config-js

## CRUDを作ってみる
それでは、簡単なCRUDアプリ（Todoリスト的なページ）を作成してみます。

#### 型定義
```/src/type/index.ts
// /src/type/index.ts
export interface Todo {
  title:string
  content:string
}
```
今回Typescriptを採用しているので、まずはTodoリストの型定義でもしてみました。
`title`と`content`をプロパティとして持つTodoオブジェクトの定義です。
この型定義ファイルを各コンポーネントやストアで利用する想定です。

続いて、ルーティングを定義していきます。

#### ルーティング定義
```/src/router/index.ts
// /src/router/index.ts
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'

Vue.use(VueRouter)

const routes = [
  {
    path: '/',
    name: 'home',
    component: Home
  }
]

const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes
})

export default router
```
プロジェクト初期構築時に生成された時からほぼ変わってません。
`localhost:8000/` にアクセスがあったときに`views/Home.vue`を描画するようにしているだけです。
※正確には`src/App.vue`に記述されている`<router-view/>`が`Home`という名のコンポーネントに置換されます。

<b>VueRouterとは</b>
上記のファイルに出現した`VueRouter`についてですが、こちらは`Vue.js`でSPAを構築するのに利用されるVue.js公式ルータです。
リクエストされたURLに対して描画したいファイルをマッピングする優れものです。
本来なら手動でプロジェクトに導入する必要がありますが、VueCLIを使えば勝手にやってくれるんですね。
VueRouterについての詳しいことはリファレンスを読みましょう。
https://router.vuejs.org/ja/

続いて、`src/App.vue`を編集します。

#### App.vue編集
```/src/App.vue
<!-- /src/App.vue -->
<template>
  <div id="app">
    <router-view/>
  </div>
</template>
```
リクエストされたURLによって`/src/views`配下のページを表示するだけのファイルです。
前述しましたが`<router-view/>`の部分がリクエストされたURLによって該当するコンポーネントに置換されます。

では、続いて`/src/views/Home.vue`を編集してみます。

#### Home.vueを編集
```/src/views/Home.vue
<!-- /src/views/Home.vue -->
<template>
  <div class="home">
    <todo-component />
  </div>
</template>

<script>
import TodoComponent from '@/components/TodoComponent.vue'

export default {
  name: 'home',
  components: {
    TodoComponent
  }
}
</script>
```
`localhost:8000/` にアクセスがあったときに描画されるページです。
`/src/components/TodoComponent.vue`というコンポーネントの登録を行っています。
`@`は`/src`から始まるパスのエイリアスです。（vue.config.jsから変更できます）
こちらの`TodoComponent.vue`に実処理をがんがん書いてきます。

<b>※コンポーネントの切り分け</b>
わざわざ`TodoComponent.vue`を用意せずに`/src/views/Home.vue`に実処理書いても問題はないですが、
propsを使った債務の切り分け等メンテナンス性を気にしたComponent設計ってのが実務では必要になります。
設計論についての詳細はググりましょう！（すみません、記事にできるほど詳しく知らないです、、）

続いて、CRUD本体部分の`TodoComponent.vue`を作成します。

#### TodoComponent.vueの作成
```/src/components/TodoComponent.vue
<!-- /src/components/TodoComponent.vue -->
<template>
  <div class="todo">
    <h1>Todo</h1>
    <div>
      <h3>登録フォーム</h3>
      <ul>
        <li><input type="text" v-model="title" placeholder="title"/></li>
        <li><input type="text" v-model="content" placeholder="content"/></li>
        <li v-if="editFlg">編集中</li>
        <li v-else><button @click="createAction">登録</button></li>
      </ul>
    </div>
    <div v-if="editFlg">
      <h3>編集フォーム</h3>
      <ul>
        <li><input type="hidden" v-model="editIndex" /></li>
        <li><input type="text" v-model="editTitle" placeholder="title"/></li>
        <li><input type="text" v-model="editContent" placeholder="content"/></li>
        <li><button @click="updateCompleteAction">編集完了</button></li>
      </ul>
    </div>
    <h3>Todo List</h3>
    <ul v-for="(item, index) in todos" v-bind:key="index">
      <li> title : {{ item.title }} </li>
      <li> content : {{ item.content }} </li>
      <li> <button @click="updateConfirmAction(index)">編集</button></li>
      <li v-if="editFlg">編集中</li>
      <li v-else><button @click="deleteAction(index)">削除</button></li>
    </ul>
  </div>
</template>

<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import { Todo } from '../type'

@Component
export default class TodoComponent extends Vue {

  todos: Array<Todo> = []
  title: string = ''
  content: string = ''
  editIndex: number = 0
  editTitle: string = ''
  editContent: string = ''
  editFlg: boolean = false

  createAction():void {
    this.todos.push({ title: this.title, content: this.content })
    this.title = ''
    this.content = ''
  }
  updateConfirmAction(index:number):void {
    this.editFlg = true
    this.editIndex = index
    this.editTitle = this.todos[index].title
    this.editContent = this.todos[index].content
  }
  updateCompleteAction():void {
    this.todos[this.editIndex].title = this.editTitle
    this.todos[this.editIndex].content = this.editContent
    this.editFlg = false
    this.editIndex = 0, this.editTitle = '', this.editContent = ''
  }
  deleteAction(index:number):void {
    this.todos.splice(index,1)
  }
}
</script>
```
実装コードは汚いですがCRUD自体は実現しました。
上記の実装は、`vue-property-decorator`というパッケージを読み込んで、
Typescriptのデコレータを活用したクラスベースなVue実装にしています。

<b>※vue-property-decoratorについて</b>
`vue-property-decorator`はVueをTypeScriptの構文で書くためのツールみたいなものです。
詳しいことは下記を参照していただければと思います。
https://www.npmjs.com/package/vue-property-decorator

## Docker化してみる
ここまでで作成したVueの環境を流行りのDocker環境に移行したいと思います。
Dockerについてはここでは触れませんが、手軽にOSレベルの仮想環境を提供するすごいやつです。
Dockerの環境さえホスト側に用意しておけば、NodeとかインストールしてなくてもVueが動いたりしてくれます。
※事前にWindows上にDockerをインストールしておきましょう。

## 作業用ディレクトリを作成する
デスクトップのどこかに適当な作業ディレクトリを用意して、今まで作ったVueプロジェクトを丸々コピーします。

```
PS C:\Users\user\Desktop> mkdir Docker
PS C:\Users\user\Desktop> cp -R vue-app Docker/vue-app
```

## Dockerfileを作成する
Dockerfileを作成します。

```bash
PS C:\Users\user\Desktop\Docker> vi Dockerfile
```
```Dockerfile
FROM node:10-alpine

ENV APP_PORT 8000
ENV APP_ROOT /app
EXPOSE $APP_PORT
WORKDIR $APP_ROOT
CMD [ "sh" ]

RUN apk update && \
    apk add git openssh curl jq && \
    curl -o- -L https://yarnpkg.com/install.sh | sh
```
Nodeの入った軽量LinuxなAlpineのイメージをベースにしています。
コンテナ内8000番ポートを外部に開放、作業ディレクトリをappディレクトリとしています。
開発に必要そうなツール（git, openssh, curl, jq, yarn等）をあらかじめインスト－ルされるようにしておきます。

## docker-compose.ymlを作成する
複数のコンテナを管理できる優れもの`docker-compose`を使ってみたいと思います。

```
PS C:\Users\user\Desktop\Docker> vi docker-compose.yml
```
```docker-compose.yml
version: '3.4'
services:

  vue-service:
    build: ./
    volumes:
    - ./vue-app:/app/
    ports: [ 8000:8000 ]
    command: sh -c "yarn serve"
```
ホスト側の8000番ポートとコンテナ側の8000番ポートをマッピングしています。
ホスト側の`localhost:8000`をブラウザからアクセスしたときにVueの画面が見れるようにしています。
Dockerfileで8000番ポートの開放、docker-compose.ymlで8000番ポートのマッピングを行っている理由は、
今回作成したVueプロジェクトの開発用サーバが8000番ポートで起動されるために必要なのと、ブラウザから確認したいからです。

## コンテナを起動する
Dockerfileとdocker-compose.ymlを作成したら下記のコマンドでコンテナを起動してみます。
※コンテナが立ち上がるまでは環境にもよりますが少し時間を要します。

```
PS C:\Users\user\Desktop\Docker> docker-compose up -d
```
ビルドが終わった後、`http://localhost:8000/`にアクセスすればVueプロジェクトが立ち上がっていると思います。

## ホットリロードの設定
私の環境（Windows 10 Pro）だとHMRがうまく起動してくれませんでした。
良い解決方法がみつからなかったので、webpackのwatcherを使って解決しました。
（`vue.config.js`に下記を追加してます。）

```vue.config.js
module.exports = {
  devServer: {
    host: '0.0.0.0',
    port: '8000',
    watchOptions: {  // ここを追記しました。
      aggregateTimeout: 300,
      poll: 1000
    }
  }
}
```

こんな感じでDocker化を終えました。
以上が、Vue&Typescript+Dockerを触ってみた記録になります。