はじめに
Flask(バックエンド)とVue.js(フロントエンド)を使ってSPAの開発を行ったので,そのときの環境や手順についてまとめていきます.FlaskではSPA用のWebサーバとREST APIを動かします.Vue.jsではVue CLI 3を使って環境を構築するのと,フロントエンド単体で開発を進められるようにRESTのモックサーバなどの環境を用意します.
Flaskとは
Python製の軽量なWebアプリケーションフレームワークです.通常はテンプレートエンジンとしてJinja2を使ってHTMLを返す仕組みが一般的かと思いますが,今回はVue.jsとの組み合わせでSPAで開発を行います.また追加のパッケージを利用してREST APIの実装も行います.
http://flask.palletsprojects.com/en/1.1.x/
Vue.jsとは
WebアプリケーションのUI開発のためのJavaScriptのフレームワークです.シングルページアプリケーション(SPA)の開発が可能です.Vue.jsでアプリケーションを開発するためのコマンドラインインタフェース(CLI)ツールである,Vue CLI 3を使って開発を行うと便利です.
事前の開発環境
今回の自分の開発環境は次のようになっています.PythonとNode.jsが必要です.
- Python : 3.7.4
- pipenv : version 2018.11.26
- Node.js : v10.16.1
- npm : 6.9.0
ディレクトリ構成
ディレクトリ構成は次のようになります.バックエンドとフロントエンドで分割して,それぞれのディレクトリで環境を作っていきます.
myspa
├─backend (Flaskのバックエンド)
└─frontend (Vue.jsのバックエンド)
バックエンドとフロントエンドをまとめて管理したいので,myspaディレクトリでgit init
をしておきます.
$ mkdir myspa && cd myspa
$ git init
フロントエンドの準備
まずはVue CLI 3を使ってフロントエンドのプロジェクト作成を行います.
Vue CLI 3のインストール
npmを使ってグローバルにVue CLI 3をインストールします.
$ npm install -g @vue/cli
$ vue --version
3.10.0
プロジェクトの作成
ディレクトリ構成にあわせてfrontendという名前でVue.jsのプロジェクトを作成します.対話形式でいろいろと聞かれるので,今回はManually select featuresでRouter
とVuex
を追加して作成しました.linterはStandard
を選んでいます.はじめにgitリポジトリを作っていない場合は,自動的に作成したプロジェクトにgitリポジトリが作成されます.
$ cd myspa
$ vue create frontend
Vue CLI v3.10.0
? 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, PostCSS, ESLint, etc.? In dedicated config files
? Save this as a preset for future projects? No
今回インストールしたパッケージのバージョンは次のようになっています.
- vue: 2.6.10
- vue-router: 3.1.2
- vuex: 3.1.1
この時点で試しにサーバを起動してhttp://localhost:8080/
にアクセスするとサンプルのWelcomページが表示されます.フロントエンド単体での開発では,このサーバを使っていくことができます.
$ cd frontend
$ npm run serve
Prettierの設定
package.json
にPrettierの設定を追加します.Linterの内容に合わせて次の設定を行いまいました.
+ "prettier": {
+ "semi": false,
+ "singleQuote": true
+ }
バックエンドの準備
次にバックエンドの環境を作成します.今回はPythonの環境構築にPipenvを使用します.
Pipenvのインストール
pipからPipenvのインストールを行います.
$ python -m pip install pipenv
$ pipenv --version
pipenv, version 2018.11.26
Pipenvで初期化
ディレクトリ構成にあわせてbackendという名前でディレクトリを作成し,その中でPipenvで初期化を行います.初期化に成功すると仮想環境が作成され,ディレクトリ内にPipfile
ファイルが作成されています.
$ cd myspa
$ mkdir backend && cd backend
$ pipenv --python 3.7
Flaskのインストール
Pipenvで作成した仮想環境にFlaskのインストールを行います.
$ pipenv install flask
同様に開発に便利なパッケージのインストールを行います.コード整形,静的解析,リファクタリングを行うツールです.今回の内容とは直接関係ないですが,入れておくと便利です.
$ pipenv install --dev autopep8 flake8 rope
今回インストールしたパッケージのバージョンは次のようになっています.
- Flask: 1.1.1
- autopep8: 1.4.4
- flake8: 3.7.8
- rope: 0.14.0
Webサーバの作成
Flaskを使ってWebサーバを起動するコードを書きます.route
の設定がSPA用に一般的なFlaskのサンプルコードと少し変わっています.またstatic_folder
とtemplate_folder
に,フロントエンドのVue.jsでBuildして出力するパスを指定しています.(このあとVue.jsの設定をこのパスにあわせて変更します.)
from flask import Flask, render_template
app = Flask(__name__, static_folder='../frontend/dist/static', template_folder='../frontend/dist')
@app.route('/', defaults={'path': ''})
@app.route('/<path:path>')
def index(path):
return render_template('index.html')
if __name__ == '__main__':
app.run()
次にフロントエンドでFlaskから読み込むファイルを生成します.Flaskのコードで指定しているパスの内容にあわせて設定を変更してBuildを行います.
まずはfrontendディレクトリ直下にvue.config.js
を用意します.
module.exports = {
assetsDir: 'static',
};
次にpublic
ディレクトリ内のfavicon.ico
ファイルをstatic/img
ディレクトリに配置するために,次のようにファイルのパスを変更します.
frontend
└─public
├─favicon.ico
└─index.html
frontend
└─public
├─static
│ └─img
│ └─favicon.ico
└─index.html
次に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/img/favicon.ico">
<title>frontend</title>
</head>
<body>
最後にフロントエンドのコードのBuildを行います.Buildに成功するとfrontend/dist
ディレクトリにFlaskから読み込むファイル一式が生成されます.
$ cd frontend
$ npm run build
Webサーバの起動
ここまで準備ができたらFlaskのWebサーバを起動します.まずPipfileにサーバ起動のスクリプトを定義します.
+ [scripts]
+ start = "python main.py"
定義したスクリプトを実行してhttp://127.0.0.1:5000/
にアクセスすると,フロントエンドのローカルサーバと同様にWelcomページが表示されます.また画面上部のHome | About
のリンクもうまく切り替わり,http://127.0.0.1:5000/about
でブラウザを更新してもうまく表示されます.(SPAのサーバとしてうまく動いています.)
$ cd backend
$ pipenv run start
これでFlask(バックエンド)とVue.js(フロントエンド)を連携した環境が立ち上がりました.
Vue.jsで画面の作成
ここまではVue CLIが用意してくれたWelcomページを表示しているだけなので,次は自分で画面を作っていくとっかかりについてまとめます.
Vue.js用のUIライブラリ
今回はUIライブラリにElement UIを使いました.Element UIはVue.jsのコンポーネントライブラリです.
https://element.eleme.io
Vue CLIからプラグインとして追加することができます.
$ vue add element
? How do you want to import Element? Fully import
? Do you wish to overwrite Element's SCSS variables? No
? Choose the locale you want to load ja
Welcomページを表示すると少し内容が変わっています.Element UIのコンポーネントのel-button
が表示されています.
リファレンスを確認しながら,いろいろなComponentsを試してみるといいと思います.
App.vue・Home.vue・About.vueの更新
Home.vueとAbout.vueをそのまま流用して画面を作成します.App.vueにトップバーel-menu
を配置して,ホームページとアバウトページへのリンクを作成します.
el-menu
にrouter
,el-menu-item
にroute
を指定してVue Routerと連動させます.またElement UIを使用しない場合にはrouter-link
タグを使用します.a
タグではVue Routerによるページの切り替えにならないので注意です.
<template>
<div id="app">
<el-menu :default-active="activeIndex" mode="horizontal" router>
<el-menu-item index="home" :route="{ name:'home' }">Home</el-menu-item>
<el-menu-item index="about" :route="{ name:'about' }">About</el-menu-item>
<el-menu-item>
<a href="https://element.eleme.io" target="_blank">Link</a>
</el-menu-item>
</el-menu>
<router-view />
</div>
</template>
<script>
export default {
name: 'app',
data () {
return {
activeIndex: this.$route.name
}
}
}
</script>
<style scoped>
#app {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
text-align: center;
color: #2c3e50;
}
a {
text-decoration: none;
}
</style>
<template>
<div class="home">
<h1>This is a home page</h1>
</div>
</template>
<script>
export default {
name: 'home'
}
</script>
<template>
<div class="about">
<h1>This is an about page</h1>
</div>
</template>
<script>
export default {
name: 'home'
}
</script>
はじめのうちは,このように既存のページを流用しながら作成していくと動きが理解しやすいと思います.また更にページを追加する場合はvueファイルを新規に作成して,router.js
にHomeやAboutと同じように新しいページの設定を追加します.
import Vue from 'vue'
import Router from 'vue-router'
import Home from './views/Home.vue'
import About from './views/About.vue'
Vue.use(Router)
export default new Router({
mode: 'history',
base: process.env.BASE_URL,
routes: [
{
path: '/',
name: 'home',
component: Home
},
{
path: '/about',
name: 'about',
component: About
}
]
})
REST APIの作成
画面に表示するデータを取得,または更新するためのAPIをFlaskで作成します.また,そのAPIで取得したデータを画面に表示するコードをVue.jsで作成します.
FlaskでREST API
FlaskでREST APIを扱うのに,今回はFlask-RESTful
を使用しました.Pipenvからインストールします.
$ pipenv install flask_restful
flask_restful.Resource
を継承したクラスを作成して,GET・POST・PUT・DELETEの定義を必要に応じて行います.そのクラスをflask_restful.Api
に登録します.詳細はUser Guideを参照してください.
from flask import Flask, render_template
from flask_restful import Api, Resource
app = Flask(__name__, static_folder='../frontend/dist/static', template_folder='../frontend/dist')
api = Api(app)
class Spam(Resource):
def get(self):
return {'id': 42, 'name': 'Name'}
api.add_resource(Spam, '/api/spam')
@app.route('/', defaults={'path': ''})
@app.route('/<path:path>')
def index(path):
return render_template('index.html')
if __name__ == '__main__':
app.run()
Webサーバを起動してhttp://127.0.0.1:5000/api/spam
にアクセスすると次のようにJSONで情報が取得できていることが分かります.実際にはフロンエンドのコード内で,例えばaxios
などのHTTP クライアントを使ってアクセスすることになります.
Blueprintによる分割
Blueprint
を使うとFlaskのアプリケーション機能を分割することができ,整理がしやすくなります.分離する側でBlueprintを用意して,それをメインのFlaskオブジェクトに登録する流れになります.
前述でREST APIをmain.pyに実装していましたが,これをBlueprintを使ってapi.pyに分割します.
api.py側でBlueprintのインスタンスを用意してflask_restful.Api
にはこれを渡すようにします.またBlueprintにはurl_prefix
を指定することができます.main.py側ではこのBlueprintのインスタンスをFlask.register_blueprint()
で登録します.
from flask import Blueprint
from flask_restful import Api, Resource
api_bp = Blueprint('api', __name__, url_prefix='/api')
class Spam(Resource):
def get(self):
return {'id': 42, 'name': 'Name'}
api = Api(api_bp)
api.add_resource(Spam, '/spam')
from flask import Flask, render_template
from api import api_bp
app = Flask(__name__, static_folder='../frontend/dist/static', template_folder='../frontend/dist')
app.register_blueprint(api_bp)
@app.route('/', defaults={'path': ''})
@app.route('/<path:path>')
def index(path):
return render_template('index.html')
if __name__ == '__main__':
app.run()
FlaskでORM
REST APIで操作するデータをDBで管理するときに,今回はSQLを書く代わりにORMを使用しました.定番のSQLAlchemyを使っています.Pipenvからインストールします.
$ pipenv install flask-sqlalchemy
簡単なサンプルコードを載せておきます.db.Model
を継承したクラスを作成するとテーブルに対する各種操作を使うことができます.main.pyでDBの初期化を行い,REST APIではテーブルから読み取った情報を返すように変更しています.
from datetime import datetime
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
class SpamModel(db.Model):
__tablename__ = 'spam_table'
pk = db.Column(db.Integer, primary_key=True)
name = db.Column(db.Text)
note = db.Column(db.Text)
created_at = db.Column(db.DateTime, nullable=False, default=datetime.now)
updated_at = db.Column(db.DateTime, nullable=False, default=datetime.now, onupdate=datetime.now)
def init_db(app):
db.init_app(app)
db.create_all()
def get_all():
return SpamModel.query.order_by(SpamModel.pk).all()
def insert(name, note):
model = SpamModel(name=name, note=note)
db.session.add(model)
db.session.commit()
from flask import Blueprint
from flask_restful import Api, Resource
from models import get_all
api_bp = Blueprint('api', __name__, url_prefix='/api')
class Spam(Resource):
def get(self):
return [{'id': x.pk, 'name': x.name, 'note': x.note} for x in get_all()]
api = Api(api_bp)
api.add_resource(Spam, '/spam')
from flask import Flask, render_template
from api import api_bp
from models import get_all, init_db, insert
app = Flask(__name__, static_folder='../frontend/dist/static', template_folder='../frontend/dist')
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///myspa.db'
app.register_blueprint(api_bp)
@app.route('/', defaults={'path': ''})
@app.route('/<path:path>')
def index(path):
return render_template('index.html')
if __name__ == '__main__':
with app.app_context():
init_db(app)
if not get_all():
insert('foo', 'This is foo.')
insert('bar', 'This is bar.')
app.run()
詳細はUser Guideを参照してください.
フロントエンドでの表示
REST APIで取得した情報を画面に表示するサンプルについてまとめます.
まずフロントエンドでREST APIを扱うのに今回はaxios
を使用しました.npmからインストールを行います.
$ npm install axios
Home.vue内でAPI経由でデータを取得して画面に表示します.表示するコンポーネントはElement UIのel-table
を使用しました.el-table
とtableData
変数をバインディングして,APIで取得したデータでtableData
を更新することで,画面上の表示が反映されます.
<template>
<div class="home">
<h1>This is a home page</h1>
<el-table class="data-table" :data="tableData" stripe>
<el-table-column prop="id" label="ID" width="180"></el-table-column>
<el-table-column prop="name" label="名前" width="180"></el-table-column>
<el-table-column prop="note" label="備考"></el-table-column>
</el-table>
</div>
</template>
<script>
const axios = require('axios').create()
export default {
name: 'home',
data () {
return {
tableData: []
}
},
mounted () {
this.updataTableData()
},
methods: {
updataTableData: async function () {
const response = await axios.get('/api/spam')
this.tableData = response.data
}
}
}
</script>
<style scoped>
.data-table {
width: 80%;
margin: auto;
}
</style>
フロントエンドのAPIモックサーバ
ここまでで,バックエンド側で用意したREST APIを使う環境は整いましたが,このままだとフロントエンド単体で開発を行うときに不便なので,フロントエンド開発用のモックのサーバをjson-server
を使って用意します.npmからインストールを行います.
$ npm install --save-dev json-server
db.json
を作成してREST APIで扱いたいデータを定義します.ルート直下のキー名(spam)がURLと一致します.
{
"spam": [
{ "id": 1, "name": "foo", "note": "This is dummy foo." },
{ "id": 2, "name": "bar", "note": "This is dummy bar." }
]
}
routes.json
を用意すると,ルーティングを設定することができます.db.jsonと下記の設定とで/api/spam
のパスでREST APIにアクセスすることができます.
{
"/api/spam": "/spam"
}
最後にjson-serverを使う場合とFlaskのサーバを使う場合とでaxiosの設定を変えたかったので,vue.jsの環境変数を利用します..env.development
ファイルを用意すると開発環境のみで有効な環境変数を定義できます.
VUE_APP_REST_SERVER=json-mock
この環境変数を確認してaxiosのbaseURL
の設定を切り替えます.
<script>
- const axios = require('axios').create()
+ const axios =
+ process.env.VUE_APP_REST_SERVER === 'json-mock'
+ ? require('axios').create({ baseURL: 'http://localhost:3000' })
+ : require('axios').create()
export default {
ここまで準備ができたら,以下のコマンドでjson-serverを起動します.また,package.jsonのscripts
に登録をしておくと便利です.
$ npx json-server --watch db.json --routes routes.json
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint",
+ "json-mock": "npx json-server --watch db.json --routes routes.json"
},
json-serverを起動している状態でフロントエンドのローカルサーバを起動すると,db.jsonで定義しているデータが読み込めます.これでフロントエンド単体での開発でも,API経由でのデータの取得を動かすことができます.
その他の詳細な内容はリファレンスを確認してください.
おまけ
SPAの開発とは直接関係ないですが,今回はサーバから動画を配信してWebブラウザ上で表示するアプリを作りました.
Motion JPEGで動画配信
今回は実装が簡単そうだったのでMotion JPEGを使用しました.FlaskでのコードはStack Overflowなどにあったものが,そのまますぐ動きました.フロントエンドもimg
タグを使ってsrc
にバックエンドで用意した配信用のURLを指定するだけで動画として表示されます.
今回はサンプル用にライフゲームをバックエンドで実装し,その画像をMotion JPEGで動画として動かしました.画像の描画処理はOpenCVを使っています.Pipenvからインストールします.
$ pipenv install opencv-python
次に要点となるコードを簡単に載せておきます.
まずはmain.pyで動画配信用のルーティングを行います.gen()
で適当な間隔(今回は1秒に3回程度)で画像を送り続けています.実際の画像はLifeGameCamera.get_frame()
で作っています.
def gen(camera):
while True:
frame = camera.get_frame()
yield b'--frame\r\n' + b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n\r\n'
sleep(0.33)
@app.route('/video')
def video():
camera = LifeGameCamera()
return Response(gen(camera), mimetype='multipart/x-mixed-replace; boundary=frame')
LifeGameCameraで実際に作る画像ですが,描画処理でOpenCVを使う場合はイメージを表すデータはnumpy.ndarray(height, width, channels)
型になります.上のmain.pyのコードで必要なのがbytes
型になるので変換する必要があります.具体的にはcv2.imencode()
でJPEG形式にして,それをnumpy.ndarray.tobytes()
でbytes
型に変換しています.
import cv2
class LifeGameCamera():
def get_frame(self):
image = self.draw_image() # OpenCVを使って描画
_, encimg = cv2.imencode('.jpg', image)
return encimg.tobytes()
最後にフロントエンドですが,前述の通りimgタグ(今回はElement UIを使っているのでel-image
タグ)を使用するだけです.srcにバックエンドで追加した/video
を指定します.
<el-image class="main-image" src="/video" fit="fill"></el-image>
バックエンドのサーバを起動して確認すると,画面に動画が表示(再生)されます.
おわりに
今回はFlask(バックエンド)とVue.js(フロントエンド)を使ったSPAの開発についてまとめました.バックエンド側もそれほど多くないコード量でサーバを立ち上げることができたと思います.また今回の環境ではおまけのようなバックエンドのプラスアルファの機能を組み込むのも簡単に行えると思います.SPAの開発を行うときの一つの選択肢になるのではと思います.