基於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 表單和用戶交互。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM