はじめに
フロントエンドを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ライブラリも使っていきます.
- https://jp.vuejs.org/v2/guide/
- https://cli.vuejs.org/
- https://vuetifyjs.com/ja/introduction/why-vuetify/
Flaskとは
Flaskは,プログラミング言語Python用の軽量なウェブアプリケーションフレームワーク.最小限の機能を標準で提供していて,導入も簡単.今回はflask restfulという追加パッケージも利用して,REST APIも実装していきます.
Herokuとは
アプリケーションの開発から実行,運用までのすべてをクラウドで完結できるプラットフォーム.無料プランでも結構色々なことができます.デプロイも割と簡単にできます.
事前準備
予め必要なもの
私の場合,以下のような環境で開発を進めていきます.
- python : 3.7.5
- pipenv : version 2018.11.26
- Node.js : v13.8.0
- npm : 6.13.7
ディレクトリ構成
まず,以下のようにディレクトリを作成し,
myspa
└ backend
myspa
下で git init
します.
$ 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
$ 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を追加しておきます.
$ 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を選択しておけば大丈夫です.
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
部分を以下のように変更します.
"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
を以下のように,変更します.
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
の行を消します.
.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下で以下のコードを実行します.
$ npm run serve
http://127.0.0.1:8080/ にアクセスすると,以下のような画面が立ち上がっているはずです.
ちなみに,この画面はVuetifyをインストールしたときに作成される初期画面です.ここでフロントエンドの準備は一旦終わりです.
バックエンドの準備
pipenvで新規プロジェクトの初期化
myspa
下で以下のコマンドを実行して,pipenvのプロジェクトの初期化を行います.
$ pipenv --python 3.7.5
これを実行すると,myspa
下にPipfile
が生成されます.
flaskのインストール
flaskと追加パッケージであるflask restfulをインストール.myspa
下で以下を実行します.
$ pipenv install flask
$ pipenv install flask-restful
flaskアプリの作成
myspa
下にbackend
というディレクトリを作成します.
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'
としてあります.
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/
内のディレクトリの構成を少し変更します.現在は,
frontend/
├ public
│ ├ favicon.ico
│ └ index.html
└ その他諸々
となっていますが,
frontend/
├ public
│ ├ static
│ │ └ favicon.ico
│ └ index.html
└ その他諸々
に変更します.これに伴って,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
ディレクトリが生成されます.
$ npm run build
FlaskのWebサーバを起動
次にバックエンド側でWebサーバを立てて,フロントエンドで作成したWebページが読み込みます.まず,Pipfile
にサーバ起動用のスクリプトを登録しておきます.
+ [scripts]
+ start = "python backend/main.py"
これをしておくと,いちいちpipenv run python backend/main.py
を打たずに,pipenv run start
を実行するだけで,Flaskサーバが立ち上がります.
myspa
下でサーバを立ち上げると,vueで作成したWebページが見れるようになります.
$ pipenv run start
http://127.0.0.1:5000/ にアクセスすると,先程フロントエンドで読み込んだページと同じものが表示されています.ひとまず,これでフロントエンドとバックエンドが連携した開発環境の構築が出来ました.
Webアプリの改造
せっかくローカルで色々といじれる環境が出来たので,少しアプリに手を加えます.あまり複雑なものを作成するのもアレなので,「ユーザーがブラウザ上で入力した文字列をカウントする」という超単純な機能を実装しようと思います.
フロントエンドとバックエンドのデータ連携もしたいので,
- 入力された文字列をbackendに送る
- backendで文字列の長さをカウント
- frontendに結果を送る
というような構造にします(こんなのわざわざbackendで処理しなくてもいいんですけどね...笑).
ただ,ここは本筋じゃないのでざっくりと説明します.
フロントエンドの変更
今回はfrontendとbackendのデータ連携をREST APIで行います.なので,HTTP通信を実装するためにaxiosをローカルにインストールします.
$ npm install -D axios
次に,frontend/src/
下のApp.vue
を以下のように変更します.Vue.jsでは,この.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
を新たに作成します.
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
を次のように変更します.
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/ にアクセスすると,ページが変更されているはずです.
Herokuへのデプロイ
最後に,ここまでで完成したアプリをHerokuにデプロイします.アプリのデプロイの手順は大まかに以下のようになります.
- Herokuのアカウントを取得
- gunicornのインストール
- デプロイに必要な設定ファイル(Procfile,runtime.txt,requirements.txt)の作成
- heroku cliをインストール
- herokuアプリの作成
- herokuのリモートにプッシュ
herokuのアカウント取得
herokuの公式ページ( https://jp.heroku.com/ )に行き,アカウントを作成します.アカウント作成にクレジットカードの情報などは必要ありません.ほんとありがたいですね.
gunicornのインストール
gunicornはWSGIサーバ3の1つです.これを利用することで,heroku上でも作成したアプリケーションを動かせるようになります.
flaskと同様に,gunicornもpipenvでインストールします.
pipenv install gunicorn
設定ファイルの作成
myspa
下にProcfile
を作成します.今回は,backend下のmain.py
にflaskアプリを記述しているので,backend.main:app
としてます.ここで,必ず--pythonpath
をmain.py
が置かれているディレクトリに設定してください.このオプションが無いと,api.py
などの自作モジュールを読み込んでくれなくなります.
web: gunicorn -b 0.0.0.0:$PORT --pythonpath backend backend.main:app
次に,runtime.txtを作成します.ここには,使用しているpythonのバージョンを記述しておきます.
python-3.7.5
最後に,requirements.txtを作成します.ここには,インストールしたpythonパッケージのバージョン情報を記載します.手書きしてもいいですが,以下のコマンドで作成してしまったほうが楽です.
$ pipenv run pip freeze > 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コマンドが使える方は,以下のコマンドインストールしてください.
$ brew tap heroku/brew && brew install heroku
brewコマンドが使えない方は,公式のダウンロードページ( https://devcenter.heroku.com/articles/heroku-cli#download-and-install )からインストールしてください.
herokuアプリの作成
コマンドライン上でherokuにログインします.
$ heroku login
次に,herokuアプリを作成します.ここで付けた名前は,アプリのURLにも使われます.
$ heroku create 好きな名前
私の場合は,test-app-nonta
という名前でアプリを作成したので,
$ 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の設定に加えておきます.
$ git remote add heroku https://git.heroku.com/アプリ名.git
これで,デプロイの準備は終了です.
Herokuへプッシュ
最後にremoteにプッシュして,デプロイ完了です.まず,コミットします.
$ git add .
$ git commit -m "コミットメッセージ"
herokuリモートにプッシュ
$ git push heroku master
デプロイ先のURLをブラウザに打ち込むか,
$ heroku open
を実行することで,デプロイしたアプリが開きます.ちゃんと動いてることが確認できますね.
おわりに
割と簡単に,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 にサーバが立ってるようですね)
$ 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)
-
端末の画面サイズを判別することで,PCからアクセスしても,スマホやタブレットからアクセスしても,綺麗にUIを表示させることができるデザイン. ↩
-
最初はグローバルではなくローカルにVue CLIをインストールしようとしたのですが,脆弱性の警告が出てきてしまい上手くいきませんでした.なので,今回はグローバルにインストールしておきます. ↩
-
WSGI(ウィズギー)とは,Web Server Gateway Interfaceという「アプリケーションとWebサーバをつなぐインタフェースをPythonで定義したもの」です.WSGIサーバは,このWSGIの仕様に則って作成されたアプリケーションを動かすことのできるサーバを指します.WSGIやWSGIサーバについては, https://www.youtube.com/watch?v=S-InxJA5NOg&t=1578s で非常にわかりやすく説明しています. ↩