Flask Web Development 第三章讀書筆記 模板

第三章 模板

為什么要分離

易于維護的代碼,關(guān)鍵在于保持簡單的結(jié)構(gòu)。
而我們之前編寫的hello.py雖然簡單,卻混合了兩種不同的部分:
如何生成頁面數(shù)據(jù)、如何顯示。
將這兩部分混合在一起會帶來復(fù)雜性。

注意hello.py中的return '<h1>Hello, world</h1>'。
第一部分是生成數(shù)據(jù):生成Hello,world,第二部分是如何顯示:<h1>一級標題。

為什么需要模板

分離后我們將把hello,world!這樣的html頁面單獨存放。
這樣當用戶打開一個網(wǎng)址時,服務(wù)器立即把對應(yīng)的html頁面發(fā)送給他。
可事情這么簡單就好了,很多html頁面中的東西并不是一成不變的。
我們需要一種技術(shù),讓頁面中的一部分可以動態(tài)變化:渲染。
為了渲染模板,F(xiàn)lask使用了Jinja2.

3.1 Jinja2模板引擎

一個簡單的靜態(tài)模板:templates/index.html
<h1>Hello world!</h1>

增加動態(tài)部分的形式 :templates/user.html
<h1>Hello ,{{ name }}!</h1>

第一個模板為靜態(tài),不需要Jinja2也能支持。
而Jinja2會對動態(tài)的部分進行替換,如第二個模板中的動態(tài)部分。
此時用html直接解析會出現(xiàn)錯誤,必須有類似Jinja2的模板引擎。

為了簡化程序,不需要在Flask程序里顯示調(diào)用Jinja2。

3.1.1 渲染模板

如何使用模板

在Flask程序里調(diào)用render_template函數(shù)后,就使用了模板。

示例 3-3 hello.py: 渲染模板  
from flask import Flask, render_template
 
# ...
  
@app.route('/')
def index():
    return render_template('index.html')
  
@app.route('/user/<name>')
def user(name):
    return render_template('user.html', name=name)

模板參數(shù)和搜索路徑

在例3-3中,render_template有兩個參數(shù)。

第一個是模板文件名,以html結(jié)尾,默認在程序/templates文件夾下搜尋。
還記得剛開始我們講Flask類的參數(shù)(__name__)的作用嗎。

第二個是動態(tài)參數(shù)的值,如name=name:
前一個name就是模板中的{{ name }}(見序的第二個文件),即模板中的動態(tài)部分;
后一個name是動態(tài)部分要在頁面顯示的值,在這里是函數(shù)user的參數(shù),它會被傳給模板渲染。

3.1.2 變量

什么是變量

模板中使用的兩個大括號包裹的{{ name }}表示一個變量,
它的值由Flask程序提供(渲染)。
Jinja2能識別python中所有類型的變量,例如列表、字典和對象。

一些在模板中使用變量的示例:

<p>引用字典的一個值: {{ mydict['key'] }}.</p>
# <p>是html標簽,表示段落(paragraph)
<p>引用列表的一個值: {{ mylist[3] }}.</p>
<p>引用列表中的一個值,鍵為變量: {{ mylist[myintvar] }}.</p>
<p>引用對象的一個方法: {{ myobj.somemethod() }}.</p>

什么是過濾器

有時候出于安全考慮或?qū)ψ兞窟M行形式變更,
這時候需要用到過濾器。
例如以首字母大寫形式顯示變量 name 的值:
Hello, {{ name|capitalize }}
使用過濾器,在變量名后加豎線和過濾器名即可。

常見的過濾器

capitalize  # 把值的首字母轉(zhuǎn)換成大寫,其他字母轉(zhuǎn)換成小寫
safe  # 渲染值時不轉(zhuǎn)義
lower  # 把值轉(zhuǎn)換成小寫形式
upper  # 把值轉(zhuǎn)換成大寫形式
title  # 把值中每個單詞的首字母都轉(zhuǎn)換成大寫
trim  # 把值的首尾空格去掉
striptags  # 渲染之前把值中所有的 HTML 標簽都刪掉

特別的safe過濾器

默認情況下,出于安全考慮, Jinja2 會轉(zhuǎn)義所有變量。
例如,如果一個變量的值為 '<h1>Hello</h1>',
Jinja2 會將其渲染成 '<h1>Hello</ h1>'
瀏覽器能顯示這個 h1 元素,但不會進行解釋。
很多情況下需要顯示變量中存儲的 HTML 代碼,
這時就可使用 safe 過濾器。

千萬別在不可信的值上使用 safe 過濾器,
例如用戶在表單中輸入的文本。
完整的過濾器列表可在
Jinja2 文檔查看。

3.1.3 控制結(jié)構(gòu)

if條件控制

{% if user %}
    Hello, {{ user }}!
{% else %}
    Hello, Stranger!
{% endif %}

控制語句以{% 關(guān)鍵字%}開始,
注意有一個結(jié)束語句endif。

for循環(huán)

一個常見需求是在模板中渲染一組元素。

<ul>
    {% for comment in comments %}
        <li>{{ comment }}</li> 
    {% endfor %}
</ul>

同樣有endfor,Jinja2中的結(jié)束關(guān)鍵字是end+開始關(guān)鍵字,中間沒有空格。

<li>用來定義列表的條目,可以用在無序列表<ul>和有序列表<ol>中。

宏類似于函數(shù),例如:

{% macro render(w) %}
    <li>{{ w }}</li>
{% endmacro %}
 
 
<ul>
    {% for w in word %}
        {{ render(w) }}
    {% endfor %}
</ul>

第二段for循環(huán)里的render就是前面定義的宏,
作用是對序列word中的每一項進行渲染。

為了重復(fù)使用宏,我們可以將其保存在單獨的文件中,
然后在需要使用的模板中導(dǎo)入。
例如我們把若干宏保存到macros.html后,可以用類似python包的形式導(dǎo)入。

{% import 'macros.html' as macros %}
<ul>
    {% for w in word %}
        {{ macros.render(w) }}
    {% endfor %}
</ul>

模板繼承

一種方式是使用include:模板插入。
需要在多處重復(fù)使用的模板代碼片段可以寫入單獨的文件,
再包含在模板的某一位置中,以避免重復(fù):
{% include 'common.html' %}

另一種是使用extends:模板繼承。
它類似于 Python 代碼中的類繼承,具有更強的功能。
首先,創(chuàng)建一個名為 base.html 的基模板:

<head>
    {% block head %}
    <title>{% block title %}{% endblock %} - My Application</title>
    {% endblock %}
</head>

使用base.html的模板叫子模板,base是它的父模板。子模板可以修改block元素。

{% extends "base.html" %}
{% block title %}Index{% endblock %}
{% block head %}
    {{ super() }}
   <style>
   </style>
{% endblock %}

extends 聲明子模板繼承自base.html,
extends必須放在第一行。
子模板重新定義了base.html中的2個block,
在子模板的block中,如果想包含在父模板block的內(nèi)容,
使用{{ super() }},
這會獲取對應(yīng)block的內(nèi)容。

這種方式和include的不同在于,include寫好的片段不再修改,只是簡單插入,
最終由調(diào)用include的模板進行渲染。
而extends繼承,是在base.html上做擴展修改,
最終由extends所在的模板進行渲染。

3.2 Flask-Bootstrap

Bootstrap是什么

Bootstrap是Twitter開發(fā)的一個開源框架,
可用來快速開發(fā)美觀的網(wǎng)頁外觀。
它不但兼容所有的現(xiàn)代web瀏覽器,
而且一份代碼,便可以支持手機、電腦、平板的瀏覽。

Bootstrap是客戶端框架,因此不會直接涉及服務(wù)器。
服務(wù)器需要的只是提供使用了Bootstrap和JavaScript的代碼,
并且在這些代碼中實例化所需組件。
這些操作最理想的執(zhí)行場所就是模板。

為什么需要Flask-Bootstrap

要想在程序中使用Bootstrap,
顯然原來的模板要進行必要的改動來適應(yīng)它。
不過有人已經(jīng)開發(fā)了Flask-Bootstrap,
用來簡化這些改動。

如何安裝使用Flask-Bootstrap

Flask-Bootstrap使用pip安裝:
(venv) $ pip install flask-bootstrap
類似Flask-Script,F(xiàn)lask擴展一般都在創(chuàng)建程序?qū)嵗龝r初始化。
下面是Flask-Bootstrap的初始化方法。

from flask_bootstrap import Bootstrap
# ...
bootstrap = Bootstrap(app)

初始化 Flask-Bootstrap 之后,
就可以在程序中使用一個包含所有 Bootstrap 文件的基模板。
這個模板利用 Jinja2 的模板繼承機制,
讓程序擴展一個具有基本頁面結(jié)構(gòu)的基模板,
其中就有用來引入 Bootstrap 的元素。

使用Flask-Bootstrap的模板

{% extends "bootstrap/base.html" %}
 
{% block title %}Flasky{% endblock %}
 
{% block navbar %}
<div class="navbar navbar-inverse" role="navigation">
    <div class="container">
        <div class="navbar-header">
            <button type="button" class="navbar-toggle"
            data-toggle="collapse" data-target=".navbar-collapse">
                <span class="sr-only">Toggle navigation</span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
            </button>
            <a class="navbar-brand" href="/">Flasky</a>
        </div>
        <div class="navbar-collapse collapse">
            <ul class="nav navbar-nav">
                <li><a href="/">Home</a></li>
            </ul>
        </div>
    </div>
</div>
{% endblock %}
 
 
{% block content %}
<div class="container">
    <div class="page-header">
        <h1>Hello, {{ name }}!</h1>
    </div>
</div>
{% endblock %}

它和Flask-Script一樣,初始化了Flask的實例app,
但不同的是,Bootstrap實例沒有run方法,
還是像以前一樣運行app.run()。

對這個模板的簡單說明

現(xiàn)在你看到的模板比較復(fù)雜,我們現(xiàn)在只需要知道,
整個模板定義了3個塊,title,navbar和content。
title很明顯是網(wǎng)頁標簽bar上出現(xiàn)的網(wǎng)站信息,
navbar就是導(dǎo)航條,而content就是網(wǎng)頁的主體部分。
其中的Flasky,Home,Hello,你也可以自己替換成中文。

注意extends后面,在當前程序下并沒有這個bootstrap目錄,
也就是說,我們寫的bootstrap實際上是表明了我們引用了
bootstrap的模板,模板名叫base.html,
具體路徑是C:\Python34\Lib\site-packages\flask_bootstrap\templates\bootstrap,
根據(jù)你的python版本不同,路徑中的python34可能是python27或python36等等。
你可以到這個路徑下看看還有什么模板。

Flask-Bootstrap基模板中定義的塊

塊名 說明
doc 整個html文檔
html_attribs <html>標簽的屬性
html <html>標簽中的內(nèi)容
head <head>標簽中的內(nèi)容
title <title>標簽中的內(nèi)容
metas 一組<meta>標簽
styles 層疊樣式表定義
body_attribs <body>標簽的屬性
body <body>標簽中的內(nèi)容
navbar 用戶定義的導(dǎo)航條
content 用戶定義的頁面內(nèi)容
scripts 文檔底部的JavaScript聲明

表 3-2 中的很多塊都是 Flask-Bootstrap 自用的,
如果直接重定義可能會導(dǎo)致一些問題。
例如,Bootstrap 所需的文件在 styles 和 scripts 塊中聲明。
如果程序需要向已經(jīng)有內(nèi)容的塊中添加新內(nèi)容,
必須使用 Jinja2 提供的 super() 函數(shù)。
例如,如果要在衍生模板中添加新
的 JavaScript 文件,需要這么定義 scripts 塊:

{% block scripts %}
    {{ super() }}
    <script type="text/javascript" src="my-script.js"></script>
{% endblock %}

3.3 自定義錯誤頁面

為什么需要自定義錯誤頁面

現(xiàn)在你在地址欄輸入一個不存在的地址,
出來的404頁面會顯示很多英文,
并且與使用了Bootstrap的頁面不一致,
這或許不是你想要的。

有哪些錯誤頁面

Flask允許程序使用基于模板的自定義錯誤頁面。
最常見的錯誤代碼有兩個:

  • 404, 客戶端請求未知頁面或路由時顯示;
  • 500,有未處理的異常時顯示。

自定義錯誤的示例

@app.errorhandler(404)
def page_not_found(e):
    return render_template('404.html'), 404
 
@app.errorhandler(500)
def internal_server_error(e):
    return render_template('500.html'), 500

裝飾器后出現(xiàn)了新的方法:errorhandler。
此方法指定了它想改變的錯誤頁面。
函數(shù)接受了一個e的錯誤,
這個值在這并沒有被使用。
注意返回渲染的模板后,還加上了錯誤代碼。
我們稍后可以試試不加會怎樣。

布局一致的要求

我們在上例中告訴Flask去尋找404.html和500.html這兩個模板,
而現(xiàn)在templates目錄下沒有這兩個模板,
也不符合extends bootstrap的情況,
如果你無法忍受默認的錯誤頁面,
那這兩個模板必須我們自己編寫。
這些模板應(yīng)該和常規(guī)頁面使用相同的布局,
因此要有一個導(dǎo)航條和顯示錯誤消息的頁面頭部。

編寫錯誤頁面的笨方法

編寫這些模板最直觀的方法是復(fù)制 templates/user.html,
分別創(chuàng)建 templates/404.html 和
templates/500.html,
然后把這兩個文件中的頁面頭部元素改為相應(yīng)的錯誤消息。
但這種方法會帶來很多重復(fù)勞動。

更靈活的方式

Jinja2 的模板繼承機制可以幫助我們解決這一問題。
Flask-Bootstrap 提供了一個具有頁面基本布局的基模板,
同樣,程序可以定義一個具有更完整頁面布局的基模板,
其中包含導(dǎo)航條,
而頁面內(nèi)容則可留到衍生模板中定義。
這個模板本身也可作為其他模板的基模板,
例如 templates/user.html、 templates/404.html 和 templates/500.html。

修改后的基模板

{% extends "bootstrap/base.html" %}
 
{% block title %}Flasky{% endblock %}
 
{% block navbar %}
<div class="navbar navbar-inverse" role="navigation">
    <div class="container">
        <div class="navbar-header">
            <button type="button" class="navbar-toggle"
            data-toggle="collapse" data-target=".navbar-collapse">
                <span class="sr-only">Toggle navigation</span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
            </button>
            <a class="navbar-brand" href="/">Flasky</a>
        </div>
        <div class="navbar-collapse collapse">
            <ul class="nav navbar-nav">
                <li><a href="/">Home</a></li>
            </ul>
        </div>
    </div>
</div>
{% endblock %}
 
{% block content %}
<div class="container">
    {% block page_content %}{% endblock %}
</div>
{% endblock %}

這個模板的 content 塊中只有一個 <div> 容器,
其中包含了一個名為 page_content 的新的空塊,
塊中的內(nèi)容由衍生模板定義。

如何編寫子模板

現(xiàn)在,程序使用的模板繼承自這個模板,
而不直接繼承自 Flask-Bootstrap 的基模板。
通過繼承 templates/base.html 模板編寫自定義的 404 錯誤頁面很簡單,

{% extends "base.html" %}
 
{% block title %}Flasky - Page Not Found{% endblock %}
 
{% block page_content %}
<div class="page-header">
    <h1>Not Found</h1>
</div>
{% endblock %}

注意基模板定義的title塊我們也可以重新修改,
也就是盡量利用父模板的代碼,
但可以進行必要的修改。
現(xiàn)在你可以試試修改index、500和user模板。

3.4 鏈接輔助函數(shù)

為什么需要鏈接輔助

任何具有多個路由的程序都需要可以連接不同頁面的鏈接,例如導(dǎo)航條。
在模板中直接編寫簡單路由的 URL 鏈接不難,
但寫死的URL不具有靈活性,
如果其中有可變部分,
重新定義路由后,模板中的鏈接可能會失效。

url_for()的簡單用法

為了避免這些問題, Flask 提供了 url_for() 輔助函數(shù),
它使用 URL 映射中的信息生成 URL。

url_for() 函數(shù)最簡單的用法是以視圖函數(shù)名作為參數(shù),
返回對應(yīng)的 URL。
例如,在當前版本的 hello.py 程序中調(diào)用 url_for('index') 得到的結(jié)果是 /。
調(diào)用 url_for('index', _external=True) 返回的則是絕對地址,
在這個示例中是 http://localhost:5000/

生成連接程序內(nèi)不同路由的鏈接時,使用相對地址就足夠了。
如果要生成在瀏覽器之外使用的鏈接,
則必須使用絕對地址,
例如在電子郵件中發(fā)送的鏈接。

url_for的更多用法

使用 url_for() 生成動態(tài)地址時,將動態(tài)部分作為關(guān)鍵字參數(shù)傳入。
例如, url_for('user', name='john', _external=True) 的返回結(jié)果是 http://localhost:5000/user/john。

傳入 url_for() 的關(guān)鍵字參數(shù)不僅限于動態(tài)路由中的參數(shù)。
函數(shù)能將任何額外參數(shù)添加到查詢字符串中。
例如, url_for('index', page=2) 的返回結(jié)果是 /?page=2。

在hello.py的最后注釋掉if __name__app.run兩行。
添加以下2行代碼并運行:

with app.test_request_context():
    print(url_for('index'))

test_request_context是Flask實例的一個屬性,
用于在沒有運行Flask實例時的上下文測試,
因為一旦app.run()運行實例,那個print語句就不會起作用。

3.5 靜態(tài)文件

什么是靜態(tài)文件

Web 程序不是僅由 Python 代碼和模板組成。
大多數(shù)程序還會使用靜態(tài)文件,
例如 HTML代碼中引用的圖片、 JavaScript 源碼文件和 CSS。

靜態(tài)路由

你可能還記得在第 2 章中檢查 hello.py 程序的 URL 映射時,
其中有一個 static 路由。
這是因為對靜態(tài)文件的引用被當成一個特殊的路由,
即 /static/<filename>。
例如, 調(diào)用url_for('static', filename='css/styles.css', _external=True)
得 到 的 結(jié) 果 是 http://localhost:5000/static/css/styles.css

默認設(shè)置下, Flask 在程序根目錄中名為 static 的子目錄中尋找靜態(tài)文件。
如果需要,可在static 文件夾中使用子文件夾存放文件。
服務(wù)器收到前面那個 URL 后,
會生成一個響應(yīng),
包含文件系統(tǒng)中 static/css/styles.css 文件的內(nèi)容。

注意filename文件名是完整路徑

定義收藏夾圖標的示例

{% block head %}
    {{ super() }}
    <link rel="shortcut icon" href="{{ url_for('static', filename = 'favicon.ico') }}"type="image/x-icon">
    <link rel="icon" href="{{ url_for('static', filename = 'favicon.ico') }}"type="image/x-icon">
{% endblock %}

圖標的聲明會插入 block head的末尾。
注意如何使用 super() 保留基模板中定義的塊的原始內(nèi)容。

3.6 Flask-Moment:本地化時間

為什么要本地化時間

如果 Web 程序的用戶來自世界各地,
那么處理日期和時間可不是一個簡單的任務(wù)。

服務(wù)器需要統(tǒng)一時間單位,
這和用戶所在的地理位置無關(guān),
所以一般使用協(xié)調(diào)世界時( Coordinated Universal Time, UTC)。
不過用戶看到 UTC 格式的時間會感到困惑,
他們更希望看到當?shù)貢r間,
而且采用當?shù)貞T用的格式。

如何本地化時間

要想在服務(wù)器上只使用 UTC 時間,
一個優(yōu)雅的解決方案是,
把時間單位發(fā)送給 Web 瀏覽器,
轉(zhuǎn)換成當?shù)貢r間, 然后渲染。
Web 瀏覽器可以更好地完成這一任務(wù),
因為它能獲取用戶電腦中的時區(qū)和區(qū)域設(shè)置。

Flask-Moment 是什么

有一個使用 JavaScript 開發(fā)的優(yōu)秀客戶端開源代碼庫,
名為 moment.js( http://momentjs.com/),
它可以在瀏覽器中渲染日期和時間。
Flask-Moment 是一個 Flask 程序擴展,
能把moment.js 集成到 Jinja2 模板中。

如何安裝和初始化

Flask-Moment 可以使用 pip 安裝:

$ pip install flask-moment
from flask_moment import Moment
moment = Moment(app)

除了 moment.js, Flask-Moment 還依賴 jquery.js。
要在 HTML 文檔的某個地方引入這兩個庫,
可以直接引入,這樣可以選擇使用哪個版本,
也可使用擴展提供的輔助函數(shù),
從內(nèi)容分發(fā)網(wǎng)絡(luò)( Content Delivery Network,
CDN)中引入通過測試的版本。
Bootstrap 已經(jīng)引入了 jquery.js,
因此只需引入 moment.js 即可。

下面的例子展示了如何在基模板的 scripts 塊中引入這個庫。

{% block scripts %}
    {{ super() }}
    {{ moment.include_moment() }}  # 引入moment.js庫
{% endblock %}

如何使用Flask-Moment

為了處理時間戳, Flask-Moment 向模板開放了 moment類。

  • hello.py:加入一個datetime變量。
from datetime import datetime
 
@app.route('/')
def index():
    return render_template('index.html',current_time=datetime.utcnow())
  • templates/index.html:使用 Flask-Moment 渲染時間戳
<p>The local date and time is {{ moment(current_time).format('LLL') }}.</p>
<p>That was {{ moment(current_time).fromNow(refresh=True) }}</p>

format('LLL') 根據(jù)客戶端電腦中的時區(qū)和區(qū)域設(shè)置渲染日期和時間。
參數(shù)決定了渲染的方式,
'L' 到 'LLLL' 分別對應(yīng)不同的復(fù)雜度。
format() 函數(shù)還可接受自定義的格式說明符。

第二行中的 fromNow() 渲染相對時間戳,
而且會隨著時間的推移自動刷新顯示的時間。
這個時間戳最開始顯示為“ a few seconds ago”,
但指定 refresh 參數(shù)后,
其內(nèi)容會隨著時間的推移而更新。 如果一直待在這個頁面,
幾分鐘后,會看到顯示的文本變成“ a minuteago”“ 2 minutes ago”等。

其他功能

Flask-Moment 渲染的時間戳可實現(xiàn)多種語言的本地化。
語言可在模板中選擇,把語言代碼
傳給 lang() 函數(shù)即可:
{{ moment.lang("zh_cn") }}
Flask-Moment 實現(xiàn)了 moment.js 中的 format()、 fromNow()、 fromTime()、 calendar()、 valueOf()和 unix() 方法。
你可查閱文檔 http://momentjs.com/docs/#/displaying/
學(xué)習(xí) moment.js 提供的全部格式化選項。

Flask-Monet 假定服務(wù)器端程序處理的時間戳是“純正的” datetime 對象,
且使用 UTC 表示。
關(guān)于純正和細致的日期和時間對象 1 的說明,
請閱讀標準庫中 datetime 包的文檔
https://docs.python.org/2/library/datetime.html

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容