Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
259
Help us understand the problem. What is going on with this article?
@y-tsutsu

FlaskとVue.jsでSPA Webアプリ開発

はじめに

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を使って開発を行うと便利です.

https://jp.vuejs.org

事前の開発環境

今回の自分の開発環境は次のようになっています.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でRouterVuexを追加して作成しました.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

image.png

Prettierの設定

package.jsonにPrettierの設定を追加します.Linterの内容に合わせて次の設定を行いまいました.

frontend/package.json
+   "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_foldertemplate_folderに,フロントエンドのVue.jsでBuildして出力するパスを指定しています.(このあとVue.jsの設定をこのパスにあわせて変更します.)

backend/main.py
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を用意します.

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内のファビコンを読み込んでいる個所を,変更したパスの内容に合わせて更新します.

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/img/favicon.ico">
     <title>frontend</title>
   </head>
   <body>

最後にフロントエンドのコードのBuildを行います.Buildに成功するとfrontend/distディレクトリにFlaskから読み込むファイル一式が生成されます.

$ cd frontend
$ npm run build

Webサーバの起動

ここまで準備ができたらFlaskのWebサーバを起動します.まずPipfileにサーバ起動のスクリプトを定義します.

backend/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が表示されています.

image.png

リファレンスを確認しながら,いろいろなComponentsを試してみるといいと思います.

App.vue・Home.vue・About.vueの更新

Home.vueとAbout.vueをそのまま流用して画面を作成します.App.vueにトップバーel-menuを配置して,ホームページとアバウトページへのリンクを作成します.
el-menurouterel-menu-itemrouteを指定してVue Routerと連動させます.またElement UIを使用しない場合にはrouter-linkタグを使用します.aタグではVue Routerによるページの切り替えにならないので注意です.

frontend/src/App.vue
<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>
frontend/src/views/Home.vue
<template>
  <div class="home">
    <h1>This is a home page</h1>
  </div>
</template>

<script>
export default {
  name: 'home'
}
</script>
frontend/src/views/About.vue
<template>
  <div class="about">
    <h1>This is an about page</h1>
  </div>
</template>

<script>
export default {
  name: 'home'
}
</script>

image.png

image.png

はじめのうちは,このように既存のページを流用しながら作成していくと動きが理解しやすいと思います.また更にページを追加する場合はvueファイルを新規に作成して,router.jsにHomeやAboutと同じように新しいページの設定を追加します.

frontend/router.js
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を参照してください.

backend/main.py
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 クライアントを使ってアクセスすることになります.

image.png

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()で登録します.

backend/api.py
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')
backend/main.py
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ではテーブルから読み取った情報を返すように変更しています.

backend/models.py
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()
backend/api.py
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')
backend/main.py
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()

image.png

詳細はUser Guideを参照してください.

フロントエンドでの表示

REST APIで取得した情報を画面に表示するサンプルについてまとめます.

まずフロントエンドでREST APIを扱うのに今回はaxiosを使用しました.npmからインストールを行います.

$ npm install axios

Home.vue内でAPI経由でデータを取得して画面に表示します.表示するコンポーネントはElement UIのel-tableを使用しました.el-tabletableData変数をバインディングして,APIで取得したデータでtableDataを更新することで,画面上の表示が反映されます.

frontend/src/views/Home.vue
<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>

image.png

フロントエンドのAPIモックサーバ

ここまでで,バックエンド側で用意したREST APIを使う環境は整いましたが,このままだとフロントエンド単体で開発を行うときに不便なので,フロントエンド開発用のモックのサーバをjson-serverを使って用意します.npmからインストールを行います.

$ npm install --save-dev json-server

db.jsonを作成してREST APIで扱いたいデータを定義します.ルート直下のキー名(spam)がURLと一致します.

frontend/db.json
{
    "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にアクセスすることができます.

frontend/routes.json
{
    "/api/spam": "/spam"
}

最後にjson-serverを使う場合とFlaskのサーバを使う場合とでaxiosの設定を変えたかったので,vue.jsの環境変数を利用します..env.developmentファイルを用意すると開発環境のみで有効な環境変数を定義できます.

frontend/.env.development
VUE_APP_REST_SERVER=json-mock

この環境変数を確認してaxiosのbaseURLの設定を切り替えます.

frontend/src/views/Home.vue
  <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
frontend/package.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経由でのデータの取得を動かすことができます.

image.png

その他の詳細な内容はリファレンスを確認してください.

おまけ

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()で作っています.

backend/main.py
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型に変換しています.

backend/camera.py
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を指定します.

frontend/src/views/Home.vue
<el-image class="main-image" src="/video" fit="fill"></el-image>

バックエンドのサーバを起動して確認すると,画面に動画が表示(再生)されます.

image.png

おわりに

今回はFlask(バックエンド)とVue.js(フロントエンド)を使ったSPAの開発についてまとめました.バックエンド側もそれほど多くないコード量でサーバを立ち上げることができたと思います.また今回の環境ではおまけのようなバックエンドのプラスアルファの機能を組み込むのも簡単に行えると思います.SPAの開発を行うときの一つの選択肢になるのではと思います.

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
259
Help us understand the problem. What is going on with this article?