《Flask 入門教程》第 6 章:模板優化
這一章我們會繼續完善模板,學習幾個非常實用的模板編寫技巧,為下一章實現建立、編輯電影條目打下基礎。
自定義錯誤頁面
為了引出相關知識點,我們首先要為 Watchlist 編寫一個錯誤頁面。目前的程式中,如果你訪問一個不存在的 URL,比如 /hello,Flask 會自動返回一個 404 錯誤響應。預設的錯誤頁面非常簡陋,如下圖所示:
在 Flask 程式中自定義錯誤頁面非常簡單,我們先編寫一個 404 錯誤頁面模板,如下所示:
templates/404.html:404 錯誤頁面模板
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>{{ user.name }}'s Watchlist</title>
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}" type="text/css">
</head>
<body>
<h2>
<img alt="Avatar" class="avatar" src="{{ url_for('static', filename='images/avatar.png') }}">
{{ user.name }}'s Watchlist
</h2>
<ul class="movie-list">
<li>
Page Not Found - 404
<span class="float-right">
<a href="{{ url_for('index') }}">Go Back</a>
</span>
</li>
</ul>
<footer>
<small>© 2018 <a href="http://helloflask.com/tutorial">HelloFlask</a></small>
</footer>
</body>
</html>複製程式碼
接著使用 app.errorhandler()
裝飾器註冊一個錯誤處理函式,它的作用和檢視函式類似,當 404 錯誤發生時,這個函式會被觸發,返回值會作為響應主體返回給客戶端:
app.py:404 錯誤處理函式
@app.errorhandler(404) # 傳入要處理的錯誤程式碼
def page_not_found(e): # 接受異常物件作為引數
user = User.query.first()
return render_template('404.html', user=user), 404 # 返回模板和狀態碼複製程式碼
提示 和我們前面編寫的檢視函式相比,這個函式返回了狀態碼作為第二個引數,普通的檢視函式之所以不用寫出狀態碼,是因為預設會使用 200 狀態碼,表示成功。
這個檢視返回渲染好的錯誤模板,因為模板中使用了 user 變數,這裡也要一併傳入。現在訪問一個不存在的 URL,會顯示我們自定義的錯誤頁面:
編寫完這部分程式碼後,你會發現兩個問題:
- 錯誤頁面和主頁都需要使用 user 變數,所以在對應的處理函式裡都要查詢資料庫並傳入 user 變數。因為每一個頁面都需要獲取使用者名稱顯示在頁面頂部,如果有更多的頁面,那麼每一個對應的檢視函式都要重複傳入這個變數。
- 錯誤頁面模板和主頁模板有大量重複的程式碼,比如
<head>
標籤的內容,頁首的標題,頁尾資訊等。這種重複不僅帶來不必要的工作量,而且會讓修改變得更加麻煩。舉例來說,如果頁尾資訊需要更新,那麼每個頁面都要一一進行修改。
顯而易見,這兩個問題有更優雅的處理方法,下面我們來一一瞭解。
模板上下文處理函式
對於多個模板內都需要使用的變數,我們可以使用 app.context_processor
裝飾器註冊一個模板上下文處理函式,如下所示:
app.py:模板上下文處理函式
@app.context_processor
def inject_user(): # 函式名可以隨意修改
user = User.query.first()
return dict(user=user) # 需要返回字典,等同於return {'user': user}複製程式碼
這個函式返回的變數(以字典鍵值對的形式)將會統一注入到每一個模板的上下文環境中,因此可以直接在模板中使用。
現在我們可以刪除 404 錯誤處理函式和主頁檢視函式中的 user
變數定義,並刪除在 render_template()
函式裡傳入的關鍵字引數:
@app.context_processor
def inject_user():
user = User.query.first()
return dict(user=user)
@app.errorhandler(404)
def page_not_found(e):
return render_template('404.html'), 404
@app.route('/')
def index():
movies = Movie.query.all()
return render_template('index.html', movies=movies)複製程式碼
同樣的,後面我們建立的任意一個模板,都可以在模板中直接使用 user
變數。
使用模板繼承組織模板
對於模板內容重複的問題,Jinja2 提供了模板繼承的支援。這個機制和 Python 類繼承非常類似:我們可以定義一個父模板,一般會稱之為基模板(base template)。基模板中包含完整的 HTML 結構和導航欄、頁首、頁尾都通用部分。在子模板裡,我們可以使用 extends
標籤來宣告繼承自某個基模板。
基模板中需要在實際的子模板中追加或重寫的部分則可以定義成塊(block)。塊使用 block
標籤建立, {% block 塊名稱 %}
作為開始標記,{% endblock %}
或 {% endblock 塊名稱 %}
作為結束標記。通過在子模板裡定義一個同樣名稱的塊,你可以向基模板的對應塊位置追加或重寫內容。
編寫基礎模板
下面是新編寫的基模板 base.html:
templates/base.html:基模板
<!DOCTYPE html>
<html lang="en">
<head>
{% block head %}
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ user.name }}'s Watchlist</title>
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}" type="text/css">
{% endblock %}
</head>
<body>
<h2>
<img alt="Avatar" class="avatar" src="{{ url_for('static', filename='images/avatar.png') }}">
{{ user.name }}'s Watchlist
</h2>
<nav>
<ul>
<li><a href="{{ url_for('index') }}">Home</a></li>
</ul>
</nav>
{% block content %}{% endblock %}
<footer>
<small>© 2018 <a href="http://helloflask.com/tutorial">HelloFlask</a></small>
</footer>
</body>
</html>複製程式碼
在基模板裡,我們添加了兩個塊,一個是包含 <head></head>
內容的 head
塊,另一個是用來在子模板中插入頁面主體內容的 content
塊。在複雜的專案裡,你可以定義更多的塊,方便在子模板中對基模板的各個部分插入內容。另外,塊的名字沒有特定要求,你可以自由修改。
在編寫子模板之前,我們先來看一下基模板中的兩處新變化。
第一處,我們添加了一個新的 <meta>
元素,這個元素會設定頁面的視口,讓頁面根據裝置的寬度來自動縮放頁面,讓移動裝置擁有更好的瀏覽體驗:
<meta name="viewport" content="width=device-width, initial-scale=1.0">複製程式碼
第二處,新的頁面添加了一個導航欄:
<nav>
<ul>
<li><a href="{{ url_for('index') }}">Home</a></li>
</ul>
</nav>複製程式碼
導航欄對應的 CSS 程式碼如下所示:
nav ul {
list-style-type: none;
margin: 0;
padding: 0;
overflow: hidden;
background-color: #333;
}
nav li {
float: left;
}
nav li a {
display: block;
color: white;
text-align: center;
padding: 8px 12px;
text-decoration: none;
}
nav li a:hover {
background-color: #111;
}複製程式碼
編寫子模板
建立了基模板後,子模板的編寫會變得非常簡單。下面是新的主頁模板(index.html):
templates/index.html:繼承基模板的主頁模板
{% extends 'base.html' %}
{% block content %}
<p>{{ movies|length }} Titles</p>
<ul class="movie-list">
{% for movie in movies %}
<li>{{ movie.title }} - {{ movie.year }}
<span class="float-right">
<a class="imdb" href="https://www.imdb.com/find?q={{ movie.title }}" target="_blank" title="Find this movie on IMDb">IMDb</a>
</span>
</li>
{% endfor %}
</ul>
<img alt="Walking Totoro" class="totoro" src="{{ url_for('static', filename='images/totoro.gif') }}" title="to~to~ro~">
{% endblock %}複製程式碼
第一行使用 extends
標籤宣告擴充套件自模板 base.html,可以理解成“這個模板繼承自 base.html“。接著我們定義了 content
塊,這裡的內容會插入到基模板中 content
塊的位置。
提示 預設的塊重寫行為是覆蓋,如果你想向父塊裡追加內容,可以在子塊中使用 super()
宣告,即 {{ super() }}
。
404 錯誤頁面的模板類似,如下所示:
templates/index.html:繼承基模板的 404 錯誤頁面模板
{% extends 'base.html' %}
{% block content %}
<ul class="movie-list">
<li>
Page Not Found - 404
<span class="float-right">
<a href="{{ url_for('index') }}">Go Back</a>
</span>
</li>
</ul>
{% endblock %}複製程式碼
新增 IMDb 連結
在主頁模板裡,我們還為每一個電影條目右側添加了一個 IMDb 連結:
<span class="float-right">
<a class="imdb" href="https://www.imdb.com/find?q={{ movie.title }}" target="_blank" title="Find this movie on IMDb">IMDb</a>
</span>複製程式碼
這個連結的 href
屬性的值為 IMDb 搜尋頁面的 URL,搜尋關鍵詞通過查詢引數 q
傳入,這裡傳入了電影的標題。
對應的 CSS 定義如下所示:
.float-right {
float: right;
}
.imdb {
font-size: 12px;
font-weight: bold;
color: black;
text-decoration: none;
background: #F5C518;
border-radius: 5px;
padding: 3px 5px;
}複製程式碼
現在,我們的程式主頁如下所示:
本章小結
本章我們主要學習了 Jinja2 的模板繼承機制,去掉了大量的重複程式碼,這讓後續的模板編寫工作變得更加輕鬆。結束前,讓我們提交程式碼:
$ git add .
$ git commit -m "Add base template and error template"
$ git push複製程式碼
提示 你可以在 GitHub 上檢視本書示例程式的對應 commit:cfc08fa。
進階提示
- 本章介紹的自定義錯誤頁面是為了引出兩個重要的知識點,因此並沒有著重介紹錯誤頁面本身。這裡只為 404 錯誤編寫了自定義錯誤頁面,對於另外兩個常見的錯誤 400 錯誤和 500 錯誤,你可以自己試著為它們編寫錯誤處理函式和對應的模板。
- 因為示例程式的語言和電影標題使用了英文,所以電影網站的搜尋連結使用了 IMDb,對於中文,你可以使用豆瓣電影或時光網。以豆瓣電影為例,它的搜尋連結為 movie.douban.com/subject_sea…,對應的
href
屬性即https://movie.douban.com/subject_search?search_text={{ movie.title }}
。 - 因為基模板會被所有其他頁面模板繼承,如果你在基模板中使用了某個變數,那麼這個變數也需要使用模板上下文處理函式注入到模板裡。
- 本書主頁 & 相關資源索引:http://helloflask.com/tutorial。