Vue + Flask on Docker

ニフティグループ Advent Calendar 2019 5日目の記事です。
昨日は@snowwhite2019さんで、[「会社でvscode活用していますか？」](https://qiita.com/snowwhite2019/items/74c78d076c69ff551b7f)でした。VScodeを使いこなしたいと思っていたため、とても参考になりました。

# はじめに
「VueとPython使ってアプリ開発したいな〜でも環境構築面倒だな〜」
ということで、今回は実験的にVueとFlaskの開発環境をDocker上で構築します。（どちらもDockerで開発する、という記事が意外となかったのもあります。）
nginxやmysqlは使いません。とりあえずコンテナ間でリクエスト・レスポンスのやりとりができるようなものができれば良しとします。
最終的にはFlaskで作ったAPIを叩いてVueで表示させます。

Docker・Vue・Flaskとは、のような説明であったり、文法についての詳しい説明は行いません。
また、Docker for Macはインストール済みとして説明していきます。

## 環境
- macOS Mojava10.14.5
- Docker：2.0.0.3
- node v8.16.0
- npm 6.4.1
- @vue/cli 4.1.1
- Python 3.6.9
- Flask 1.1.1

## 構築の流れ
- 環境構築
    - ディレクトリ作成
    - Dockerfile作成
    - docker-compose.yml作成
    - Dockerイメージの作成とコンテナの作成
- ソースコード変更
- 動作確認

# 環境構築
## ディレクトリ
ディレクトリの初期状態。

```
.
├── Dockerfile_python
├── Dockerfile_vue
└──docker-compose.yml
```

最終的なディレクトリ構成は次の通り。

```
.
├── Dockerfile_node
├── Dockerfile_python
├── backend
│   └── app.py
├── docker-compose.yml
└── frontend
     ├── README.md
　　(略)
```

## Dockerfile
pythonイメージ・nodeイメージを作成するためのDockerfileを作成します。

### Dockerfile_python
```Dockerfile_python
FROM python:3.6

WORKDIR /usr/src/app

RUN pip install flask
```

### Dockerfile_node
```Dockerfile_node
FROM node:8.16.0-alpine

WORKDIR /usr/src/app

RUN yarn global add @vue/cli && yarn global add @vue/cli-init
```

## docker-compose.yml

Docker composeを使って、FlaskとVueのコンテナをまとめて管理します。volumesで二つのコンテナがホスト側の同じディレクトリをマウントするように指定しています。Vueでビルドしたindex.htmlをFlask側で参照する必要があるためです。

```docker-compose.yml
version: '3'
services:
  flask:
    build: 
      context: .
      dockerfile: Dockerfile_python
    container_name: flask_container
    tty: true
    volumes:
      - ./:/usr/src/app
    ports:
      - "5000:5000"
  vue:
    build:
      context: .
      dockerfile: Dockerfile_node
    image: vue_img
    container_name: vue_container
    tty: true
    volumes:
      - ./:/usr/src/app
    ports:
      - "8080:8080"
```

## Dockerイメージの作成とコンテナの生成
```
$ docker-compose build
$ docker-compose up -d
```

環境構築はこれで終了です。
あとはVueからFlaskのAPIを叩けるようにソースコードを書き換えていきます。

# ソースコード変更
- vue_containerでの変更
- flask_containerでの変更

## vue_containerでの作業
1. プロジェクト作成
2. frontend/config/index.js の変更
3. frontend/src/router/index.js の変更
4. frontend/src/components/Home.vue の作成

### 1. プロジェクト作成
vue_containerに入り、vue init でfrontendプロジェクトを作成します。（5分ほどかかる）
コマンドを叩いてすぐに色々聞かれるが、全てYes（Enter）でオーケーです。

```
$ docker-compose exec vue /bin/sh
/usr/src/app # vue init webpack frontend

/bin/sh: git: not found
? Project name frontend
? Project description A Vue.js project
? Author 
? Vue build standalone
? Install vue-router? Yes
? Use ESLint to lint your code? Yes
? Pick an ESLint preset Standard
? Set up unit tests Yes
? Pick a test runner jest
? Setup e2e tests with Nightwatch? Yes
? Should we run `npm install` for you after the project has been created? (recommended) npm

----------中略----------

# Project initialization finished!

```

プロジェクト作成完了後、frontendディレクトリが作られていることが分かります。

```
/usr/src/app # ls
Dockerfile_python   Dockerfile_node      docker-compose.yml  frontend
```

### 2. frontend/config/index.js の変更
試しにvue_containerだけでビルドとサーバの立ち上げを行い、テンプレートが表示されるか確認してみましょう。
この時、vue initでプロジェクトの作成を行った場合、frontend/config/index.js で設定されているhostのアドレスを変更しないと、http://localhost:8080 へアクセスできません！

```frontend/config/index.js
  dev: {

    // Paths
    assetsSubDirectory: 'static',
    assetsPublicPath: '/',
    proxyTable: {},

    // Various Dev Server settings
    // host: 'localhost', // can be overwritten by process.env.HOST　★★★コメントアウト★★★
    host: '0.0.0.0',  // ★★★追加★★★
    port: 8080, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined
    autoOpenBrowser: false,
```

修正したところで、ビルドとサーバの立ち上げを行います。

```
/usr/src/app # cd frontend/
/usr/src/app/frontend # npm run build
/usr/src/app/frontend # npm run dev

 DONE  Compiled successfully in 22310ms                                 

 I  Your application is running here: http://0.0.0.0:8080
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/267904/690a9e4a-5644-a425-4205-9eab1424ccb3.png)

表示できました。

### 3. frontend/src/components/index.js の変更
このファイルにはURLのパスとそれに対応して出力されるテンプレートが記述されています。

```frontend/src/components/index.js
import Vue from 'vue'
import Router from 'vue-router'
import HelloWorld from '@/components/HelloWorld'
import Home from '@/components/Home'　// ★★★追加★★★

Vue.use(Router)

export default new Router({
  // パス末尾へ'#'が追加されるのを防ぐ
  mode: 'history',　// ★★★追加★★★
  routes: [
    {
      // '/'に対して、frontend/src/components/ 配下のHelloWorld.vueファイルを参照する
      path: '/',
      name: 'HelloWorld',
      component: HelloWorld
    },
// -----追加ここから-----
    {
      // '/home'に対して、frontend/src/components/ 配下のHome.vueファイルを参照する
      path: '/home',
      name: 'Home',
      component: Home
    }
// -----追加ここまで-----
  ]
})
```

### 4. frontend/src/components/Home.vue の作成
以下のvueファイルを作成します。
scriptタグでバックエンド側のAPIを叩き、取得した乱数を変数"randomNum"に代入してtemplateタグに渡して表示しています。
scriptタグでインポートしているaxiosは、Ajax通信のためのライブラリで、Vueで外部APIを叩くことができます。

#### 注意：vueファイルでは最後に一行空行を入れないとエラーが出ます。

```frontend/src/components/Home.vue
<!-- HTMLを記述 -->
<template>
  <div>
    <p>Home</p>
    <button @click="getRandom">占う</button>
    <p>Random number from backend: {{ randomNum }}</p>
    <h1 v-if="randomNumber%4==0">Awesome!!!</h1>
    <h2 v-if="randomNumber%4==1">Good</h2>
    <h2 v-if="randomNumber%4==2">Bad...</h2>
    <h1 v-if="randomNumber%4==3">S〇〇ks!!!</h1>
    </div>
  </div>
</template>

<!-- JavaScriptを記述 -->
<script>
import axios from 'axios'

export default {
  data () {
    return {
      randomNum: 0,
    }
  },
  methods: {
    getRandom () {
      this.randomNum = this.getRandomNum()
    },
    getRandomNum () {
      const path = 'http://localhost:5000/rand'
      axios.get(path)
        .then(response => {
          this.randomNum = response.data.randomNum
        })
        .catch(error => {
          console.log(error)
        })
    }
  },
  created () {
    this.getRandom()
  }
}
</script>
<!--一行空行を入れてください-->
```

「axiosはもともとVueに入っているのかな？」
いいえ、入っていないのでインストールしましょう。

```
/usr/src/app # npm install axios
```

ファイルの変更・追加が終わったら、ビルドとサーバの立ち上げを行います。

```
/usr/src/app # cd frontend/
/usr/src/app/frontend # npm run build
/usr/src/app/frontend # npm run dev
```

これでVue側の変更は終わりです。

## flask_containerでの作業
1. backend/app.pyの作成

### 1. backend/app.py の作成

Flask側ではプロジェクト作成などがないため、こちらでディレクトリとソースコードを用意します。
frontendとディレクトリを分けるために、backendディレクトリを作成し、その中にソースファイルを作成します。
Ajax通信を行うためのライブラリ Flask-CORSのインストールも行いましょう。

```
$ docker-compose exec flask bash
/usr/src/app# mkdir backend
/usr/src/app# cd backend && touch app.py
/usr/src/app# pip install flask-cors
```

app.py を作成します。


```backend/app.py
# render_template：参照するテンプレートを指定
# jsonify：json出力
from flask import Flask, render_template, jsonify

# CORS：Ajax通信するためのライブラリ
from flask_cors import CORS
from random import *

# static_folder：vueでビルドした静的ファイルのパスを指定
# template_folder：vueでビルドしたindex.htmlのパスを指定
app = Flask(__name__, static_folder = "./../frontend/dist/static", template_folder="./../frontend/dist")

app.config.from_object(__name__)

CORS(app)

# 任意のリクエストを受け取った時、index.htmlを参照
@app.route('/', defaults={'path': ''})
@app.route('/<path:path>')
def catch(path):
    return render_template("index.html")

# '/rand'が叩かれた時、乱数を生成
@app.route('/rand')
def random():
    response = {
        'randomNum': randint(1,100)
    }
    return jsonify(response)

# app.run(host, port)：hostとportを指定してflaskサーバを起動
if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)
```

app.py を実行してサーバを起動し、localhost:5000 にアクセスします。

```
/usr/src/app# python app.py
 * Serving Flask app "app" (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: off
 * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)
```

## 動作確認
それでは、vue_containerとflask_cointainerのサーバが立ち上がった状態で、localhost:5000/home と localhost:8080/home にアクセスし、APIを叩いてみましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/267904/66348df3-7841-ac4d-d002-3dc090d13a2e.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/267904/02b35a1e-94b4-678f-b64e-43dd36ba1e75.png)


Vue_container、flask_container どちらからもアクセスし、APIを叩くことができました！！！

# あとがき
　以上、Docker composeからフロント・バックエンドのコンテナを作成し、連携させることができました。今回の構成に意味があるのかと言われると怪しいです（コンテナ行ったり来たりが面倒...）。しかし、Dockerの各種ファイルを作成して数コマンド叩けば環境ができるので、アプリ開発に集中しやすい思います。やはりDockerは便利ですね。
　詰まったところも多くあったので、別の機会に紹介したいと思います。

　明日は@yt_hattereneさんがスクラムマスターについて書いてくれるそうです！お楽しみに！！

# 参考文献
参考にさせていただきました。
[Vue.js(vue-cli)とFlaskを使って簡易アプリを作成する【前半 - フロントエンド編】](https://qiita.com/mitch0807/items/2a93d93adbf6b5fc445c)
[Vue.js(vue-cli)とFlaskを使って簡易アプリを作成する【後半 - バックエンド編】]
(https://qiita.com/mitch0807/items/c2e84beee6c9a61e86cd)