適当おじさんの適当ブログ

技術のことやゲーム開発のことやゲームのことなど自由に雑多に書き連ねます

いまさらながら Flask についてまとめる 〜Template〜

はじめに

いまさらながら Flask について整理していきます。「Flaskとかいうやつを使って、試しにアプリ開発にトライしてみたい」くらいの感覚の人を対象にしています。

Flaskのバージョンは 0.12.2 です。

この記事では、Template Engine と Template について紹介していきます。

Template Engine?

Webアプリケーションでは、「画面のレイアウトは同じだが、その画面の一部のデータは異なる表示にしたい」ということがよくあります。たとえば、ユーザ情報画面です。ユーザ情報画面では、ログインしているユーザごとに名前やIDを表示するのが普通です。つまり、データや状態によって画面を動的に生成する必要があります。これを実現しているのがテンプレートエンジンです。

Flaskのデフォルトのテンプレートエンジンは、 jinja2 です。jinja2 では、以下のように HTML と Python のコードを同居させることができます。この user.html を Flask で処理して返すことで、1つのHTMLファイルで異なる画面を表示できます。

<!-- user.html -->
<p>
    {{ user_name }}
</p>

<p></p> がHTMLのタグで、 {{...}} で囲われた部分がPythonのコードです。{{...}}のようなPythonの値を埋め込むための記号を Directive と呼びます。値をHTMLに出力するには、{{...}}を使い、if文などの式を埋め込みたい場合には、{%...%}など、目的別にディレクティブを利用します。詳しくは、jinja2の公式ドキュメントを参照していただければと思います。

Template

render_template() 関数を利用することで、Template(=雛形) である user.html に サーバのデータを組み込めます。

from flask import Flask
from flask import session
from flask import render_template

app = Flask(__name__)

@app.route('/user')
def user():
    user_name = 'my name'
    return render_template('user.html', user_name=user_name)

わかりやすくするために、user_name はセッションからの取得ではなく、固定値を与えています。上記が実行されると、以下のHTMLが生成されます。

<!-- rendered user.html -->
<p>
    my name
</p>

Escape

Flaskでは {{ }} で出力される値は、自動的にエスケープされます。

@app.route('/user')
def user():
    user_name = '<script>alert("alert")</script>'
    return render_template('user.html', user_name=user_name)

値はエスケープされるので、以下のようなHTMLが出力されます。scriptタグの<>がそれぞれエスケープされていることがわかります。したがってXSSの心配はありません。

<!-- rendered user.html -->
<p>
    <!-- エスケープされているのでスクリプトは実行されない -->
    &lt;script&gt;alert(&#39;alert&#39;)&lt;/script&gt;
</p>

一方で、エスケープしたくないケースもあるかと思います。その場合は、{% autoescape %} を用います。ただし、ユーザの入力値を表示する場合はXSSの危険性があるので、使い所はしっかり検討しなければなりません。

<!-- user.html -->
<p>
    {% autoescape False %}
        {{ user_name }}
    {% endautoescape %}
</p>
<!-- rendered user.html -->
<p>
    <!-- scriptタグのJavaScriptが実行される -->
    <script>alert("alert")</script>
</p>

共通化

上記のようなテンプレートを作っていると、多くの画面で共通している要素や処理が出てくることがあります。それらをうまく共通化するための機能として、 extendsmacros があります。これらはいずれもFlaskではなくjinja2の機能ですが、便利な機能なのでここで紹介しておきます。

extends

テンプレートを継承することで、親となるテンプレートに子となるテンプレートの要素を加えた画面を生成できます。

<!-- parent.html -->
<p> parent content </p>
{% block body %}{% endblock %}
<p> parent end </p>
<!-- child.html -->
<p> not contain part </p>
{% extends "parent.html" %}
{% block body %}
    <p> this is child content </p>
{% endblock %}

{% extends %} で、親のテンプレートを指定します。子のテンプレートでは、{% block 識別子 %} で子の内容を定義します。識別子は、親と子で揃えておく必要があります。揃っていれば、識別子は自由につけることができます。ここでは識別子は、body としています。

上記は以下のようなHTMLとして生成されます。{% block %} に含まれていない部分は生成結果にも含まれません。

<p> parent content </p>
    <p> this is child content </p>
<p> parent end </p>

ナビゲーションバーやフッターなど多くの画面に共通するコンポーネントを親テンプレートとして作成することで、各画面に同じ要素を記述する必要がなくなります。

macros

extends よりももう少し細かい、特定の処理や部品を再利用できるようにするための機能が macros です。以下のようにして、macroを定義できます。macroはPythonの関数のように引数を指定できます。

<!-- macro.html -->
{% macro generate_h5(text)) %}
<h5>{{ text }}</h5>
{% endmacro %}

定義したmacroは他のテンプレートから以下のようにして呼び出せます。macroが別のファイルに記載されているのであれば、macroが定義されているファイルからimportする必要があります。

<!-- using_macro.html -->
{% from "macro.html" import generate_h5 %}
{{ generate_h5('one') }}

このテンプレートは以下のようなHTMLにレンダリングされます。

<h5>one</h5>

通常、macroの呼び出し先で呼び出し元の変数を参照できません。import時にwith context とつけると、呼び出し元と呼び出し先で変数コンテキストが共有されます。したがって、macro呼び出し時に変数の受け渡しが不要となります。macro呼び出し元のhtmlで引数を指定していませんが、レンダリング結果は同じです。

<!-- macro.html -->
{% macro generate_h5()) %}
<h5>{{ text }}</h5>
{% endmacro %}
<!-- using_macro_with_context.html -->
{% set text = 'one' %}

{% from "macro.html" import generate_h5 with context %}
{{ generate_h5() }}

状況にもよりけりですが、無闇にwith contextを使うと、macroとmacroの呼び出し元の依存が強くなってしまうように感じます。一瞬便利のような気がしてバシバシ使いたくなりますが、よく吟味したほうが良いと思っています。

Standard Context

Flaskでは、テンプレート内でいくつかの変数や関数がデフォルトで利用可能です。すべての利用可能な変数は、公式ドキュメント に記載されています。sessionurl_for()など、知っておくと便利なものもあるので事前に確認しておくと幸せになれそうです。

Customize Directive

jinja2のディレクティブをFlask経由で変更できます。以下の例では、{{...}} {%...%} {#...#}をそれぞれ<<...>> <%...%> <#...#>に変更しています。

from flask import Flask

app = Flask(__name__)

jinja_options = app.jinja_options.copy()                                         
jinja_options.update({                                                      
    'block_start_string': '<%',                                                 
    'block_end_string': '%>',                                                   
    'variable_start_string': '<<',                                              
    'variable_end_string': '>>',                                                
    'comment_start_string': '<#',                                               
    'comment_end_string': '#>'                  
})                                                                               
app.jinja_options = jinja_options    

app.jinja_options.update()でオプションを直接追加しようとすると、TypeError: 'ImmutableDict' objects are immutableが発生してしまい変更できません。そのため、一度コピーを取り、そのコピーに値を追加しています。

他のライブラリなどとディレクティブが競合した際に、この設定が有用です。具体的には、 Vue.js などが例として挙げられます。