Dockerで【TypeScript+Vue+Express+MySQL】の環境を構築する方法~Vue編~

#内容
この記事は、Vueを勉強しているけどTypeScriptベースでアプリを構築したい！という方へ向けて実際に一から環境構築していく内容となっております！
ついでにDockerで構築を行いコンテナ環境下で動くアプリを作成していこうと思います！

構成としては Docker + Vue.js + Express(Node.js) + MySQLをTypeScript ベースで構築をしつつモジュール拡張性を持たせる環境を目指していこうと思います！

※初めての記事となりますので至らぬ点があるかと思いますが、優しくご指摘を頂けますと幸いです。

## 目指す状態

1. アプリケーションコンテナ
  - Vue + Vuex + Vuetify + TypeScript
2. APIサーバーコンテナ
  - Express + Sequelize + TypeScript
3. データベースコンテナ
  - MySQL

![スクリーンショット 2020-09-14 23.47.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/427188/cef4a7df-c62f-b65e-be86-e376352fe639.png)

「Vue → Vuex → HTTP通信(Axios) → API(Express) → MySQLデータ取得(Sequelize) → Storeにレスポンス(Express) →Vue」
といった流れになります。

## 対象者
- Docker,Docker-Compose がインストール済みである(利用可能な状態)
- JavaScript / Vue.jsの基礎を理解している

Docker上でさくっと環境構築することを目的にしておりDockerについて詳しく触れる内容ではございません。
Dockerってそもそも何？という方は下記の記事をご参考にしていただくことをオススメします。
[【図解】Dockerの全体像を理解する -前編-](https://qiita.com/etaroid/items/b1024c7d200a75b992fc)

## 作業手順
### Vue 編
 1. アプリケーションコンテナ作成
 2. コンテナ上でVueアプリを構築
 3. コンテナ起動時にアプリ実行を自動化
 4. Vueの主要ライブラリを導入
 5. Vuetify 導入
 6. テスト用のカウンターアプリ作成

### MySQL 編
 1. DBコンテナの作成
 2. テストデータの投入
 3. MySQLへのアクセス

[Dockerで【TypeScript+Vue+Express+MySQL】の環境を構築する方法~MySQL編~](https://qiita.com/WEBERYOTA/items/6a5ef7bdfc5bd972f92b)

### Express 編
 1. APIコンテナ作成
 2. ts-node/nodemonの導入
 3. Expressサーバー構築
 4. ルーティング設定
 5. APIハンドラーの設定
 6. APIのテンプレートを作成

[Dockerで【TypeScript+Vue+Express+MySQL】の環境を構築する方法~Express編~](https://qiita.com/WEBERYOTA/items/649cacbfb1623079b090)

### Sequelize 編
 1. コンテナ同士のネットワークを設定
 2. Sequelize 導入
 3. Model作成
 4. GetTests APIを完成させる
 5. フロント側からAxiosでAPIをコールする
 6. レスポンスをVuexにバインドして描写する

[Dockerで【TypeScript+Vue+Express+MySQL】の環境を構築する方法~Sequelize編~
](https://qiita.com/WEBERYOTA/items/e595176890cb0f435014)

##1. アプリケーションコンテナの作成
まずはdocker-composeでコンテナを作成するためのファイルを作成していきます。
下記の構成で必要なファイルを作成します。

```
root
├─ docker
│    └─ app
│        └─ Dockerfile
└─ docker-compose.yml
```
### docker-compose.yml

```yml
version: "3"
services:
  app:
    container_name: app_container
    build: ./docker/app
    ports:
      - 8080:8080
    volumes:
      - ./app:/app
    stdin_open: true
    tty: true
    environment:
      TZ: Asia/Tokyo
```
こちらが根幹になる docker-compose の設定ファイルです。
後からAPIサーバーコンテナ、DBコンテナを追加しますが一旦APPコンテナのみ作成していきます。


### Dockerfile

```yml
FROM node:12.13-alpine

RUN yarn global add @vue/cli

WORKDIR /app
```
今回はベースイメージとして軽量なalpineを採用します。
コンテナ内でvue-cliを使うのでグローバルインストールを行っておきます。

### コンテナ・ビルド

```
$ docker-compose build
```
先ほど作成した設計書を下にイメージをビルド

### コンテナ・起動
 
```
$ docker-compose up -d
```
ビルドされたコンテナを実際に起動します。
-d をつけることでデタッチドモード(バックグランド)で起動します。

### 起動中のコンテナを確認

```
$ docker ps
```
下記が出力結果となりますので実際に起動されているかチェックしましょう。

```
CONTAINER ID   IMAGE   COMMAND   CREATED   STATUS    PORTS     NAMES
// ここに作成したコンテナが表示されていればOK
```

##2. コンテナ上でVueアプリ立ち上げ
ここまででベースのコンテナができたので、実際にコンテナの中に入ってアプリを構築していきます。
 
### コンテナにアクセス

```
$ docker exec -it app_container sh

/app # 　// こうなってればコンテナにアクセスできています
```

### Vueアプリ作成

```
$ vue create app
```
マニュアル選択
TypeScript あり
Router あり
Vuex あり
Babel **なし**

バージョンは2.x。
Use class-style component syntax? Yes **※これ大事**
yarn **※パッケージはYarnでいこうと思います。**
あとはお好みで。

### 起動
```
$ cd app
$ yarn serve
```
## localhost:8080 にアクセスして表示されればOK！
![スクリーンショット 2020-09-15 0.01.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/427188/5ba8a5b0-8d93-0add-5ac4-725c0582bfd4.png)

ここまででコンテナの中にアプリを作成し、コンテナの内部からアプリを実行するところまでが完了です。
毎回この作業は面倒なのでコンテナ起動時に中のアプリケーションも自動で実行される設定を行っていきます。

### 一旦アプリを停止
```
$ controll + c
```

### コンテナから抜ける
```
$ exit
```

### コンテナの停止
```
$ docker-compose stop
```

##3. コンテナ起動時にアプリ実行を自動化
### ディレクトリ構造を変更
root直下のappディレクトリはお邪魔なのでどこかに行っていただきます。

**Before**

```
root
├─ app                // ここに移植(こいつは削除)
│    └─ app           // このディレクトリごと
│        ├─ node_modules
│        ├─ public
│        ├─ src
│     その他もろもろ
│
├─ docker
│    └─ app
│         └─ Dockerfile
└─ docker-compose.yml
```
**After**

```
root     
├─ app     
│   ├─ node_modules
│   ├─ public
│   ├─ src
│ その他もろもろ
│
├─ docker
│    └─ app
│         └─ Dockerfile
└─ docker-compose.yml
```

### Composeの設定ファイルに起動時のコマンドを追加
**docker-compose.yml**

```yml
version: "3"
services:
  app:
    container_name: app_container
    build: ./docker/app
    ports:
      - 8080:8080
    volumes:
      - ./app:/app
    stdin_open: true
    tty: true
    environment:
      TZ: Asia/Tokyo
    command: yarn serve  // こいつを追加
```
### コンテナ起動

```
$ docker-compose up -d
```

この状態でlocalhost:8080にアクセスしてアプリが立ち上がっていれば成功です！
yarn serveをしなくてもdockerコンテナが起動時にアプリも起動されるようになります！
## prettier の導入(お好み)
JavaScriptのセミコロンが嫌いなので自動整形してくれるPrettierを導入します。
このあたりはお好みでどうぞ。

### コンテナへアクセス
```
$ docker exec -it app_container sh
```

### Prettierのインストール
```
$ yarn add prettier --dev
```

app/**.prettierrc** 作成

```json
{
  "singleQuote": true,
  "semi": false
}
```

##4. 主要なVueライブラリの導入
実はこちらが今回の本命。
VueをTypeScriptで記述しつつ、デコレータ(@)を使うことですっきりと直感的に記述が可能になるライブラリを導入します。
この先Vueファイルで実際に見慣れない書き方が出てきますが、これらのライブラリがうまいことすっきりさせてくれています。

- vue-class-component
- vue-property-decorator
- vuex-class
- vuex-module-decorators

```
$ yarn add vue-class-component vue-property-decorator vuex-class vuex-module-decorators
```

下記の記事がとてもわかりやすいのでご興味ある方は是非お読みください。

- [TypeScriptではじめるVueコンポーネント（vue-class-component）](https://qiita.com/hatakoya/items/8d9968d07748d20825f8)
- [はじめてのvue-property-decorator (nuxtにも対応）](https://qiita.com/simochee/items/e5b77af4aa36bd0f32e5)


### App.vueを編集

**src/App.vue**

```vuejs

<template>
  <div id="app">
    <router-view />
  </div>
</template>

<style>
#app {
  width: 95%;
  margin: 0 auto;
}
</style>
```
表示するコンポーネントはRouter側に任せたいので router-link を削除し router-view に差し替えます。

### 不要なファイルを削除

```
├─ assets
│    └─ logo.ping        // 削除
├─ components
│     └─ HelloWorld.vue  // 削除
├─ views                 // ディレクトリごと削除
│      ├─ Home
│      └─ About
```
今後使わないファイルさんたちにはご退場いただきましょう。

### Test.vue 作成
**/pages/Test.vue**

```vuejs

<template>
  <div class="test">
    <h1>Test Page</h1>
  </div>
</template>

<script lang="ts">
import { Vue, Component } from 'vue-property-decorator'

@Component
export default class Test extends Vue {}
</script>
```
下記をチェックするためにテスト用のコンポーネントを作成します。
1. 正しくルーティングができるか
2. コンポーネントが正しく読み込めるか


### Routerを編集
**/router/index.ts**

```ts

import Vue from 'vue'
import VueRouter, { RouteConfig } from 'vue-router'
import Test from '../pages/Test.vue'

Vue.use(VueRouter)

const routes: Array<RouteConfig> = [
  {
    path: '/',
    name: 'Test',
    component: Test,
  },
]

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

export default router
```
作成したTestコンポーネントがルートで呼ばれるようにルーティングの設定を行います。

### ページ確認
リロードして「Test Page」が表示されればOK！
この要領でURLとコンポーネントをつなげていけば自由にページを増やすことができます！

##5. Vuetify 導入
### ライブラリのインストール

```
yarn add vuetify
yarn add --dev sass sass-loader fibers deepmerge
```
vuetifyだけだとエラーが出るので必要なライブラリ群も合わせてインストールします。

### プラグインファイルの作成
src/plugins/**vuetify.ts**

```ts

import Vue from 'vue'
import Vuetify from 'vuetify'
import 'vuetify/dist/vuetify.min.css'

Vue.use(Vuetify)

const opts = {}

export default new Vuetify(opts)
```
こちらはアプリ全体でVuetifyを適用するための設定ファイルです。

### main.ts編集

```ts

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import vuetify from './plugins/vuetify' // 追加
import 'vuetify/dist/vuetify.min.css' // 追加

Vue.config.productionTip = false

new Vue({
  vuetify, // 追加
  router,
  store,
  render: (h) => h(App),
}).$mount('#app')

```
上記で作成したプラグインを読み込むことでアプリケーション内でVuetifyが使えるようになります。


### App.vue 編集

```vuejs

<template>
  <v-app>  <!-- 追加 -->
    <div id="app">
      <router-view />
    </div>
  </v-app> <!-- 追加 -->
</template>
```
最後にアプリ本体のViewを囲ってあげればOKです。

### Test.vue編集

```vuejs

<v-btn large color="primary">テスト</v-btn>
```
![スクリーンショット 2020-09-17 21.05.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/427188/d1a3042f-2ed6-5d0d-21b3-768b8973acd5.png)

これでボタンが表示されていればVuetifyの導入が完了です！お疲れ様でした！
あとはこの要領でページを自由に増やしていけばOKです！

##6. テスト用のカウンターアプリ作成
ここまででアプリケーションの雛形は完成したのでVuex/Storeが正しく使えるかチェックしがてらカウンター機能を作っていきます。

### Counter.vue 作成
pages/**Counter.vue**

```vuejs

<template>
  <div class="counter">
    <h1>Counter</h1>
  </div>
</template>

<script lang="ts">
import { Vue, Component } from 'vue-property-decorator'

@Component
export default class Counter extends Vue {}
</script>
```
Counterページの作成

- router/index.ts 編集
- 
```js
import Vue from 'vue'
import VueRouter, { RouteConfig } from 'vue-router'
import Test from '../pages/Test.vue'
import Counter from '../pages/Counter.vue' //追加

Vue.use(VueRouter)

const routes: Array<RouteConfig> = [
  {
    path: '/',
    name: 'Test',
    component: Test,
  },
  { 
    path: '/counter',    // 追加
    name: 'Counter',     // 追加
    component: Counter,  // 追加
  },
]

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

export default router
```
「localhost:8080/counter」 で上記のコンポーネントがレンダリングされるようにルーティングを設定します。

### Storeモジュールの作成
store/modules/**counter.ts＊＊

```ts

import {
  getModule,
  Module,
  VuexModule,
  Mutation,
  Action,
} from 'vuex-module-decorators'
import store from '../index'

type CounterState = {
  count: number
}

@Module({ store, dynamic: true, namespaced: true, name: 'Test' })
class CounterModule extends VuexModule implements CounterState {
  count = 0

  @Mutation
  SET_COUNT(payload: number) {
    this.count = payload
  }

  @Action
  async increment() {
    this.SET_COUNT(this.count + 1)
  }

  @Action
  async decrement() {
    this.SET_COUNT(this.count - 1)
  }

  @Action
  async reset() {
    this.SET_COUNT(0)
  }
}

export const counterModule = getModule(CounterModule)
```
はじめに導入した「vuex-module-decorators」で直感的にStoreモジュールを記述することができます。
今回は "count" というデータを用いて Actionが発火された時に count が 動的に変化するかを確認していきます。 

### tsconfig.json 編集

**tsconfig.json**

```ts

"experimentalDecorators": true, // 追加
```
Linterに怒られる方はこちらを設定してください。

### Counter.vue 編集

pages/**Counter.vue**

```vuejs

<template>
  <v-card>
    <v-container>
      <v-card-title>Counter</v-card-title>
      <v-divider />

      <v-container>
        <v-avatar v-for="item in count" :key="item" color="primary">
          <span class="white--text headline">{{ item }}</span>
        </v-avatar>
      </v-container>

      <v-divider />

      <v-card-actions>
        <v-btn @click="increment">+</v-btn> <!-- クリックされた時にincrementメソッドを実行 -->
        <v-btn @click="decrement">-</v-btn> <!-- クリックされた時にdecrementメソッドを実行 -->
        <v-btn text @click="reset">Reset</v-btn> <!-- クリックされた時にresetメソッドを実行 -->
      </v-card-actions>
    </v-container>
  </v-card>
</template>

<script lang="ts">
import { Vue, Component } from 'vue-property-decorator'
import { counterModule } from '../store/modules/counter'

@Component
export default class Counter extends Vue {
  get count() {
    return counterModule.count // Store の count をそのまま返す
  }

  /**
   * カウントをプラスする
   */
  increment() {
    counterModule.increment() // Store の increment メソッドを発火
  }

  /**
   * カウントをマイナスする
   */
  decrement() {
    counterModule.decrement() // Store の decrement メソッドを発火
  }

  /**
   * カウントをリセットする
   */
  reset() {
    counterModule.reset() // Store の reset メソッドを発火
  }
}
</script>

<style lang="scss" scoped></style>
```
ここは存分にVuetifyの恩恵にあやかるとしましょう。
先ほどの count の数だけ v-avatar が生成されるように編集したらあとは動かすのみ！

## 動作確認！
[+] [-] を押してカウンターが増減すればOKです！
お疲れ様でした！

![スクリーンショット 2020-09-15 0.14.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/427188/9ca0a3db-087d-6fad-1e2c-11c9f697556c.png)

簡単なカウンターを作成してVuex(Store)の挙動を確認してみましたが、StoreのAction内でAPI通信を入れていくことで実際のDBとの連携が可能になります。次回はAPIサーバーと実際に操作するDBのコンテナ作成を行っていこうと思います！

## 次回
[Dockerで【TypeScript+Vue+Express+MySQL】の環境を構築する方法~MySQL編~](https://qiita.com/WEBERYOTA/items/6a5ef7bdfc5bd972f92b)

## 参考
[Dockerで【TypeScript+Vue+Express+MySQL】の環境を構築する方法~Express編~](https://qiita.com/WEBERYOTA/items/649cacbfb1623079b090)
[Dockerで【TypeScript+Vue+Express+MySQL】の環境を構築する方法~Sequelize編~
](https://qiita.com/WEBERYOTA/items/e595176890cb0f435014)