Vue.js + FlaskでWebアプリケーション制作 - herokuにデプロイするまで -

# はじめに
フロントエンドをVue.js，バックエンドをFlaskでWebアプリ(SPA)を作成したので，その際の手順をまとめておきました．作成したWebアプリのherokuでの公開方法までまとめます．
個人的に，フロントとバックのデータ連携の方法や，herokuへのデプロイ方法などで，ハマりポイントを量産したので，そのあたりを詳しくまとめたいと思います．

当記事の作成にあたり，以下の記事を参考にさせていただきました．

* https://qiita.com/y-tsutsu/items/67f71fc8430a199a3efd
* https://www.sukerou.com/2018/11/flask-restful-rest-apipython-tips.html

## Vue.jsとは
WebアプリのUI作成用のjavascriptのフレームワーク．今回はVueのコマンドラインインターフェース(Vue CLI)を使って，開発を行っていきます．
レスポンシブなWebデザイン[^1]にするために，Vuetifyというとっても便利なUIライブラリも使っていきます．

[^1]: 端末の画面サイズを判別することで，PCからアクセスしても，スマホやタブレットからアクセスしても，綺麗にUIを表示させることができるデザイン．

* https://jp.vuejs.org/v2/guide/
* https://cli.vuejs.org/
* https://vuetifyjs.com/ja/introduction/why-vuetify/

## Flaskとは
Flaskは,プログラミング言語Python用の軽量なウェブアプリケーションフレームワーク．最小限の機能を標準で提供していて，導入も簡単．今回はflask restfulという追加パッケージも利用して，REST APIも実装していきます．

* https://a2c.bitbucket.io/flask/

## Herokuとは
アプリケーションの開発から実行，運用までのすべてをクラウドで完結できるプラットフォーム．無料プランでも結構色々なことができます．デプロイも割と簡単にできます．

* https://jp.heroku.com/home

# 事前準備
## 予め必要なもの
私の場合，以下のような環境で開発を進めていきます．

* python : 3.7.5
* pipenv : version 2018.11.26
    * (参考) https://qiita.com/y-tsutsu/items/54c10e0b2c6b565c887a
* Node.js : v13.8.0
* npm : 6.13.7
    * https://www.sejuku.net/blog/75691

## ディレクトリ構成
まず，以下のようにディレクトリを作成し， 

```
myspa
  └ backend
```

`myspa`下で `git init` します．

```bash
$ cd myspa
$ git init
```

(注)ちなみに，最終的に作成するディレクトリの構成は次のようになります．一応，herokuにデプロイすることまで考慮したディレクトリ構成です．

```
myspa (ルートディレクトリ)
  ├ frontend (Vue.jsの諸々)
  ├ backend (Flaskの諸々)
  ├ dist (フロントエンド側でbuildして作成されるファイル)
  ├ Pipfile (pipenvのバージョン管理情報)
  ├ Pipfile.lock (pipenvのバージョン管理情報)
  ├ Procfile (herokuのデプロイに必要なファイル)
  ├ requirements.txt
  └ runtime.txt (herokuのデプロイに必要なファイル)
```

# フロントエンドの準備
Veu CLIを使って，フロントエンドの準備をします．

## Vue CLIのインストール
npmを使って，Vue CLIをグローバルにインストールします．[^2]

[^2]: 最初はグローバルではなくローカルにVue CLIをインストールしようとしたのですが，脆弱性の警告が出てきてしまい上手くいきませんでした．なので，今回はグローバルにインストールしておきます．

```bash
$ npm install -g @vue/cli
$ vue --version
@vue/cli 4.2.3
```

## Vueプロジェクトの作成
Vue CLIを使って新規にvueのプロジェクトを作成します．最終的なディレクトリ構造を踏まえて，frontendという名前で作成しておきます．`vue create プロジェクト名`を実行すると，対話形式で細かな設定を確認されます．
後々使うことになるので，2番目の質問 `Check the features needed for your project`でBabel，Router，Vuex，Linterを追加しておきます．

```bash:myspa/
$ vue create frontend

Vue CLI v4.2.3
? Please pick a preset: Manually select features
? Check the features needed for your project: Babel, Router, Vuex, Linter
? Use history mode for router? (Requires proper server setup for index fallback in production) Yes
? Pick a linter / formatter config: Standard
? Pick additional lint features: (Press <space> to select, <a> to toggle all, <i> to invert selection)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
```

ここまで実行すると，`myspa`下に`frontend`というディレクトリが作成されているはずです．

## Vuetifyの追加
Vueコマンド使って，Vuetifyを加えます．frontendディレクトリ下に移動して，以下のコマンドを実行します．インストール途中でプリセットを聞かれるのですが，Defaultを選択しておけば大丈夫です．

```bash:myspa/frontend/
vue add vuetify
? Choose a preset: Default (recommended)
```

## configなどの設定
ここまで実行すると，frontendディレクトリは以下のようになっているはずです．

```
frontend
  ├ node_modules
  ├ public
  ├ package.json
  ├ vue.config.js
  ├ .gitignore
  └ その他諸々のファイル
```
ここで，`package.json`と`vue.config.js`，`.gitignore`を以下のように編集します．

### package.jsonの編集
元々の`package.json`の`scripts`部分を以下のように変更します．

```diff:package.json
"scripts": {
    "serve": "vue-cli-service serve",
-    "build": "vue-cli-service build",
+    "build": "vue-cli-service build --dest ../dist",
    "lint": "vue-cli-service lint"
  },
```

これにより，`npm run build`(後で出てくる)を実行した際に，`dist`ディレクトリ(バックエンド側が読み込むファイル類が格納されるフォルダ)が`myspa`の直下に生成されるようになります．

### vue.config.jsの編集
元々の`vue.config.js`を以下のように，変更します．

```diff:vue.config.js
module.exports = {
  transpileDependencies: [
    'vuetify'
  ],
+ assetsDir: 'static',
+ devServer: {
+   port: 8080,
+   host: '127.0.0.1'
+ }
}
```

`devServer`の設定を変更したことで， `npm run serve`を実行した際に http://127.0.0.1:8080/ に簡易サーバが立つようになります．

### .gitignoreの編集
元々の`.gitignore`から, `/dist`の行を消します．

```diff:.gitignore
.DS_Store
node_modules
- /dist

# local env files
.env.local
.env.*.local

# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
```

今回使用するherokuは，gitを使ってリモートレポジトリにプッシュします．なので，`/dist`が`.gitignore`に登録されていると，herokuにデプロイするときに，「distディレクトリが見つかりません！」というエラーを吐くようになってしまいます．

## 簡易サーバーの起動
ここまで，出来たら開発用の簡易サーバーが立ち上がるようになっています．frontend下で以下のコードを実行します．

```bash:myspa/frontend/
$ npm run serve
```

http://127.0.0.1:8080/ にアクセスすると，以下のような画面が立ち上がっているはずです．
<img width="1388" alt="SCS01.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610526/77df1d48-2ab0-c40e-75aa-e3d7d30a9641.png">
ちなみに，この画面はVuetifyをインストールしたときに作成される初期画面です．ここでフロントエンドの準備は一旦終わりです．

# バックエンドの準備
## pipenvで新規プロジェクトの初期化
`myspa`下で以下のコマンドを実行して，pipenvのプロジェクトの初期化を行います．

```bash:myspa/
$ pipenv --python 3.7.5
```

これを実行すると，`myspa`下に`Pipfile`が生成されます．

## flaskのインストール
flaskと追加パッケージであるflask restfulをインストール．`myspa`下で以下を実行します．

```bash:myspa/
$ pipenv install flask
$ pipenv install flask-restful
```

## flaskアプリの作成
`myspa`下に`backend`というディレクトリを作成します．

```plaintext:現在のディレクトリ構成
myspa
  ├ frontend
  ├ backend (今作成したディレクトリ)
  ├ Pipfile
  └ Pipfile.lock
```

`backend`に移動し，`main.py`を作成します．中身は，公式ドキュメント( https://a2c.bitbucket.io/flask/quickstart.html#id2 )に詳しく書いてあります．日本語でも書かれてるので，かなりわかりやすいと思います．
`static_folder=...`と`template_folder=...`のところは，`npm run build`を実行して作成される`dist`ディレクトリに合わせます．フロントエンドの準備をする際に，`dist`は`myspa`直下に生成されるように設定したので，今回は`template_folder='../dist'`としてあります．

```python:backend/main.py
from flask import Flask, render_template
import os


app = Flask(__name__, static_folder='../dist/static', template_folder='../dist')

@app.route('/', defaults={'path': ''})
@app.route('/<path:path>')
def index(path):
    return render_template('index.html')

if __name__ == '__main__':
    app.run()
```

ここで，バックエンドの準備も一旦終わりです．

# フロントエンドとバックエンドを繋げる
`frontend/`内のディレクトリの構成を少し変更します．現在は，

```plaintext:今のディレクトリ構成
frontend/
  ├ public
  │  ├ favicon.ico
  │  └ index.html
  └ その他諸々
```

となっていますが，

```plaintext:変更後のディレクトリ構成
frontend/
  ├ public
  │  ├ static
  │  │  └ favicon.ico
  │  └ index.html
  └ その他諸々
```

に変更します．これに伴って，`public`内の`index.html`の参照箇所も変更しておきます．

```diff:frontend/public/index.html
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
-   <link rel="icon" href="<%= BASE_URL %>favicon.ico">
+   <link rel="icon" href="<%= BASE_URL %>static/favicon.ico">
    <title><%= htmlWebpackPlugin.options.title %></title>
    <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900">
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@latest/css/materialdesignicons.min.css">
  </head>
```

## フロントエンドのコードをBuildする
`frontend`に移動し，以下を実行．ビルドに成功すると，`myspa`直下に`dist`ディレクトリが生成されます．

```bash:frontend/
$ npm run build
```

## FlaskのWebサーバを起動
次にバックエンド側でWebサーバを立てて，フロントエンドで作成したWebページが読み込みます．まず，`Pipfile`にサーバ起動用のスクリプトを登録しておきます．

```diff:myspa/Pipfile
+ [scripts]
+ start = "python backend/main.py"
```

これをしておくと，いちいち`pipenv run python backend/main.py`を打たずに，`pipenv run start`を実行するだけで，Flaskサーバが立ち上がります．

`myspa`下でサーバを立ち上げると，vueで作成したWebページが見れるようになります．

```bash:myspa/
$ pipenv run start
```

http://127.0.0.1:5000/ にアクセスすると，先程フロントエンドで読み込んだページと同じものが表示されています．ひとまず，これでフロントエンドとバックエンドが連携した開発環境の構築が出来ました．
<img width="1389" alt="SCS02.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610526/60179383-da48-6664-87b8-15507f26e63c.png">

# Webアプリの改造
せっかくローカルで色々といじれる環境が出来たので，少しアプリに手を加えます．あまり複雑なものを作成するのもアレなので，「ユーザーがブラウザ上で入力した文字列をカウントする」という超単純な機能を実装しようと思います．
フロントエンドとバックエンドのデータ連携もしたいので，

1. 入力された文字列をbackendに送る
2. backendで文字列の長さをカウント
3. frontendに結果を送る

というような構造にします(こんなのわざわざbackendで処理しなくてもいいんですけどね...笑)．
ただ，ここは本筋じゃないのでざっくりと説明します．

## フロントエンドの変更
今回はfrontendとbackendのデータ連携をREST APIで行います．なので，HTTP通信を実装するためにaxiosをローカルにインストールします．

```bash:frontend/
$ npm install -D axios
```

次に，`frontend/src/`下の`App.vue`を以下のように変更します．Vue.jsでは，この.vue拡張子のファイルに手を加えて，見た目を変えていきます．

```html:frontend/src/App.vue
<template>
  <v-app>
    <v-container fluid>
      <v-row align="start" justify="center">
        <v-col cols="10">
          <v-textarea
          outlined
          name="input-7-4"
          label="テキストを入力してください"
          v-model="InputText"
        ></v-textarea>
        </v-col>
        <v-col cols="2">
          <v-btn outlined @click="SendData"> 文字数をカウント </v-btn>
        </v-col>
      </v-row>

      <v-row align="start" justify="center">
        <v-col cols="6">
        <v-card
          max-width="450"
          class="mx-auto"
        >
          <v-toolbar
            dark
          >
            <v-toolbar-title>Result</v-toolbar-title>
          </v-toolbar>

          <v-list three-line>
            <template v-for="(item, index) in items">
              <v-list-item
                :key="item.title"
              >
                <v-list-item-content>
                  <v-list-item-title >{{ item.count }}文字です</v-list-item-title>
                  <v-list-item-subtitle> {{ item.text }} </v-list-item-subtitle>
                </v-list-item-content>
              </v-list-item>
              <v-divider
                :key="index"
                :inset="item.inset"
              ></v-divider>
            </template>
          </v-list>
        </v-card>
        </v-col>
      </v-row>
    </v-container>
  </v-app>
</template>

<script>
import axios from 'axios'

export default {
  name: 'App',

  data () {
    return {
      // 入力データ
      InputText: '',
      TextLength: null,
      items: []
    }
  },

  methods: {
    SendData: function () {
      var data = { text: this.InputText }

      axios
        .post('/api/post', data)
        .then(response => {
          this.items.push(response.data)
        })
        .catch(err => {
          alert('APIサーバと接続できません')
          err = null
        })
    }
  }
}
</script>
```

`frontend`下で`npm run build`をして，`dist`ディレクトリを更新．

## バックエンド側の変更
まず，以下の`api.py`を新たに作成します．

```python:backend/api.py
from flask import Blueprint, jsonify, request, session
from flask_restful import Api, Resource
import json

# postされたテキストをカウントするapi(POSTメソッド)
text_count_bp = Blueprint('text_count', __name__, url_prefix='/api/post')
class TextCount(Resource):
    def post(self):

        # postされたデータを読み込み
        input_data = request.json

        # 入力文字列の文字数をカウント
        result_data = {'text':input_data['text'], 'count':len(input_data['text'])}

        return jsonify(result_data)

text_count = Api(text_count_bp)
text_count.add_resource(TextCount, '')
```

次に，`main.py`を次のように変更します．

```python:backend/main.py
from flask import Flask, render_template
from api import text_count_bp


app = Flask(__name__, static_folder='../dist/static', template_folder='../dist')
app.register_blueprint(text_count_bp)

@app.route('/', defaults={'path': ''})
@app.route('/<path:path>')
def index(path):
    return render_template('index.html')

if __name__ == '__main__':
    app.run()
```

ここで，`myspa`下で`pipenv run start`を実行して， http://127.0.0.1:5000/ にアクセスすると，ページが変更されているはずです．
<img width="1389" alt="SCS03.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610526/413fa206-6b62-ef08-b748-6be62981b8ac.png">

# Herokuへのデプロイ
最後に，ここまでで完成したアプリをHerokuにデプロイします．アプリのデプロイの手順は大まかに以下のようになります．

1. Herokuのアカウントを取得
2. gunicornのインストール
3. デプロイに必要な設定ファイル(Procfile，runtime.txt，requirements.txt)の作成
4. heroku cliをインストール
5. herokuアプリの作成
6. herokuのリモートにプッシュ

## herokuのアカウント取得
herokuの公式ページ( https://jp.heroku.com/ )に行き，アカウントを作成します．アカウント作成にクレジットカードの情報などは必要ありません．ほんとありがたいですね．

## gunicornのインストール
gunicornはWSGIサーバ[^3]の1つです．これを利用することで，heroku上でも作成したアプリケーションを動かせるようになります．

[^3]: WSGI(ウィズギー)とは，Web Server Gateway Interfaceという「アプリケーションとWebサーバをつなぐインタフェースをPythonで定義したもの」です．WSGIサーバは，このWSGIの仕様に則って作成されたアプリケーションを動かすことのできるサーバを指します．WSGIやWSGIサーバについては， https://www.youtube.com/watch?v=S-InxJA5NOg&t=1578s で非常にわかりやすく説明しています．

flaskと同様に，gunicornもpipenvでインストールします．

```bash:myspa/
pipenv install gunicorn
```

## 設定ファイルの作成
`myspa`下に`Procfile`を作成します．今回は，backend下の`main.py`にflaskアプリを記述しているので，`backend.main:app`としてます．ここで，必ず`--pythonpath`を`main.py`が置かれているディレクトリに設定してください．このオプションが無いと，`api.py`などの自作モジュールを読み込んでくれなくなります．

```plaintext:myspa/Procfile
web: gunicorn -b 0.0.0.0:$PORT --pythonpath backend backend.main:app
```

次に，runtime.txtを作成します．ここには，使用しているpythonのバージョンを記述しておきます．

```plaintext:myspa/runtime.txt
python-3.7.5
```

最後に，requirements.txtを作成します．ここには，インストールしたpythonパッケージのバージョン情報を記載します．手書きしてもいいですが，以下のコマンドで作成してしまったほうが楽です．

```bash:myspa/
$ pipenv run pip freeze > requirements.txt
```

実行すると，以下のようなファイルが作成されます．

```plaintext:myspa/requirements.txt
aniso8601==8.0.0
click==7.1.1
Flask==1.1.1
Flask-RESTful==0.3.8
gunicorn==20.0.4
itsdangerous==1.1.0
Jinja2==2.11.1
MarkupSafe==1.1.1
pytz==2019.3
six==1.14.0
Werkzeug==1.0.0
```

## heroku cliのインストール
heroku cliはコマンドラインからheroku関連の操作をすることができるツールです．brewコマンドが使える方は，以下のコマンドインストールしてください．

```bash
$ brew tap heroku/brew && brew install heroku
```

brewコマンドが使えない方は，公式のダウンロードページ( https://devcenter.heroku.com/articles/heroku-cli#download-and-install )からインストールしてください．

## herokuアプリの作成
コマンドライン上でherokuにログインします．

```bash
$ heroku login
```

次に，herokuアプリを作成します．ここで付けた名前は，アプリのURLにも使われます．

```bash
$ heroku create 好きな名前
```

私の場合は，`test-app-nonta`という名前でアプリを作成したので，

```bash
$ heroku create test-app-nonta
Creating ⬢ test-app-nonta... done
https://test-app-nonta.herokuapp.com/ | https://git.heroku.com/test-app-nonta.git
```

のように，アプリがデプロイされるURLとgitのリモートレポジトリを返してくれます．
最後に，herokuが返してくれたリモートレポジトリの設定をgitの設定に加えておきます．

```bash
$ git remote add heroku https://git.heroku.com/アプリ名.git
```

これで，デプロイの準備は終了です．

## Herokuへプッシュ
最後にremoteにプッシュして，デプロイ完了です．まず，コミットします．

```bash:myspa/
$ git add .
$ git commit -m "コミットメッセージ"
```

herokuリモートにプッシュ

```bash:myspa/
$ git push heroku master
```

デプロイ先のURLをブラウザに打ち込むか，

```bash
$ heroku open
```

を実行することで，デプロイしたアプリが開きます．ちゃんと動いてることが確認できますね．
<img width="1388" alt="SCS04.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610526/b93d6d24-38ae-8097-287d-1a7ea5bc82e3.png">

# おわりに
割と簡単に，Vue.js + Flask + HerokuでWebアプリを作れたのではないかと思います．今回は簡単な動作のアプリケーションしか実装しませんでしたが，少しいじるだけで色々な機能を追加で実装することができると思います．
今後は，Vue+flaskアプリにgoogle firebaseのAuthenticationを使ったログイン機能や，leaflet.jsでの地図描画機能を追加する方法なんかもまとめていこうと思います．

# 個人的にハマった・詰まったポイント
最後に私自身がハマって，時間を取られたポイントをまとめておこうと思います．

## 1. distディレクトリの位置
herokuは，gitの機能を使ってデプロイを行います．vue cliでプロジェクト作成すると，自動で`.gitignore`も作成され，その中に`/dist`も含まれてしまっています．このまま何もしないと，herokuのリモートレポジトリにdistディレクトリはプッシュされないので，`.gitignore`から`/dist`を外すことと，`dist`をルートディレクトリに生成するように設定を変更することは忘れないようにしてください．

## 2. Procfileのpythonpath
ここには，かなりハマりました．Heroku(Procfile)の仕様というより，gunicornの仕様によるものです．今回のように複数ファイルに分けてを記述してる場合，`--pythonpath`の設定をしないと自作モジュールを正しく読み込んでくれなくなります．
一応，herokuにデプロイする前に，ローカルでgunicornサーバを立てて，動作するか確認しておくといいかもしれないですね．以下のコマンドでgunicornのサーバをローカルでて立てれます．( http://127.0.0.1:8000 にサーバが立ってるようですね)

```bash:myspa/
$ pipenv run gunicorn --pythonpath backend backend.main:app
[2020-03-30 02:42:23 +0900] [93950] [INFO] Starting gunicorn 20.0.4
[2020-03-30 02:42:23 +0900] [93950] [INFO] Listening at: http://127.0.0.1:8000 (93950)
```