【Flask】Jinja2のテンプレート継承でHTMLファイルを役割ごとに分割する

この記事では、Flaskを使ったWebアプリケーション開発において、ヘッダー、フッターなど、複数のページで共通の部品を表示するときに効率よく開発する方法を紹介しています。

記事を理解するための準備

CHECK.1
ディレクトリ

Flaskの作業用フォルダ名はflaskで、venvでPython3の仮想環境を作成し、Flaskをインストールしています。

また、 ターミナル(コマンドプロンプト)のカレントディレクトリは、 flaskフォルダに存在します。

ディレクトリ
flask/
 ┣ app.py
 ︙
 ┗ templates/
   ┗ index.html
CHECK.2
app.py

Webアプリケーションの核であるapp.pyは、flask.render_templateメソッドでindex.htmlを読み込むシンプルなプログラムになっています。

app.py
# -*- coding: utf-8 -*-
from flask import Flask, render_template

app = Flask(__name__)

@app.route('/')
def index():
	return render_template('index.html', \
		title = 'index.html')

if __name__ == '__main__':
	app.run()
CHECK.3
index.html

クライアントサイドに表示されるindex.htmlは、わかりやすさを考慮してBootstrapでデザインしているページになっています。

index.html
<!doctype html>
<html lang="ja">
	<head>
		<meta charset="utf-8">
		<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
		<title>{{ title }}</title>
		
		<!-- Bootstrap CSS -->
		<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
	</head>
	<body>
		<div class="container">
			<h1 class="mt-4">テンプレート継承({{ title }})</h1>
			
			<!-- ヘッダー -->
			<header class="my-3">
				<ul class="nav nav-pills">
					<li class="nav-item"><a class="nav-link" href="/prev/">prev.html</a></li>
					<li class="nav-item"><a class="nav-link active" href="/">index.html</a></li>
					<li class="nav-item"><a class="nav-link" href="/next/">next.html</a></li>
				</ul>
			</header>
		  
			<!-- メインコンテンツ -->
			<div class="pt-3 pb-4">
				<p>{{ title }}が読み込まれています。</p>				
				<div class="alert alert-warning" role="alert">
					今日の天気は晴れです。ポカポカ陽気で気持ちいいですね。
				</div>
			</div>
		  
			<!-- フッター -->
			<footer class="bg-dark text-light">
				<p class="text-center">Copyright 2020 tanu.</p>
			</footer>
			
		</div>
		
		<!-- Optional JavaScript -->
		<!-- jQuery first, then Popper.js, then Bootstrap JS -->
		<script src="https://code.jquery.com/jquery-3.4.1.slim.min.js" integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n" crossorigin="anonymous"></script>
		<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
		<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script>
	</body>
</html>
CHECK.4
動作確認
ターミナル
$ python app.py

http://127.0.0.1:5000/

END

index.htmlを分割する|extends, block

Flaskで使っているJinja2テンプレートエンジンには、『テンプレートの継承』という機能が存在します。

テンプレートの継承は、Jinja2のextendsブロックを使用します。

継承の指定
{% extends "ファイルのパス" %}

そして、継承したテンプレートには継承元にあるブロックに、はめ込むコンテンツを記述します。

ブロックを呼び出すためには、Jinja2のblockブロックを使用します。

ブロックの記述
{% block 任意のブロック名 %}
<!-- コンテンツ -->
{% endblock %}

それでは、extendsブロックとblockブロックを使用して、

  • ページ全体のレイアウトを司るHTMLファイルをlayout.html
  • ルートで表示されるメインコンテンツ部分のHTMLファイルをindex.html

と設定し、もともと1つだったindex.htmlを2つのHTMLファイルにそれぞれ分割してみましょう。

レイアウトとコンテンツ部分に分割する

STEP.1
layout.html
ディレクトリ
flask/
 ┣ app.py
 ︙
 ┗ templates/
   ┣ index.html
   ┗ layout.html NEW

layout.htmlは、ページ全体のレイアウトを司るHTMLファイルです。

もともとのindex.htmlからメインコンテンツだけを取り除いた部分で構成されています。

取り除いたメインコンテンツ部分には、{% block content %}{% endblock %}と記述し、メインコンテンツ部分のindex.htmlを読み込ませるように設定します。

layout.html
<!doctype html>
<html lang="ja">
	<head>
		<meta charset="utf-8">
		<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
		<title>{{ title }}</title>
		
		<!-- Bootstrap CSS -->
		<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
	</head>
	<body>
		<div class="container">
			<h1 class="mt-4">テンプレート継承({{ title }})</h1>
			
			<!-- ヘッダー -->
			<header class="my-3">
				<ul class="nav nav-pills">
					<li class="nav-item"><a class="nav-link" href="/prev/">prev.html</a></li>
					<li class="nav-item"><a class="nav-link active" href="/">index.html</a></li>
					<li class="nav-item"><a class="nav-link" href="/next/">next.html</a></li>
				</ul>
			</header>

			<!-- メインコンテンツ -->
			{%- block content %}{% endblock %}
		  
			<!-- フッター -->
			<footer class="bg-dark text-light">
				<p class="text-center">Copyright 2020 tanu.</p>
			</footer>
			
		</div>
		
		<!-- Optional JavaScript -->
		<!-- jQuery first, then Popper.js, then Bootstrap JS -->
		<script src="https://code.jquery.com/jquery-3.4.1.slim.min.js" integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n" crossorigin="anonymous"></script>
		<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
		<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script>
	</body>
</html>
STEP.2
index.html

index.htmlには、STEP.1で取り除かれたメインコンテンツ部分をそのまま記述します。

index.html
{%- extends "layout.html" %}
{%- block content %}
			<div class="pt-3 pb-4">
				<p>{{ title }}が読み込まれています。</p>				
				<div class="alert alert-warning" role="alert">
					今日の天気は晴れです。ポカポカ陽気で気持ちいいですね。
				</div>
			</div>
{%- endblock %}
CHECK.3
動作確認

無事に2つのHTMLファイルに分割することに成功しました。

END

新しいページを作成する

メインコンテンツの中身が違う2つのHTMLファイル(prev.htmlnext.html)を作成してみましょう。

ディレクトリ
flask/
 ┣ app.py
 ︙
 ┗ templates/
   ┣ index.html
   ┣ layout.html
   ┣ next.html NEW
   ┗ prev.html NEW
STEP.1
prev.html
prev.html
{%- extends "layout.html" %}
{%- block content %}
			<div class="pt-3 pb-4">
				<p>{{ title }}が読み込まれています。</p>				
				<div class="alert alert-secondary" role="alert">
					今日の天気は曇りです。光のコントラストが少なくポートレート撮影にはもってこいですね。
				</div>
			</div>
{%- endblock %}
STEP.2
next.html
next.html
{%- extends "layout.html" %}
{%- block content %}
			<div class="pt-3 pb-4">
				<p>{{ title }}が読み込まれています。</p>				
				<div class="alert alert-primary" role="alert">
					今日の天気は雨です。雨音ってなんだか落ち着きますね。
				</div>
			</div>
{%- endblock %}
STEP.3
app.py
app.py
# 中略
@app.route('/')
def index():
	return render_template('index.html', \
		title = 'index.html')

@app.route('/prev/')
def prev():
	return render_template('prev.html', \
		title = 'prev.html')

@app.route('/next/')
def next():
	return render_template('next.html', \
		title = 'next.html')
STEP.4
動作確認

http://127.0.0.1:5000/


http://127.0.0.1:5000/prev/


http://127.0.0.1:5000/next/

ヘッダーのナビゲーションがindex.htmlで固定されていますが、後述する方法で対応しています。

END

layout.htmlを役割ごとに分割する|include

layout.htmlを見直してみましょう。

layout.html
<!-- 中略 -->
<div class="container">
	<h1 class="mt-4">テンプレート継承({{ title }})</h1>
			
	<!-- ヘッダー -->
	<header class="my-3">
		<ul class="nav nav-pills">
			<li class="nav-item"><a class="nav-link" href="/prev/">prev.html</a></li>
			<li class="nav-item"><a class="nav-link active" href="/">index.html</a></li>
			<li class="nav-item"><a class="nav-link" href="/next/">next.html</a></li>
		</ul>
	</header>

	<!-- メインコンテンツ -->
	{%- block content %}{% endblock %}
		  
	<!-- フッター -->
	<footer class="bg-dark text-light">
		<p class="text-center">Copyright 2020 tanu.</p>
	</footer>
			
</div>

この中には、ヘッダーやフッターなどが混在しています。

これらを管理しやすくするため、それぞれheader.htmlfooter.htmlとして細分割してみましょう。

今回は、ヘッダーやフッターをindex.htmlprev.htmlのように新しいページ単位ではなく、あくまで1つの部品として保管させたいです。

そんなときは blockブロックではなく、includeブロックを使用します。

includeの記述
{% include "ファイルのパス" %}
ディレクトリ
flask/
 ┣ app.py
 ︙
 ┗ templates/
   ┣ footer.html NEW
   ┣ header.html NEW
   ┣ index.html
   ┣ layout.html
   ┣ next.html
   ┗ prev.html

layout.html

layout.html
<!-- 中略 -->
<div class="container">
	<h1 class="mt-4">テンプレート継承({{ title }})</h1>
			
	<!-- ヘッダー -->
	{% include "header.html" %}

	<!-- メインコンテンツ -->
	{%- block content %}{% endblock %}
		  
	<!-- フッター -->
	{% include "footer.html" %}
			
</div>

header.html

header.html
<header class="my-3">
				<ul class="nav nav-pills">
					<li class="nav-item"><a class="nav-link" href="/prev/">prev.html</a></li>
					<li class="nav-item"><a class="nav-link active" href="/">index.html</a></li>
					<li class="nav-item"><a class="nav-link" href="/next/">next.html</a></li>
				</ul>
			</header>
MEMO
※インデントがずれているのは、ソースをきれいに整えるためです。

また、リストの部分でactiveなクラスを動的に対応させるために、header.htmlを以下のように修正します。

header.html
<header class="my-3">
				<ul class="nav nav-pills">
					<li class="nav-item"><a class="nav-link{% if prev %} active{% endif %}" href="/prev/">prev.html</a></li>
					<li class="nav-item"><a class="nav-link{% if index %} active{% endif %}" href="/">index.html</a></li>
					<li class="nav-item"><a class="nav-link{% if nextt %} active{% endif %}" href="/next/">next.html</a></li>
				</ul>
			</header>

footer.html

footer.html
<footer class="bg-dark text-light">
				<p class="text-center">Copyright 2020 tanu.</p>
			</footer>
MEMO
※インデントがずれているのは、header.htmlと同様に、ソースをきれいに整えるためです。

app.py

app.py
# 中略
@app.route('/')
def index():
	return render_template('index.html', \
		index = True, \
		title = 'index.html')

@app.route('/prev/')
def prev():
	return render_template('prev.html', \
		prev = True, \
		title = 'prev.html')

@app.route('/next/')
def next():
	return render_template('next.html', \
		nextt = True, \
		title = 'next.html')

動作確認

http://127.0.0.1:5000/


http://127.0.0.1:5000/prev/


http://127.0.0.1:5000/next/

ちゃんと、ヘッダーのナビゲーションも動作してることが確認できますね(^o^)

まとめ

  • extends+blockで、アプリケーションの枠組みを継承
  • 継承の指定
    {% extends "ファイルのパス" %}
    ブロックの記述
    {% block 任意のブロック名 %}
    <!-- コンテンツ -->
    {% endblock %}
  • includeで、役割ごと(headerfooterなど)に分割されたHTMLファイルを差し込む
  • includeの記述
    {% include "ファイルのパス" %}

    連載目次:FlaskでWebアプリケーションを開発するためのロードマップ

    入門編:10記事

    入門編の10記事を順に読んでいけば、FlaskでWebアプリケーションを開発する必要最小限のことが学べます。

    簡単なアプリケーションであれば、セキュリティ上の観点を考慮しなかった場合公開できるでしょう。

    実践編

    実践編の記事では、FlaskでWebアプリケーションを公開するために欠かせないセキュリティのことや実践的なテクニックを紹介しています。

    ここまで読み込めば、あとはアイディア次第でいろんなWebアプリケーションを公開できるでしょう!

    1. セッション
    2. Cookie
    3. メソッドベース・ディスパッチ
    4. ベーシック認証・Digest認証
    5. ログイン機能
    6. サーバーサイドからクライアントサイドにpandas.Series、pandas.DataFrameを渡す
    7. クライアントサイド(form)からPOSTされた1、2次元配列を受け取る
    8. matplotlibを使う
    9. Bootstrap4を使う
    10. フォームの非同期通信(Ajax, jQuery)