1. 程式人生 > >基於Python的Web應用開發實戰——3 模板

基於Python的Web應用開發實戰——3 模板

要想開發出易於維護的程式,關鍵在於編寫形式簡潔且結構良好的程式碼。

當目前為止,你看到的示例都太簡單,無法說明這一點,但Flask檢視函式的兩個完全獨立的作用卻被融合在了一起,這就產生了一個問題。

 

檢視函式的作用很明確,即生成請求的響應。

如第2章中的示例,對簡單的請求來所,這就足夠了。

但一般而言,請求會改變程式的狀態,而這種變化也會在檢視函式中產生。

 

例如,使用者在網站中註冊一個一個新賬戶。

使用者在表單中輸入電子郵箱地址和密碼,然後點選提交按鈕。

伺服器接收到包含使用者輸入資料的請求,然後Flask把請求分發到處理註冊請求的檢視函式。

這個檢視函式需要訪問資料庫,新增新使用者,然後生成響應回送瀏覽器。

這兩個過程分別稱為業務邏輯表現邏輯

 

把業務邏輯和表現邏輯混在一起會導致程式碼難以理解和維護。假設要為一個大型表格構建HTML程式碼,表格中的資料由資料庫中讀取的資料以及必要的HTML字元連線在一起。把表現邏輯移到模板中能提升程式的可維護性。

 

模板是一個包含相應文字的檔案,其中包含用佔位變來那個表示的動態部分,其具體值只在請求的上下文中才能知道。

使用真實值替換變數,在返回最終的響應字串,這一過程稱為渲染。

為了渲染模板,Flask使用了一個名為Jinja2的強大模板引擎。

 

3.1 Jinja2模板引擎

形式最簡單的Jinja2模板就是一個包含相應文字的檔案。

示例 3-1 是一個Jinjia2模板,它和示例 2-1 中的index() 檢視函式的響應一樣。

示例 3-1 templates/index.html :Jinjia2模板

<h1>Hello World!</h1>

 

示例 2-2 中,檢視函式user() 返回的響應中包含了一個使用變量表示的動態部分。示例 3-2 實現了這個響應。

示例 3-2 template/user.html :Jinjia2模板

<h1>Hello,{{ name }}</h1>

 

3.1.1 渲染模板

預設情況下,Flask在程式資料夾中的templates子資料夾中尋找模板。

在下一個hello.py版本中,要把前面定義的模板儲存在templates資料夾中,並分別命名為 index.html 和 user.html。

 

程式中的檢視函式需要修改一下,以便渲染這些模板。修改方法參見示例 3-3

示例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)
    
from flask import Flask,render_template
from flask import request
from flask_script import Manager
app = Flask(__name__)
manager = Manager(app)

@app.route("/")                    #路由
def index():                    #檢視函式
    return render_template('index.html')
    
    
@app.route("/user/<name>")        #動態路由
def user(name):
    return render_template('user.html',name=name)
    

if __name__ == "__main__":
    manager.run()
View Code

Flask提供的render_template 函式把Jinjia2模板引擎整合到了程式中。

render_template 函式的一個引數是模板的檔名。

隨後的引數都是鍵值對,表示模板中變數對應的真實值。

在這段程式碼中,第二個模板收到一個名為name的變數。

 

前例中的 name=name 是關鍵字引數,這類關鍵字引數很常見,但如果你不熟悉它們的話,可能會覺得迷惑且難以理解。左邊的“name” 表示引數名,就是模板中使用的佔位符;右邊的“name”是當前作用域中的變數,表示同名引數的值。

 

3.1.2 變數

示例 3-2 在模板中使用 {{ name }} 結構便是一個變數,它是一種特殊的佔位符,告訴模板引擎這個位置的值從渲染模板時使用的資料中獲取。

 

Jinjia2能是被所有型別的變數,設定是一些複雜的型別,例如列表、字典和物件。

在模板中使用變數的一些示例如下:

<p>A value from a dictionary:{{ mydict['key'] }}.</p>
<p>A value from a list:{{ mylist[3] }}.</p>
<p>A value from a list,with a variable index:{{ mylist[myintvar] }}.</p>
<p>A value from an object's method:{{ myobj.somemethod() }}.</p>

 

可以使用過濾器修改變數,過濾器名新增在變數名之後,中間使用豎線分隔。

例如,下述模板以首字母大寫形式顯示變數 name 的值:

Hello,{{ name|capitalize }}

 

表3-1 出了Jinjia2提供的部分常用過濾器。

Jinji2變數過濾器
過濾名 說明
safe 渲染值時不轉義
capitalize 把值的首字母裝換成大寫,其他字母轉換成小寫
lower 把值轉換成小寫形式
upper 把值轉換成大寫形式
title 把值中的每個單詞的首字元都轉換成大寫
trim 把值的收尾空格去掉
striptags 渲染之前把值中所有的HTML標籤都刪掉

safe 過濾器值得特別說明一下。

預設情況下,出於安全考慮,Jinjia2會轉義所有變數。

例如,如果一個變數的值為 ‘ <h1>Hello</h1>’,Jinjia2會將其渲染成 ‘&lt; h1&gt;Hello&lt;/h1&gt;’,瀏覽器能顯示這個h1元素,但不會進行解釋。

很多情況下需要顯示變數中儲存的HTML程式碼,這時就可使用safe過濾器。

 

:千萬別在不可信的值上用safe 過濾器,例如使用者在表單中輸入的文字。

完整的過濾器列表可在Jinjia2文件(http://jinja.pocoo.org/docs/templates/#builtin-filters)中檢視。

 

3.1.3 控制結構

Jinjia2提供了多種控制結構,可用來改變模板的渲染流程。

本節使用簡單的例子介紹其中最有用的控制結構。

 

下面這個例子展示瞭如何在模板中使用條件控制語句

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

 

另一種常見需求是在模板中渲染一組元素

下例展示瞭如何使用for 迴圈實現這一需求:

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

 

Jinja2還支援巨集。巨集類似於Python程式碼中的函式。

例如:

{% macro render_comment(comment) %}
    <li>{{ comment }}</li>
{% endmacro %}

<ul>
    {% for comment in comments %}
        {{ render_comment(comment) }}
    {% endfor %}
</ul>


為了重複使用巨集,我們還可以將其儲存在單獨的檔案中,然後再需要使用的模板中匯入:

{% import 'macro.html' as macros %}
<ul>
    {% for comment in comments %}
        {{ macros.render_comment(comment) }}
    {% endfor %}
</ul>

 

需要在多處重複使用的模板程式碼片段可以寫入單獨的檔案,再包含在所有模組中,以避免重複:

{% include 'common.html' %}

 

另一種重複使用程式碼的強大方式是模板繼承,它類似於Python程式碼中的類繼承。

首先,建立一個名為base.html 的基模板:

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

block 標籤定義的元素可在衍生模板中修改。

在本例中,我們定義了名為 head、title和 block 的塊。

注意,title 包含在 head 中。下面這個示例是基模板的衍生模板:

{% extends "base.html" %}
{% block title %}Index{% endblock %}
{% block head %}
    {{ super() }}
    <style>
    </style>
{% endblock %}
{% block body %}
<h1>Hello,World!</h1>
{% endblock %}

extends 指令聲明瞭這個模板衍生自 base.html。

在 extends 指令之後,基模板中的3個塊被重新定義,模板引擎會將其插入適當的位置。

注意新定義的 head 塊,在基模板中其內容不是空的,所以使用 super() 獲取原來的內容。

 

稍後會展示這些控制結構的具體用法,讓你瞭解一些它們的工作原理。

 

3.2 使用Flask-Bootstrap 整合 Twitter Bootstrap

 BootStrap(http://getbootstrap.com/)是 Twitter 開發的一個開源框架。

它提供的使用者介面元件可用於建立整潔且具有吸引力的網頁,而且這些網頁還能相容所有現代Web瀏覽器。

 

Bootstrap 是客戶端框架,因此不能直接涉及伺服器。

伺服器需要做的只是提供了引用Bootstrap層疊樣式表(CSS)和JavaScript檔案的HTML相應,並在HTML、CSS和JavaScript 程式碼中例項化所需元件。

這些操作最理想的執行場所就是模板。

 

要想在程式中整合Bootstrap,顯然要對模板做所有必要的改動。

不過,更簡單的方法是使用一個名為 Flask-Bootstrap 的Flask 擴充套件,簡化整合的過程。

Flask-Bootstrap使用pip安裝

pip install flask-bootstrap

 

Flask 擴充套件一般都在建立程式示例時初始化。

示例 3-4 是 Flask-Bootstrap的初始化方法。

示例 3-4 hello.py: 初始化 Flask-Bootstrap

from flask-bootstrap import Bootstrap

#......

bootstrap = Bootstarp(app)

 

初始化Flask-Bootstrap 之後,就可以在程式中使用一個包含所有 Bootstrap 檔案的基模板。

這個模板利用Jinja2的模板繼承機制,讓程式擴充套件一個具有基本頁面結構的基模板,其中就有用來引入Bootstrap的元素。

示例 3-5    templates/user.html  : 使用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%}

Jinja2 中的 extends 指令從 Flask-Bootstrap 中匯入 bootstrap/base.html ,從而實現模板繼承。

Flask-Bootstrap 中的基模板提供了一個網頁框架,引入了 Bootstrap總的所有CSS和JavaStript檔案。

 

基模板中定義了可在衍生模板中重定義的塊。Block和endblock指令定義的塊中的內容可新增到基模板中。

 

上面這個 user.html 模板定義了 3個塊,分別名為 title、navbar和content。

這些塊都是及模板提供的,可在衍生模板中重新定義。

title 塊的作用很明顯,其中的內容會出現在渲染後的HTML文件頭部,放在<title>標籤中。

navbar和content這兩個塊分別表示頁面中的導航條和主體內容。

 

在這個模板中,vavbar 塊使用Bootstrap 元件定義了一個簡單的導航條。

content 塊中有個<div>容器,其中包含一個頁面頭部。

之前版本的模板中的歡迎資訊,現在就放在這個頁面頭部。

改動之後的程式,如圖:

 

 Flask-Bootstrap 的 base.html 模板還定義了很多其他塊,都可以在衍生模板中使用。

Flask-Bootstrap基模板中定義的塊

表3-2 Flask-Bootstrap 基模板中定義的塊
塊名 說明
doc  整個HTML文件
html_attribs <html>標籤的屬性
html <html>標籤中的內容
head <head>標籤中的內容
titlte <title>標籤中的內容
metas 一組<meta>標籤
styles 層疊樣式表定義
body_attribs <body>標籤的屬性
body <body>標籤中的內容
navbar 使用者定義的導航條
content 使用者定義的頁面內容
scripts 文件底部的javaScript宣告

上表中很多塊都是Flask-Bootstrap自用的,如果直接重定義可能會導致一些問題。

例如,Bootsrap 所需的檔案在styles 和 scripts 塊中宣告。

如果程式需要向已經有內容的塊中新增新內容,必須使用Jinja2提供的super() 函式。

例如,若果要在衍生模板中新增新的JavaScript檔案,需要這麼定義scripts塊。

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

 

3.3 自定義錯誤頁面

如果在瀏覽器的位址列中輸入了不可用的路由,那麼會顯示一個狀態碼為 404 的錯誤頁面。

現在這個錯誤頁面太簡陋、平庸,而且樣式和使用了 Bootstrap 的頁面不一致。

 

像常規路由一樣,Flask允許程式使用基於模板的自定義頁面。

最常見的錯誤程式碼有兩個:

404 ,客戶端請求未知頁面或路由時顯示;

500 , 有未處理的異常時顯示。

為這兩個錯誤程式碼指定自定義處理程式的方式如示例 3-6 所示。

 

示例3-6 hello.py :自定義錯誤頁面

@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

和檢視函式一樣,錯誤處理程式也會返回響應。

它們還返回與該錯誤對應的數數字狀態碼。

 

錯誤處理程式中引用的模板頁需要編寫。

這些模板應該和常規頁面使用相同的佈局,因此要有一個導航條和顯示錯誤訊息的頁面頭部。

 

編寫這些模板最直觀的方法是複製 templates/user.html ,分別建立 templates/404.html 和 templates/500.html ,然後把這兩個檔案中的頁面為頭部元素改為響應的錯誤訊息。但這種方法會帶來很多重複勞動。

 

Jinja2 的模板繼承機制可以幫助我們解決這一問題。

Flask-Bootstrap 提供了一個具有頁面基本佈局的基模板,同樣,程式可以定義一個具有完整頁面佈局的基模板,其中包含導航條,而頁面內容則可留到衍生模板中定義。

示例 3-7 展示了 templates/base.html 的內容,這時一個繼承自bootstrap/base.html 的新模板,其中定義了導航條。這個模板本身也可作為其他模板的基模板,例如 templates/user.html、templates/404.html 和 templates/500.html 。

示例 3-7 templates/base.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 的新的空塊,該塊中的內容由衍生模板定義。

 

現在,程式使用的模板繼承自這個模板,而不是直接繼承自 Flask-Bootstrap 的基模板。

通過整合 templates/base.html 模板編寫自定義的404錯誤頁面很簡單,如示例 3-8 所示

 

示例3-8 templates/404.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 %}

 

錯誤頁面在瀏覽器總顯示效果:

 

 

templates/user.html 現在可以通過繼承這個基模板來簡化內容。如示例 3-9 所示。

 

示例 3-9   templates/user.html : 使用模板繼承機制簡化頁面模板

{% extends "base.html" %}

{% block title %}Flasky{% endblock%}

{% block page_content %}
<div class="page-header">
    <h1>Hello,{{ name }}!</h1>
</div>
{% endblock %}

 

3.4 連結

 任何具有多個路由的程式都需要可以連線不同頁面的連結,例如導航條。

 

在模板中直接編寫簡單的路由的URL連結不難,但對於包含可變部分的動態路由,在模板中構建正確的URL就很困難。

而且,直接編寫URL會對程式碼中定義的路由產生不必要的依賴關係。

如果重新定義路由,模板中的連結可能會失效。

 

為了避免這些問題,Flask 提供了  url_for() 輔助函式,它可以使用程式URL對映中儲存的資訊生成URL

 

 url_for() 函式最簡單的用法是以檢視函式名(或者 app.add_rul_route() 定義路由時使用的端點名)作為引數,返回對應的URL。

例如,在當前版本的 hello.py 程式中呼叫 url_for('index' )得到的結果是 / 。

呼叫 url_for('index',_external=True) 返回的則是絕對地址,在這個示例中是 http://localhost:5000/ 。

 

生成連線程式內不同路由的連結時,使用相對地址就足夠了。

若要生成在瀏覽器之外使用的連結,則必須使用絕對地址,例如在電子郵件中傳送的連結。

 

使用 url_for() 生成動態地址時,將動態部分作為關鍵字引數傳入。

例如, url_for('user',name='john',_external=True)  返回的結果是 http://localhost:5000/user/john 。

 

傳入 url_for() 的關鍵字引數不僅限於動態路由中的引數。

函式能將任何額外引數新增到查詢字串中。

例如, url_for('index',page=2) 返回的是 /?page=2 。

 

3.5 靜態檔案

Web程式不是僅由Python程式碼和模板組成。

大多數程式還會使用靜態檔案,例如HTML程式碼中引用的圖片、JavaScript原始碼檔案和CSS。

 

你可能還記得在第2章中檢查hello.py 程式的URL對映時,其中有一個static路由。

這是因為對靜態檔案的引用被當成一個特殊的路由。即 /static/<filename>。

例如,呼叫  url_for('static',filename='css/styles.css',_external=True) 

得到的結果是 http://localhost:5000/static/css/styles.css 

 

預設設定下,Flask在程式根目錄中名為static的資料夾中尋找靜態檔案。

如果需要,可在static資料夾中使用子資料夾存放檔案。

伺服器收到前面那個URL後,會生成以響應,包含檔案系統中 static/css/static.css 檔案的內容。

 

示例 3-10 展示瞭如何在程式的基模板中放置 favicon.ico 圖示。這個圖示會顯示在瀏覽器的位址列中。

示例 3-10 templates/base.html :定義收藏夾圖示

{% 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 %}

圖示的宣告會插入 head 塊的末尾。

注意使用super() 保留基模板中定義的塊的原始內容。

 

3.6 使用 Flask-Moment 本地化日期和時間

如果 Web 程式的使用者來時世界各地,那麼處理日期和時間可不是一個簡單的任務。

 

伺服器需要統一時間單位,這和使用者所在的地理位置無關,所以一般使用協調時間時(Coordinated Universal Time ,UTC)。

不過使用者看到UTC格式的時間會感到困惑,它們更希望看到當地時間,而且採用當地管用的格式。

 

要想在伺服器上只使用UTC時間,一個優雅的解決方法是,把時間單位傳送給Web瀏覽器,轉換成當地時間,然後渲染。

Web 瀏覽器可以更好地完成這一任務,因為它能獲取使用者電腦中的時區和區域設定。

 

有一個使用JavaScript 開發的優秀客戶端開原始碼庫,名為 moment.js (http://momentjs.com/),

它可以在瀏覽器中渲染日期和時間。

Flask-Moment 是一個Flask 程式擴充套件,能把moment.js 整合到Jinja2模板中。

Flask-Moment 可以使用 pip 安裝:

pip  install flask-moment

這個擴充套件的初始化方法如示例3-11。

示例 3-11   hellp.py :初始化 Flask-Moment

from flask_moment import Moment
moment = Moment(app)

除了 moment.js ,Flask-Moment 還依賴jquery.js。

要在HTML文件的某個地方引入這兩個庫,可以直接引入,這樣可以選擇使用哪個版本,也可以使用擴充套件提供的輔助函式,從內容分發網路(Content Delivery ,CDN)中引入通過測試的版本。

Bootstrap 已經引入了 jquery.js,因此只需要引入 moment.js 即可。

 

示例 3-12 展示瞭如何在基模板的scripts 塊中引入這個庫。

示例 3-12   templates/base.html :引入Moment.js 庫

{% block scripts %}
{{ super() }}
{{ moment.include_moment() }}
{% endblock %}

為了處理時間戳,Flask-Moment 向模板開放了moment 類。示例 3-13 中程式碼把變數current_time 傳入模板進行薰染。

示例 3-13   hello.py:加入一個datetime變數

from datetime import datetime

@app.route("/")                    
def index():                    
    return render_template('index.html',current_time=datetime.utcnow())

 

示例 3-14 展示瞭如何在模板中渲染 current_time。

示例 3-14   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') 根據客戶端電腦中的時區和區域設定渲染日期和時間。

引數決定了渲染的方式,‘L’ 到 ‘LLL’ 分貝對應不同的複雜度。

format() 函式還可接受自定義的格式說明符。

 

第二行中的 fromNow() 渲染相對時間戳,而且會隨著時間的推移自動重新整理顯示的時間。

這個時間戳最開始顯示為 “a few seconds ago”,但指定 refresh 引數後,其內容會隨著時間的推移個更新。

如果一直待在這個頁面,幾分鐘後,會看到顯示的文字變成 "a mimute age" "2 minutes age"等。

 

Flask-Moment 實現了 moment.js中的 format()fromNow()calendar()valueOf()unix() 方法。

你可查閱文件(http://momentjs.com/docs/#/displaying/)學習moment.js提供的全部格式化選項。

 

Flask-Moment 假定伺服器端程式處理的時間戳是“純正的”datetime物件,且使用UTC表示

關於純正細緻的日期和時間物件的說明,請閱讀標準庫中datetime包的文件(https://docs.python.org/3/library/datetime.html)

( 純正的時間戳,英文為 navie time,指不包含時區的時間戳;
  細緻的時間戳,英文為 aware time,指包含時區的時間戳) 

 

Flask-Moment 渲染的時間戳可實現多種語言的本地化。

語言可在模板中選擇,把語言程式碼傳給 lang() 函式即可:

{{ moment.lang('es') }}

 

使用本章介紹的技術,你應該能為程式編寫出現代化且使用者友好的網頁。

下一章將介紹本章沒有涉及的一個模板功能,即如果通過Web 表單和使用者互動。