Webアプリを作る時、ひな形があると便利です。
今回は、僕が普段使っているFlaskアプリのひな形について紹介したいと思います。
前提
- OS: Windows 10
- エディタ: Visual Studio Code
- Herokuにホスティングする
- Python 3.6
- Flask 1.1.2
- gunicorn 20.0.4
- Anacondaで環境作成した。
- ただし、gunicornだけはAnacondaのリポジトリに見つからなかったので、pipでインストールした
サイト概要
- TOP画面
- PRJリソースの作成・参照・検索機能
- Facebook認証機能
を持つシンプルなPRJ作成・管理を行うWebアプリです。
サイトの主な仕様は下記の通り。
- ログインした人はPRJを作成することができる
- サイト利用者はPRJ詳細からPRJに参加することができる
- PRJの進捗を更新できるのはPRJメンバーのみ
- PRJを完了させられるのはPRJ作成者(リーダー)のみ
フォルダ構成
上記の通りシンプルなアプリなので、1ファイルで書ききることもできる規模ですが、
拡張性や可読性を確保するため、きれいにフォルダを整理したいと思います。
特に以下の点に注意してフォルダ構成をしました。
- 処理の階層別にフォルダを分ける
- TOP/PRJなど、リソースごとにファイルを分ける
実際のフォルダ構成は下記の通り。
root ルートフォルダ
│ .gitignore
│ LICENSE
│ main.py 最初に実行されるアプリケーションサーバ本体
│ Procfile Heroku上で稼働させるとき使う
│ README.md
│ requirements.txt Heroku上で稼働させるとき使う
│
├─.vscode VSCodeの設定ファイル(gitignore対象)
│ launch.json デバッグの構成を定義
│ settings.json Python仮想環境の指定など
│
├─biz ビジネスロジック置き場
│ prj.py DB操作やAPI呼び出しなどのビジネスロジックを書く
│
├─mw ミドルウェア(フィルター)置き場
│ prj.py routeとbizをつなぐ中間層の処理全般を書く
│
├─route ルーティング定義(ブループリント)置き場
│ prj.py リソース別にルーティング定義を書く
│ top.py リソース別にルーティング定義を書く
│
├─static 静的コンテンツ置き場
│ ├─common 全てのページで使用する静的コンテンツ
│ │ ├─css
│ │ │ page.css
│ │ │
│ │ └─js
│ │ page.js
│ │
│ ├─prj リソース別の静的コンテンツ置き場
│ │ ├─css
│ │ │ page.css
│ │ │
│ │ └─js
│ │ page.js
│ │
│ └─top リソース別の静的コンテンツ置き場
│ ├─css
│ │ page.css
│ │
│ └─js
│ page.js
│
└─templates Jinjaテンプレート置き場
│ footer.html 共通フッター
│ header.html 共通ヘッダー
│ layout.html ページレイアウト定義
│ top.html リソース別の個別ページ(1ファイル完結系)
│
└─prj リソース別の個別ページ(複数ページ系)
entry.html
search.html
show.html
処理概要
Flaskサーバ起動
Flaskサーバを起動するときは、main.pyを実行します。
オンライン処理(TOP画面の表示)
ユーザからのTOPページへのリクエストをFlaskサーバが受け付けるときは、下記の流れで処理します。
TOPページ表示時の処理には特にロジックはなく、単純に対応するtemplateを返すだけのため、下記の処理フローになります。
- ユーザがブラウザで
https://my-site.com/
にアクセスする - Flaskサーバ(main.py)がリクエストを受け取り、
route/top.py
へルーティングする -
route/top.py
はrender_template('top')
メソッドを返す -
render_template('top')
メソッドが返されたので、template/top.html
を使ってTOPページを構成し、結果をブラウザに返す
オンライン処理(PRJ画面の表示)
PRJ系の画面の処理フローについて、PRJ詳細画面を例にざっくり説明します。
PRJ系の画面は、TOP画面とは異なり、登録・検索・参照などの複数のアクションを実装する必要があります。
そのため、レイヤごとに下記の役割を持たせています。
- route:受け取ったリクエストからアクションを取り出して処理を振り分ける(ルーティング)
- mw:各アクションごとに、チェック処理や呼び出すbiz処理を定義する
- biz:APIやDBへのアクセスを行う
このように設計することで、各レイヤに処理を分割して分かりやすく管理することができます。
また、新規機能を追加する時も、どこに修正を加えるべきかが分かりやすくなります。
※たとえば、新しいアクションdeleteを追加したければ、route、mw、biz、templateの各prj.py/prj.htmlにdeleteアクションに対応するメソッドを追加する、など。
※ほかの例としては、たとえば、prj情報を取得する際に、prj作成者のユーザ情報を別エンティティであるuserエンティティから取得したい場合、mw/prj.pyにuser取得処理を追加すれば対応できたりします。
レイヤ間の処理の関係性としては、下記のポリシーで実装していきます。
- routeレイヤの各メソッドは、ルーティングの単位で定義する
- routeレイヤの各メソッドは、mwレイヤのメソッド呼び出し処理を好きなように組み合わせて処理を行う
- mwレイヤの各メソッドは、任意の単位で定義する。
- mwレイヤの各メソッドは、bizレイヤのメソッド呼び出し処理を好きなように組み合わせて処理を行う
- bizレイヤの各メソッドは、特定の1つのAPI呼び出し処理または1つのDBアクセス処理を行う
実装詳細
maiy.py: Flaskサーバ起動処理
routeフォルダ内の*.pyをインポートし、
各種リクエストを適切にルーティングできるようにapp.register_blueprint()
する。
from flask import Flask, redirect, url_for
app = Flask(__name__)
from route.top import top
from route.prj import prj
app.register_blueprint(top)
app.register_blueprint(prj)
if __name__ == '__main__':
app.debug = True
app.run(host='127.0.0.1',port=5000)
route/top.py
TOP画面表示する処理のみ。
サイトルート(https://my-site.com/
)にアクセスされたときのルーティングなので、
Blueprintのurl_prefixは''
(空文字)を設定する。
from flask import Flask, render_template, request, Blueprint
top = Blueprint('top', __name__, url_prefix='')
@top.route('/')
def index():
return render_template('top.html', title='TOP')
route/prj.py
prj系のルーティング定義。
Blueprintのurl_prefixに'/prj'
を設定する。こうすることで、https://my-site.com/prj/~
に対するリクエストを受け付けられるようになる。
@prj.route('/search', methods=['POST'])
のようにルーティング定義を行うことで、https://my-site.com/prj/search
など各種アクションを受け付けられるようになる。
from flask import Flask, render_template, request, Blueprint
from mw.prj import parse, search_prj, get_prj, create_prj, update_prj
prj = Blueprint('prj', __name__, url_prefix='/prj')
@prj.route('/')
def index():
return render_template('prj/search.html', title='Project検索', prj={})
@prj.route('/search', methods=['POST'])
def search():
q = parse(request)
if q is None:
# 早期リターン
return jsonify({'code': 'W00100','message': '入力された値が無効です。'})
# 入力された条件でDBを検索
data = search_prj(q)
# 検索結果を表示
return jsonify({'data': data})
@prj.route('/show/<prj_id>', methods=['GET'])
def show(prj_id):
if prj_id is None:
# 早期リターン
return render_template('prj/show.html', title='Project詳細', data={})
# prj_idでDBを検索
data = get_prj(prj_id)
# 詳細ページを表示
return render_template('prj/show.html', title='Project詳細', data=data)
@prj.route('/entry', methods=['GET'])
def entry():
# PRJ入力画面の初期表示
return render_template('prj/entry.html', title='Project作成')
@prj.route('/create', methods=['POST'])
def create():
# PRJ作成
data = create_prj(parse(request))
# 詳細ページを表示
return render_template('prj/show.html', title='Project詳細', data=data, message='プロジェクトを作成しました。')
mw/prj.py
各アクションの処理を行うためのチェック処理やBiz呼び出し処理を書く。
from flask import request
import biz.prj as prj
def parse(request):
# 割愛
def search_prj(dto):
# 割愛
def get_prj(prj_id):
"""
指定されたプロジェクトIDのプロジェクトを取得する
"""
return prj.get(prj_id)
def create_prj(dto):
# 割愛
def update_prj(data, dto):
# 割愛
biz/prj.py
APIアクセス処理やDBアクセス処理を書く。
下記のサンプルは、HTTPでAPIを呼び出すget処理を書いている。
また、APIのエンドポイントやAPIキーはシステムとしての機密情報なので、環境変数から読み込むように設定している。
import requests
import os
api_endpoint = os.environ.get('API_ENDPOINT')
headers = {
'x-api-key':os.environ.get('API_KEY')
}
def get(prj_id):
r_get = requests.get(api_endpoint + '/prj/' + prj_id, headers=headers)
return r_get.json()
template/layout.html
すべてのHTMLページのひな形。
全ページ共通のCSSやJSはこのlayout.htmlの中で読込定義することで、
各ページにいちいち書かなくても済むようにしている。
各ページ個別のCSSやJSを読み込めるように、{% block css %}
や{% block js %}
を定義している。
bodyタグの中身は、共通ヘッダー、各ページのコンテンツ、共通フッターとなるように定義している。
<link rel="stylesheet" href="/static/common/css/page.css" />
<script src="/static/common/js/page.js"></script>
のように、static配下の静的コンテンツを読み込む際、hrefおよびsrcの指定は/static
のようにスラッシュ始まりで定義しています。
これは、template/top.html
とtemplate/prj/show.html
のようにページの階層が異なっていてもstatic/common/
配下にある共通静的コンテンツを同じlayout.htmlを使って読み込めるようにするためです。
<!DOCTYPE html>
<html>
<head>
<title>{{ title }}</title>
<meta http-equiv="Content-Type" content="text/html" charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0" />
<!-- 共通スタイル -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.8.2/css/bulma.min.css">
<script defer src="https://use.fontawesome.com/releases/v5.3.1/js/all.js"></script>
<link rel="stylesheet" href="/static/common/css/page.css" />
<!-- 個別ページcss -->
{% block css %}
{% endblock %}
</head>
<body class="has-navbar-fixed-top">
<!-- ヘッダー -->
{% include "header.html" %}
<!-- コンテンツ -->
{% block content %}
{% endblock %}
<!-- フッター -->
{% include "footer.html" %}
<!-- 共通JS -->
<script src="https://code.jquery.com/jquery-2.1.1.min.js"></script>
<script src="/static/common/js/page.js"></script>
<!-- 個別ページJS -->
{% block js %}
{% endblock %}
</body>
</html>
template/top.html
{% extends "layout.html" %}
{% block css %}
<link rel="stylesheet" href="/static/top/css/page.css" type="text/css" />
{% endblock %}
{% block content %}
<section class="hero is-medium is-primary is-bold">
<div class="hero-body">
<div class="container">
<div class="columns is-centered">
<div class="column is-narrow">
<h1 class="title is-1">
Site Concept
</h1>
</div>
</div>
</div>
</div>
</section>
{% endblock %}
{% block js %}
<script async src="/static/top/js/page.js"></script>
{% endblock %}
template/prj/show.html
{% extends "layout.html" %}
{% block css %}
<link rel="stylesheet" href="/static/prj/css/page.css" type="text/css" />
{% endblock %}
{% block content %}
<section class="hero is-medium is-primary is-bold">
<div class="hero-body">
<div class="container">
<div class="columns is-centered">
<div class="column is-narrow">
<h1 class="title is-1">
{{ data['name'] }}
</h1>
</div>
</div>
</div>
</div>
</section>
{% endblock %}
{% block js %}
<script async src="/static/prj/js/page.js"></script>
{% endblock %}
サンプルアプリの全コード
このひな形を利用して作成中のアプリケーションコードをGithubに公開しています。
https://github.com/KeitaShiratori/ripple