Flask初体験


「今度新たに入るプロジェクトがwebアプリなんですが、webアプリ開発が初めてなので教えてほしい」

後輩からそう言われて、聞いてみるとPython、Flask、jQuery、PostreSQLという事でした。
Python、jQuery、PostgreSQLについては、一応知ってましたが、Flaskは初めて(PythonでWebってのも初めて)なので環境を作って、ちょっと触ってみました。
(ちなみに、後輩からの質問自体は web に関する一般的な概念や jQuery の部分が中心で、質問に答えるのに必ずしも Fask を知ってる必要はなかったのですが、興味があって触ってみました)

タイトルの通り、環境はDocker上に作ることにしました。

 

 

まずは開発環境から

ディレクトリ構成
- flask
  - app
  - dev
    * docker-compose.yml
    * Dockerfile
    - init_db
      * init.sql

Flask開発用のコンテナイメージを定義するDockerfileを作ります。

Dockerfile
# Baseイメージ
FROM python:3

# Flaskのインストール
RUN pip install --trusted-host pypi.org --trusted-host pypi.python.org --trusted-host files.pythonhosted.org flask

# psycopg2(PostgreSQLクライアント)のインストール
RUN pip install psycopg2

開発環境はpythonのイメージにFlaskをインストールしただけのシンプルな環境です。
このコンテナ上でFlaskのサンプルをコーディングします。

せっかくなのでPosgreSQLのDBコンテナも作ってDB接続も試せるようにPostgreSQLクライアントのpsycopg2もインストールしてます。

という事で docker-compose.yml はこんな感じになりました。

docker-compose.yml
version: '3.2'
services:
  db:
    image: postgres:latest
    container_name: flask_db
    restart: always
    environment:
      POSTGRES_USER: 'dev' # DBのユーザー名(=DB名)
      POSTGRES_PASSWORD: 'pass' # DBのパスワード
    volumes:
      - ..\init_db:/docker-entrypoint-initdb.d    
  flask:
    build: .
    container_name: flask_dev
    restart: always
    volumes:
      - ..\app:/usr/local/src/work
    ports:
      - 5000:5000
    tty: true
    depends_on:
      - db

flaskコンテナは先ほどのDockerfileをbuildして、ポート5000をフォーワーディングしてます。
それから、ソースコードをホスト側で取り出せるようにホスト上のflask\appとコンテナ内の/usr/local/src/workをマウントしてます。
さらに、depends_onでDBコンテナに依存していることを指定してます。

DBコンテナはPostgreSQLのコンテナイメージをpullしてきてます。
ユーザー「dev」、パスワード「pass」としました。
また、ホスト上のflask\dev\init_dbをDBコンテナ内の/docker-entrypoint-initdb.dとマウントすることでDBの初期化用SQLを走らせるようにしてます。

で、初期化用のSQLは init_db フォルダ内に「init.sql」として用意しました。
中身はこんな感じです。

init.sql
CREATE TABLE INPUTS (
    value      varchar(100),
    input_date date
);

サンプル用なので、極シンプルなテーブルを1つだけ定義してます。

コマンドでflask\devに移動してコンテナを起動します。

$ docker-compose up -d

以前の記事と同じようにVSCodeのRemoteDevelopmentでFlaskコンテナに入ります。

 

 

簡単なサンプルアプリを実装

FlaskコンテナにVSCodeで入ったら、Python(Flask)のコーディングを開始します。

 

ディレクトリ構成
- /usr/local/src/work
  - static
    - css
      * style.css
    - js
      * script.js
  - templates
    * _layout.html
    * hello.html
  * app.py

ところで、Flask はMVC ではなく MVT(Model-View-Template)と呼ばれるパターンを採用したフレームワークとのことです。
MVTではMVCでいうところのControllerに該当するものがViewという事になっています。
今回のサンプルではapp.pyで実装しました。

 

app.py
from flask import Flask, render_template, request, jsonify
import psycopg2

app = Flask(__name__)

def getDbConnection():
    return psycopg2.connect("postgresql://dev:pass@db:5432/dev")

def getInputs(con):
    inputs = []
    with con.cursor() as cur:
        cur.execute("SELECT * FROM INPUTS")
        for row in cur:
            inputs.append("[" + str(row[1]) + "]" + row[0])
    return inputs
def insertInput(con, value):
    with con.cursor() as cur:
        cur.execute("INSERT INTO INPUTS (value, input_date) VALUES (%s,current_date)", (value, ))

@app.route("/")
def index():
    return "Hello world!"

@app.route("/hello")
def hello():
    with getDbConnection() as con:
        return render_template("hello.html", title = "Helloページ", inputs = getInputs(con))

@app.route("/hello", methods=["POST"])
def hello_post():
    name = request.form["name"]
    message = request.form["message"]
    inputValue = name + ":" + message
    with getDbConnection() as con:
        insertInput(con, inputValue)
        return render_template("hello.html", title = "Hello(post)ページ", input1 = "入力値=[" + inputValue + "]", inputs = getInputs(con))

@app.route("/api/hello/post", methods=["POST"])
def hello_post_ajax():
    name = request.json["name"]
    message = request.json["message"]
    inputValue = name + ":" + message
    with getDbConnection() as con:
        insertInput(con, inputValue)
    return jsonify({"input1": "入力値=[" + inputValue + "]", "input": inputValue})

if __name__ == "__main__":
    app.run(debug=True, host="0.0.0.0")

今回は開発環境を作ることが目的なのでプログラムの詳細な説明は省きますが、簡単に言うと

  • 「http://localhost:5000」で「Hello world!」の文字列を返します。
  • 「http://localhost:5000/hello」のGETでサンプルのページを返します。
  • サンプルページの「送信」ボタンで「http://localhost:5000/hello」のPOSTが呼び出され、画面の入力値を受け取ってDBに登録した上でサンプルのページを返します。
  • サンプルページの「非同期送信」ボタンで「http://localhost:5000/api/hello/post」のPOSTがajax経由で呼び出され、DB登録した上で結果をjson形式で返します。
という感じです。
なお、サンプルプログラムなので入力チェック等は省略してます。

続いて、MVCでいうところのViewに相当するTemplate部分です。
Templateはルートディレクトリの直下にtemplatesの名前でディレクトリを作成し、その下にhtmlを置きます。
すると、app.pyの中で
@app.route("/hello")
def hello():
  return 

とすることで、templates/hello.htmlをクライアントへ返すことができます。

ちなみに、FlaskではJinja2というテンプレートエンジンが使われており、変数や制御構文をhtml内に埋め込んで、クライアントへ返すhtmlを動的に生成できるようになっています。

templates/_layout.html
<!DOCTYPE html>
<html>
    <head>
        <title>{{title}}</title>
        <script src="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.11.1.min.js"></script>
        <script src="/static/js/script.js"></script>
        <link rel="stylesheet"
          href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
          integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T"
          crossorigin="anonymous">
        <link rel="stylesheet" href="/static/css/style.css">
    </head>
    <body>
        <nav class="navbar navbar-expand-md navbar-dark fixed-top bg-dark">
          <a class="navbar-brand" href="#">Navbar</a>
          <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarsExampleDefault" aria-controls="navbarsExampleDefault" aria-expanded="false" aria-label="Toggle navigation">
            <span class="navbar-toggler-icon"></span>
          </button>
          <div class="collapse navbar-collapse" id="navbarsExampleDefault">
            <ul class="navbar-nav mr-auto">
              <li class="nav-item active">
                <a class="nav-link" href="#">Home <span class="sr-only">(current)</span></a>
              </li>
              <li class="nav-item">
                <a class="nav-link" href="#">Link</a>
               </li>
              <li class="nav-item">
                <a class="nav-link disabled" href="#">Disabled</a>
              </li>
              <li class="nav-item dropdown">
                <a class="nav-link dropdown-toggle" href="http://example.com" id="dropdown01" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">Dropdown</a>
                <div class="dropdown-menu" aria-labelledby="dropdown01">
                  <a class="dropdown-item" href="#">Action</a>
                  <a class="dropdown-item" href="#">Another action</a>
                  <a class="dropdown-item" href="#">Something else here</a>
                </div>
              </li>
            </ul>
            <form class="form-inline my-2 my-lg-0">
              <input class="form-control mr-sm-2" type="text" placeholder="Search" aria-label="Search">
              <button class="btn btn-outline-success my-2 my-sm-0" type="submit">Search</button>
            </form>
          </div>
        </nav>
        <main class="main">
          {% block content %}
          {% endblock %}
        </main>
    </body>
</html>

共通レイアウトは「_layout.html」として複数のhtmlから利用できるようにしています。
デザインはBootstrapのテンプレートサイトからのまるパクリですww

また、「jQuery 1.11.1」と「Bootstrapの4.3.1」をCDNでから取り込んでいます。

アプリ内固有のjs、cssは「script.js」「style.css」としてルートディレクトリ直下の「static」ディレクトリからダウンロードしてます。

templates/hello.html
{% extends '_layout.html' %}
{% block content %}
    <div class="jumbotron">
        <div class="container">
            <h1 class="display-3">Hello, world!</h1>
            <p>This is a template for a simple marketing or informational website. It includes a large callout called a jumbotron and three supporting pieces of content. Use it as a starting point to create something more unique.</p>
            <p><a class="btn btn-primary btn-lg" href="#" role="button">Learn more »</a></p>
        </div>
    </div>
    <div class="container">
        <form method="POST" action="/hello">
            <div class="form-group">
                <label for="name">名前</label>
                <input type="text" class="form-control" id="name" name="name">
            </div>
            <div class="form-group">
                <label for="message">メッセージ</label>
                <input type="text" class="form-control" id="message" name="message">
            </div>
            <button class="btn btn-lg btn-primary btn-block" type="submit">送信</button>
            <button class="btn btn-lg btn-danger btn-block" type="submit" id="btnSendAjax">非同期送信</button>
        </form>
        <span id="lblInput1" class="hogelabel">{{input1}}</span>
        <ul id="listInputs">
            {% for value in inputs %}
            <li>{{value}}</p>
            {% endfor %}
        </ul>
    </div>
{% endblock %}

「hello.html」の1行目で「_layout.html」を利用することが宣言されています。
{{input1}}{% for value in inputs %}のあたりが Jinja2 による変数や制御構文の利用部分です。

最後に、アプリ内固有のcssやjsの実装です。
cssやjsのような静的なファイルはルートディレクトリ直下の「static」ディレクトリ内に配置するルールのようです。

static/css/style.css
body {
    background-color: antiquewhite;
}
static/js/script.js
$(function() {
    $('#btnSendAjax').click(function () {
        // 入力値取得
        var name = $('input[name=name]').val();
        var message = $('input[name=message]').val();
        $.ajax({
            url: "/api/hello/post",
            type: "POST",
            data: JSON.stringify({name: name, message: message}),
            dataType: "json",
            contentType: "application/json",
            success: function (XMLHttpRequest, textStatus, errorThrown) {
                if (textStatus == 'success' || textStatus == 'nocontent') {
                    // 成功時
                    $('#lblInput1').text(XMLHttpRequest.input1);
                    $('#listInputs').append($("<li>").text(input));
                } else {
                    // エラー時
                    console.error(XMLHttpRequest.responseJSON.Message);
                }
            },
            error: function (XMLHttpRequest, textStatus, errorThrown) {
                // エラー時
                console.error(XMLHttpRequest.responseJSON.Message);
            },
        });
        return false;
    });
});
「script.js」にはサンプルページ内の「非同期送信」ボタンクリック時のajax呼び出しが実装されているだけです。

これで実装は完了です。
動かしてみます。

動作確認&デバッグ

実行方法はいくつかあって、/usr/local/src/workで下記コマンドのいずれを実行しても動きます。

$ flask run -h 0.0.0.0
$ python app.py
ホストのブラウザから localhost:5000 にアクセスして「Hello world!」と表示されたら成功です。
http://localhost:5000/helloでサンプルページが表示されて、「送信」ボタン、「非同期送信」ボタンで画面に入力した内容がDBに登録されるはずです。

http://localhsot:5000/helloのサンプルページ


ただ実行するだけならこのやり方でもいいですが、VSCode上でブレークを張っても止まってくれません。

ブレークしたい場合は「launch.json」を作る必要があります。

まず、Flaskコンテナ内でPython拡張をインストールします。



DEBUGビューを開いてCreate a launch.jsonリンクをクリック。
Python → Flask の順に選択してlaunch.jsonを作成します。



作成されたlaunch.jsonは基本的にデフォルトのままでOKですが、ホストからのアクセスを受け付けられるように、argsに「--host=0.0.0.0」だけ追加します。
 
{
    // Use IntelliSense to learn about possible attributes.
    // Hover to view descriptions of existing attributes.
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Python: Flask",
            "type": "python",
            "request": "launch",
            "module": "flask",
            "env": {
                "FLASK_APP": "app.py",
                "FLASK_ENV": "development",
                "FLASK_DEBUG": "0"
            },
            "args": [
                "run",
                "--no-debugger",
                "--no-reload",
                "--host=0.0.0.0" // ←追加
            ],
            "jinja": true
        }
    ]
}

この通り編集、保存したら、F5キーでデバッグします。

app.pyの任意の場所にブレークして、ホストのブラウザからアクセスして、うまく処理が止まれば成功です。
 

次回は本番環境を作ります

ここまではあくまで Flask の開発環境の話でした。
しかし実際リリースする場合、このままの構成は推奨されていないという事です。
というわけで、次回は本番で稼働させることを想定した環境を作ってみることにします。