5
15

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 3 years have passed since last update.

FlaskでMVP作りたいときに使う個人的ベストプラクティスひな形

Last updated at Posted at 2020-05-12

Webアプリを作る時、ひな形があると便利です。
今回は、僕が普段使っているFlaskアプリのひな形について紹介したいと思います。

前提

  • OS: Windows 10
  • エディタ: Visual Studio Code
  • Herokuにホスティングする
  • Python 3.6
  • Flask 1.1.2
  • gunicorn 20.0.4
  • Anacondaで環境作成した。
  • ただし、gunicornだけはAnacondaのリポジトリに見つからなかったので、pipでインストールした

サイト概要

image.png

  • TOP画面
  • PRJリソースの作成・参照・検索機能
  • Facebook認証機能

を持つシンプルなPRJ作成・管理を行うWebアプリです。

サイトの主な仕様は下記の通り。

  • ログインした人はPRJを作成することができる
  • サイト利用者はPRJ詳細からPRJに参加することができる
  • PRJの進捗を更新できるのはPRJメンバーのみ
  • PRJを完了させられるのはPRJ作成者(リーダー)のみ

フォルダ構成

上記の通りシンプルなアプリなので、1ファイルで書ききることもできる規模ですが、
拡張性や可読性を確保するため、きれいにフォルダを整理したいと思います。

特に以下の点に注意してフォルダ構成をしました。

  • 処理の階層別にフォルダを分ける
  • TOP/PRJなど、リソースごとにファイルを分ける

実際のフォルダ構成は下記の通り。

tree
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を返すだけのため、下記の処理フローになります。

image.png

  • ユーザがブラウザでhttps://my-site.com/にアクセスする
  • Flaskサーバ(main.py)がリクエストを受け取り、route/top.pyへルーティングする
  • route/top.pyrender_template('top')メソッドを返す
  • render_template('top')メソッドが返されたので、template/top.htmlを使ってTOPページを構成し、結果をブラウザに返す

オンライン処理(PRJ画面の表示)

PRJ系の画面の処理フローについて、PRJ詳細画面を例にざっくり説明します。

PRJ系の画面は、TOP画面とは異なり、登録・検索・参照などの複数のアクションを実装する必要があります。
そのため、レイヤごとに下記の役割を持たせています。

  • route:受け取ったリクエストからアクションを取り出して処理を振り分ける(ルーティング)
  • mw:各アクションごとに、チェック処理や呼び出すbiz処理を定義する
  • biz:APIやDBへのアクセスを行う

image.png

このように設計することで、各レイヤに処理を分割して分かりやすく管理することができます。
また、新規機能を追加する時も、どこに修正を加えるべきかが分かりやすくなります。

※たとえば、新しいアクション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()する。

main.py
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は''(空文字)を設定する。

route/top.py
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など各種アクションを受け付けられるようになる。

route/prj.py
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呼び出し処理を書く。

mw/prj.py
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キーはシステムとしての機密情報なので、環境変数から読み込むように設定している。

biz/prj.py
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.htmltemplate/prj/show.htmlのようにページの階層が異なっていてもstatic/common/配下にある共通静的コンテンツを同じlayout.htmlを使って読み込めるようにするためです。

template/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

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

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

5
15
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
5
15

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?