10
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

FlaskにSPAバックエンドを全て任せたい話

Posted at
  • Flask 1.0.2
  • REST APIサーバだけでなく、SPA(Single Page Application)のWebフロントサーバーとしての役割をFlaskが担うにはどうするか?

概要

SPAルーターに定義された各URLパスに対するリクエストを捌きつつ、SPAのエントリポイントとなるindex.htmlや各種JavaScript/CSSなど静的ファイルを担当し、さらにRESTエンドポイントとしても動作させるための設定をまとめた。

通常はフロントにWebサーバーを置いて、静的リソースへのリクエストはそちらに担当させることが多い。しかし簡易的なツールなど「Python単独で動作すること」が要件になることもある。その場合に参考にして欲しい。

全体像

次に示す4パターンのリクエストに対処する必要がある。

# 分類 種別 パスのパターン 備考
1 バックエンド REST API /api/**/*
2 フロントエンド 静的ファイル
(HTML/JS/CSS)
/
/*
ファイルが存在する場合のみ
3 静的ファイル
(画像/JSON)
/images/*
/json/*
4 SPAルータに定義されたURLパス 上記以外 ブラウザリロードやURL直打ちで発生する

本記事で示すサンプルコードのプロジェクト構成を以下に示す。

<root>
+---src/
|   |   main.py
|   +---app/
|           __init__.py
|           api.py
|           view.py
+---static/ 
    +---html/
    |       index.html
    |       bundle.js
    |       bundle.css
    +---images/
    |      image001.png
    +---json/
           messages.json

バックエンド

BlueprintとしてRESTエンドポイントを定義する。

ここではFlask-RESTfulを利用しているが、特に必須ではない。Flask単体でも良いし、他のプラグインを使っても良い。

app/api.py
from typing import Dict
from flask import Blueprint
from flask_restful import Resource, Api

hello = Blueprint('hello', __name__)
api = Api(hello)


@api.resource('/hello')
class HelloResource(Resource):
    def get(self) -> Dict[str, str]:
        return {
            'hello': 'world',
        }

main.pyにて定義したRESTエンドポイントを登録する。

Flask#register_blueprint()を呼び出す際にURLプリフィックス/apiを指定するのがポイント。

main.py(途中)
from flask import Flask

from app.api import hello

flask_app = Flask(__name__)

# REST API を定義
flask_app.register_blueprint(hello, url_prefix='/api')

上記のmain.pyは後ほど追記する。

フロントエンド

HTML/JavaScript/CSSファイルをserveする設定。これもやはりBlueprintとして定義する。

リクエストとして受け取ったファイル名が<root>/static/html/ディレクトリに存在すればそのファイルを返し、存在しなければindex.htmlを返すのがポイント。これにより、SPAルーターに定義されたURLパスに対するリクエストの処理をJavaScriptへ委ねることができる。

app/view.py
from flask import Blueprint, send_from_directory
from flask.helpers import NotFound

html = Blueprint('html', __name__)


@html.route('/', defaults={'filename': ''})
@html.route('/<path:filename>')
def index(filename: str):
    try:
        return send_from_directory('../static/html', filename)
    except NotFound:
        return send_from_directory('../static/html', 'index.html')

main.pyに追記し、上記で定義したBlueprintを登録する。さらに画像ファイルおよびJSONファイルといった他の静的リソースに関するBlueprintも作成・登録している。

main.py(完成)
from flask import Flask, Blueprint

from app.api import hello
from app.view import html

flask_app = Flask(__name__)

# REST API を定義
flask_app.register_blueprint(hello, url_prefix='/api')

# 静的ファイル(画像、JSON)
json = Blueprint('json', __name__, static_url_path='/json', static_folder='../static/json')
images = Blueprint('images', __name__, static_url_path='/images', static_folder='../static/images')
flask_app.register_blueprint(json)
flask_app.register_blueprint(images)

# 静的ファイル(HTML/JavaScript/CSS)
flask_app.register_blueprint(html)

if __name__ == '__main__':
    flask_app.run(host='0.0.0.0', port=8000)


  • 注意
    • Blueprintの定義・登録順が上記のサンプルコードと異なると正しく作動しない可能性がある

補足

本記事のサンプルコードの動作確認のために、以下の8パターンのHTTPリクエストに対して期待するレスポンスが返却されることを確認した。

# リクエスト レスポンス
1 / index.html
2 /index.html index.html
3 /bundle.js bundle.js
4 /bundle.css bundle.css
5 /foo/bar/buz index.html
6 /images/image001.png image001.png
7 /json/messages.json messages.json
8 /api/hello {"Hello":"World"}
10
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?