Vue.jsとFlaskでフルスタックなWebアプリの開発環境を構築 その1〜〜環境構築〜〜

はじめに

最近のWebアプリはReact.jsやVue.jsを使用したSPAが主流かと思いますが、データ取得のAPIを実装したりなど、サーバサイドの実装も必要になります。

自分はJavascriptに不慣れなのでサーバサイドはJavascriptではなくPythonを使いたかったので、Flaskと共存できる構成にしたかったのがきっかけです。

いろいろと探してみたらすでに先人がやっていました。

Full-stack single page application with Vue.js and Flask
Single Page Apps with Vue.js and Flask

以下のような構成で開発できるようにします。

app_dir
  ┗frontend  // Vue.jsのプログラム
  ┗backend   // Flaskのプログラム

英語記事なのと、それぞれの記事で若干やっていることが異なるため
それぞれを混ぜる形で環境を構築したので、備忘として自分の言葉でまとめておこうと思います。

vue-cliで雛形のインストール(フロントエンド)

以下コマンドでvue-cliをインストールできます。

$ npm install -g vue-cli

アプリの雛形をインストールします。

$ vue init webpack frontend

? Project name (frontend)[Enter]
? Project description (A Vue.js project)[Enter]
? Author (hogehoge <hogehogehogehoge@gmail.com>)[Enter]
? Vue build (Use arrow keys)[Enter]
❯ Runtime + Compiler: recommended for most users
  Runtime-only: about 6KB lighter min+gzip, but templates (or any Vue-specific HTML) are ONLY allowed
 in .vue files - render functions are required elsewhere
? Install vue-router? (Y/n)[Enter]
? Use ESLint to lint your code? (Y/n)[Enter]
? Pick an ESLint preset (Use arrow keys)
❯ Standard (https://github.com/standard/standard)
  Airbnb (https://github.com/airbnb/javascript)
  none (configure it yourself)
? Set up unit tests (Y/n)[Enter]
? Pick a test runner (Use arrow keys)[Enter]
❯ Jest
  Karma and Mocha
  none (configure it yourself)
? Setup e2e tests with Nightwatch? (Y/n)[Enter]
? Should we run `npm install` for you after the project has been created? (recommended) (Use arrow keys)
❯ Yes, use NPM
  Yes, use Yarn
  No, I will handle that myself

  vue-cli · Generated "frontend".


# Installing project dependencies ...
# ========================

(略)

# Project initialization finished!
# ========================

To get started:

  cd frontend
  npm run dev

Documentation can be found at https://vuejs-templates.github.io/webpack
$ cd frontend
$ npm install

# after installation
$ npm run dev

DONE  Compiled successfully in 21497ms                                                      12:42:34

I  Your application is running here: http://localhost:8080

ブラウザでhttp://localhost:8080を開いて以下の画面が表示されれば、アプリの雛形インストール完了です。

サンプルページの追加

Vue.jsの構成や説明はここでは記載しませんが、練習用にページを追加してみましょう。 frontend/src/componentsHome.vueAbout.vueを追加します。

// Home.vue
<template>
  <div>
    <p>Home page</p>
  </div>
</template>
// About.vue
<template>
  <div>
    <p>About</p>
  </div>
</template>

また、frontend/src/router/index.jsを以下のように編集します。

import Vue from 'vue'
import Router from 'vue-router'
// もともと記載されていたものは削除orコメントアウト
// import HelloWorld from '@/components/HelloWorld'

// Vue.use(Router)

// export default new Router({
//   routes: [
//     {
//       path: '/',
//       name: 'HelloWorld',
//       component: HelloWorld
//     }
//   ]
// })

const routerOptions = [
  { path: '/', component: 'Home' },
  { path: '/about', component: 'About' }
]

const routes = routerOptions.map(route => {
  return {
    ...route,
    component: () => import(`@/components/${route.component}.vue`)
  }
})

Vue.use(Router)

export default new Router({
  routes,
  mode: 'history'
})

このように編集したら再度ブラウザでlocalhost:8080localhost:8080/aboutを開いてページが表示されたらOKです。

ビルドディレクトリの変更

npm run buildコマンドで立ち上がるのは開発用のサーバなので、実際にサービス提供する際はブラウザが解釈できる形にビルドする必要があります。
そのビルドした結果がどこに出力されるかはfrontend/config/index.jsに記載があります。
もともとの記載は、

build: {
  // Template for index.html
  index: path.resolve(__dirname, '../dist/index.html'),

  // Paths
  assetsRoot: path.resolve(__dirname, '../dist'),
  //////以下略//////
}

となっており、frontend/dist配下にビルドされたソースコードが出力される設定となっております。
このままだとFlaskでindex.htmlにアクセスするときにfrontend配下を参照しなくてはならなくなってしまうので、管理上明確に分離するためにfrontendの上の階層に出力されるようにします。

build: {
  // Template for index.html
  index: path.resolve(__dirname, '../../dist/index.html'),

  // Paths
  assetsRoot: path.resolve(__dirname, '../../dist'),
  //////以下略//////
}

以下コマンドでビルドします。

$ npm run build

以下のような構成になります。

app_dir
  ┗frontend  // Vue.jsのプログラム
  ┗dist   // コンパイルされたVue.jsのプログラム
$ ls ../dist/
index.html static

Flaskのインストール(バックエンド)

バックエンドのプログラムには今回はFlaskを使用します。
元記事にはPythonの環境を整えるのにvirtualenvを使用していますが、僕はpyenv(pyenv-virtualenv)のほうが使い慣れているのでこちらを使います。
pyenvは以下コマンドでインストールできます。

$ brew install pyenv
$ echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.bash_profile
$ echo 'export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.bash_profile
$ echo 'eval "$(pyenv init -)"' >> ~/.bash_profile
$ source ~/.bash_profile

以下コマンドで本アプリ用のPython3.6.1の環境を作ります。

$ pyenv install --list
$ pyenv install 3.6.1
$ pyenv virtualenv 3.6.1 vue-flask
$ pyenv versionos
system
3.6.1
3.6.1/envs/vue-flask
vue-flask

app_dirに行き、以下のコマンドを実行します。

$ pyenv local vue-flask
(vue-flask) $ # このようにプロンプトが変わったらOK

これで、app_dir内に閉じたPython3.6.1の環境が作れました。
(この中でパッケージのインストールを行っても他の環境には影響ありません。)

以下コマンドでFlaskをインストールします。

$ pip install Flask
$ mkdir backend

Flaskのサンプル作成

いよいよFlaskでバックエンドのプログラムを作成していきます。
今回は以下のようなファイル構成にしようと思います。

app_dir
 ┗appserver.py
 ┗frontend/  
 ┗backend/
   ┗api.py
   ┗application.py
   ┗config.py
   ┗router.py

それぞれのファイルの中身は以下です。

# appserver.py
from backend.application import create_app
from flask import render_template
app = create_app()

@app.route('/')
def index():
    return render_template("index.html")

if __name__ == '__main__':
    app.run()
# application.py
from flask import Flask

def create_app(app_name='FLASK-VUE'):
    app = Flask(app_name,
                static_folder = "./dist/static",
                template_folder = "./dist")
    app.config.from_object('backend.config.BaseConfig')

    return app
# config.py
class BaseConfig(object):
    DEBUG = True

ポイントは、application.pyにおいて、

app = Flask(app_name,
            static_folder = "./dist/static",
            template_folder = "./dist")

を指定することです。
これを指定することで、Flaskのテンプレート参照先が./dist配下になり、Vue.jsでビルドしたindex.htmlが参照されることになります。

以下コマンドで、Flaskの開発用サーバが立ち上がります。

$ python appserver.py
* Serving Flask app "FLASK-VUE" (lazy loading)
* Environment: production
  WARNING: Do not use the development server in a production environment.
  Use a production WSGI server instead.
* Debug mode: on
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
* Restarting with stat
* Debugger is active!
* Debugger PIN: 977-628-540

http://localhost:5000/にアクセスすると、先程vue.jsで作成したホーム画面が表示されます。
しかし、vue.js側でルーティングを実装したはずの、http://localhost:5000/aboutにアクセスしても、Not Foundとなってしまいます。
これは、Flask側ではルーティングをしていないにもかかわらず、Flask側で処理しようとしてしまっているためです。
そこで以下をappserver.pyに追記して、/以下のパスをvue.jsでビルドしたindex.htmlにリダイレクトするようにします。

# appserver.py
from backend.application import create_app
from flask import render_template
app = create_app()

#### 以下を追記 ###
@app.route('/', defaults={'path': ''})
@app.route('/<path:path>')
def catch_all(path):
    return render_template("index.html")
#################

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

再度、http://localhost:5000/aboutにアクセスするとAboutページが表示されます。

404ページの追加

これで、ブラウザ上のルーティングがvue.jsでビルドしたindex.htmlにリダイレクトされるので、vue.js側で404ページも追加しておかなくてはなりません。
frontend/src/components配下に以下NotFound.vueを作成します。

// NotFound.vue
<template>
  <div>
    <p>404 - Not Found</p>
  </div>
</template>

また、frontend/src/router/index.jsに以下の通り、ページがない場合のパスを追記します。

const routerOptions = [
  { path: '/', component: 'Home' },
  { path: '/about', component: 'About' },
  { path: '*', component: 'NotFound' }
]

http://localhost:8080/配下の適当なパスにアクセスしてNotFoundのページが表示されればOKです。
npm run buildでビルドすればFlask側のサーバhttp://localhost:5000/でも同様にNotFoundのページが表示されます。

APIの追加

さて、基本的な環境は整ってきたので、バックエンド側にAPIエンドポイントを追加していきたいと思います。

バックエンド(Flask)→APIエンドポイントの追加
フロントエンド(Vue.js)→APIアクセスし結果を描画

まずはFlask側にAPIに対するルーティングを追加します。
index.htmlに対するルーティングと同様に、そのままappserver.pyに記載しても良いのですが、APIのルーティングは別ファイルで管理したいので、FlaskのBluprintを使用します。

backend配下にapi.pyというファイルを作成します。

from flask import Blueprint, jsonify, request
from random import *

api = Blueprint('api', __name__)

@api.route('/hello/<string:name>/')
def say_hello(name):
    response = { 'msg': "Hello {}".format(name) }
    return jsonify(response)

@api.route('/random')
def random_number():
    response = {
        'randomNumber': randint(1, 100)
    }
    return jsonify(response)

今回は試しに名前に対してHelloのメッセージを返すAPIと乱数を返すAPIを作成しました。
backend/application.pyにてこのAPIのルーティングを記載したBlueprintファイルをimportします。

# application.py
from flask import Flask

def create_app(app_name='FLASK-VUE'):
    app = Flask(app_name,
                static_folder = "./dist/static",
                template_folder = "./dist")
    app.config.from_object('backend.config.BaseConfig')

    ##### ここを追記 #####
    from backend.api import api
    app.register_blueprint(api, url_prefix="/api")
    ####################

    return app

url_prefix="/api"を指定しているので、ここで記載するルーティングはすべて/api/配下のルーティングとなります。

http://localhost:5000/api/randomにアクセスすると以下の結果

{
"randomNumber": 38
}

http://localhost:5000/api/hello/hogeにアクセスすると以下の結果

{
"msg": "Hello hoge"
}

上記のような結果になればOKです。

次にfrontend配下のVue.jsのプログラム側でこのAPIにリクエストをし、画面表示させたいと思います。

まずは、Vue.jsでajaxを利用するために、axiosをインストールします。

$ cd frontend
$ npm install --save axios

frontend/src/components/Home.vueを以下のように書き換えます。

// Home.vue
<template>
  <div>
    <p>Home page</p>
    <p>Random number from backend: {{ randomNumber }}</p>
    <button @click="getRandom">New random number</button>
  </div>
</template>

<script>
import axios from 'axios'
export default {
  data () {
    return {
      randomNumber: 0
    }
  },
  methods: {
    getRandom () {
      const path = 'http://localhost:5000/api/random'
      axios.get(path)
        .then(response => {
          this.randomNumber = response.data.randomNumber
        })
        .catch(error => {
          console.log(error)
        })
    }
  },
  created () {
    this.getRandom()
  }
}
</script>

バックエンドのFlask側では、デフォルトでは他のサーバからのリクエストを受け付ける設定になっていないため、フロントエンドのVue.jsからのAjaxのリクエストを受け入れるようにします。

まずは以下コマンドでCORSをインストールします。

$ pip install -U flask-cors

backend/application.pyを以下のように書き換えます。

# application.py
from flask import Flask
from flask_cors import CORS # ここを追記

def create_app(app_name='FLASK-VUE'):
    app = Flask(app_name,
                static_folder = "./dist/static",
                template_folder = "./dist")
    app.config.from_object('backend.config.BaseConfig')

    from backend.api import api
    app.register_blueprint(api, url_prefix="/api")
    # ここを追記
    cors = CORS(app, resources={r"/api/*": {"origins": "*"}})

    return app

これでhttp://localhost:8080/アクセスしたときにAPIから取得した乱数を表示することができるようになったのですが、
もし静的なファイルをFlask経由で取得する必要が無いのであれば、COREの機能を使う必要はありません。
その場合、backend/appserver/pyを次のように書き換えればOKです。

# appserver.py
from backend.application import create_app
from flask import render_template
import requests # ここを追記
app = create_app()

@app.route('/', defaults={'path': ''})
@app.route('/<path:path>')
def catch_all(path):
    #### 以下を追記 ###
    if app.debug:
        return requests.get('http://localhost:8080/{}'.format(path)).text
    return render_template("index.html")
    #################

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

必要に応じて以下コマンドを実行。

$ pip install requests

これで、開発モード(DEBUG=1)の時は、http://localhost:5000にアクセスした時はhttp://localhost:8080にリダイレクトされるようになり、Vue.js側の画面でちゃんと乱数が取得できるようになります。

Flask側のサーバhttp://localhost:5000とVue.js側のサーバhttp://localhost:8080どちらにアクセスしても同様の結果が得られます。

終わりに

これで、バックエンドのAPIエンドポイントはFlaskで実装し、フロントエンドのUIはVue.jsで実装するフルスタックなアプリケーションが構築できました。
Flask側の開発用サーバとVue.jsの開発用サーバどちらも起動しておけば、それぞれ配下のプログラムを編集した際にホットリロードでリアルタイムに反映されるので、とても開発がしやすいです。
FlaskとVue.jsの詳細な使い方は今後もっと勉強していきたいと思います。

↓続編 kittagon.hateblo.jp