Flask入門系列(一)–Hello Worldjavascript
項目開發中,常常要寫一些小系統來輔助,好比監控系統,配置系統等等。用傳統的Java寫,太笨重了,連PHP都嫌麻煩。一直在尋找一個輕量級的後臺框架,學習成本低,維護簡單。發現Flask後,我立馬被它的輕巧所吸引,它充分發揮了Python語言的優雅和輕便,連Django這樣強大的框架在它面前都以爲繁瑣。能夠說簡單就是美。這裏咱們不討論到底哪一個框架語言更好,只是從簡單這個角度出發,Flask絕對是佼佼者。這一系列文章就會給你們展現Flask的輕巧之美。css
系列文章html
Hello World前端
程序員的經典學習方法,從Hello World開始。不要忘了,先安裝python, pip,而後運行」pip install Flask」,環境就裝好了。固然本人仍是強烈建議使用virtualenv來安裝環境。細節就很少說了,讓咱們寫個Hello World吧:java
1node 2python 3mysql 4git 5程序員 6 7 8 9 |
from flask import Flask app = Flask(__name__)
@app.route('/') def index(): return '<h1>Hello World</h1>'
if __name__ == '__main__': app.run() |
一個Web應用的代碼就寫完了,對,就是這麼簡單!保存爲」hello.py」,打開控制檯,到該文件目錄下,運行
$ python hello.py
看到」* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)」字樣後,就說明服務器啓動完成。打開你的瀏覽器,訪問」http://127.0.0.1:5000/」,一個碩大的」Hello World」映入眼簾:)。
簡單解釋下這段代碼
1 2 |
from flask import Flask app = Flask(__name__) |
這裏給的實例名稱就是這個python模塊名。
1 |
@app.route('/') |
這個函數級別的註解指明瞭當地址是根路徑時,就調用下面的函數。能夠定義多個路由規則,會在下篇文章裏詳細介紹。說的高大上些,這裏就是MVC中的Contoller。
1 2 |
def index(): return '<h1>Hello World</h1>' |
當請求的地址符合路由規則時,就會進入該函數。能夠說,這裏是MVC的Model層。你能夠在裏面獲取請求的request對象,返回的內容就是response。本例中的response就是大標題」Hello World」。
1 2 |
if __name__ == '__main__': app.run() |
當本文件爲程序入口(也就是用python命令直接執行本文件)時,就會經過」app.run()」啓動Web服務器。若是不是程序入口,那麼該文件就是一個模塊。Web服務器會默認監聽本地的5000端口,但不支持遠程訪問。若是你想支持遠程,須要在」run()」方法傳入」host=0.0.0.0″,想改變監聽端口的話,傳入」port=端口號」,你還能夠設置調試模式。具體例子以下:
1 2 |
if __name__ == '__main__': app.run(host='0.0.0.0', port=8888, debug=True) |
注意,Flask自帶的Web服務器主要仍是給開發人員調試用的,在生產環境中,你最好是經過WSGI將Flask工程部署到相似Apache或Nginx的服務器上。
本例中的代碼能夠在這裏下載。
Flask入門系列(二)–路由
上一篇中,咱們用Flask寫了一個Hello World程序,讓你們領略到了Flask的簡潔輕便。從這篇開始咱們將對Flask框架的各功能做更詳細的介紹,咱們首先從路由(Route)開始。
系列文章
路由
從Hello World中,咱們瞭解到URL的路由能夠直接寫在其要執行的函數上。有人會質疑,這樣不是把Model和Controller綁在一塊兒了嗎?的確,若是你想靈活的配置Model和Controller,這樣是不方便,可是對於輕量級系統來講,靈活配置意義不大,反而寫在一塊更利於維護。Flask路由規則都是基於Werkzeug的路由模塊的,它還提供了不少強大的功能。
帶參數的路由
讓咱們在上一篇Hello World的基礎上,加上下面的函數。並運行程序。
1 2 3 |
@app.route('/hello/<name>') def hello(name): return 'Hello %s' % name |
當你在瀏覽器的地址欄中輸入」http://localhost:5000/hello/man」,你將在頁面上看到」Hello man」的字樣。URL路徑中」/hello/」後面的參數被做爲」hello()」函數的」name」參數傳了進來。
你還能夠在URL參數前添加轉換器來轉換參數類型,咱們再來加個函數:
1 2 3 |
@app.route('/user/<int:user_id>') def get_user(user_id): return 'User ID: %d' % user_id |
試下訪問」http://localhost:5000/user/man」,你會看到404錯誤。可是試下」http://localhost:5000/user/123″,頁面上就會有」User ID: 123″顯示出來。參數類型轉換器」int:」幫你控制好了傳入參數的類型只能是整形。目前支持的參數類型轉換器有:
類型轉換器 |
做用 |
缺省 |
字符型,但不能有斜槓 |
int: |
整型 |
float: |
浮點型 |
path: |
字符型,可有斜槓 |
另外,你們有沒有注意到,Flask自帶的Web服務器支持熱部署。當你修改好文件並保存後,Web服務器自動部署完畢,你無需從新運行程序。
多URL的路由
一個函數上能夠設施多個URL路由規則
1 2 3 4 5 6 7 |
@app.route('/') @app.route('/hello') @app.route('/hello/<name>') def hello(name=None): if name is None: name = 'World' return 'Hello %s' % name |
這個例子接受三種URL規則,」/」和」/hello」都不帶參數,函數參數」name」值將爲空,頁面顯示」Hello World」;」/hello/「帶參數,頁面會顯示參數」name」的值,效果與上面第一個例子相同。
HTTP請求方法設置
HTTP請求方法經常使用的有Get, Post, Put, Delete。不熟悉的朋友們能夠去度娘查下。Flask路由規則也能夠設置請求方法。
1 2 3 4 5 6 7 8 |
from flask import request
@app.route('/login', methods=['GET', 'POST']) def login(): if request.method == 'POST': return 'This is a POST request' else: return 'This is a GET request' |
當你請求地址」http://localhost:5000/login」,」GET」和」POST」請求會返回不一樣的內容,其餘請求方法則會返回405錯誤。有沒有以爲用Flask來實現Restful風格很方便啊?
URL構建方法
Flask提供了」url_for()」方法來快速獲取及構建URL,方法的第一個參數指向函數名(加過」@app.route」註解的函數),後續的參數對應於要構建的URL變量。下面是幾個例子:
1 2 3 4 |
url_for('login') # 返回/login url_for('login', id='1') # 將id做爲URL參數,返回/login?id=1 url_for('hello', name='man') # 適配hello函數的name參數,返回/hello/man url_for('static', filename='style.css') # 靜態文件地址,返回/static/style.css |
靜態文件位置
一個Web應用的靜態文件包括了JS, CSS, 圖片等,Flask的風格是將全部靜態文件放在」static」子目錄下。而且在代碼或模板(下篇會介紹)中,使用」url_for(‘static’)」來獲取靜態文件目錄。上小節中第四個的例子就是經過」url_for()」函數獲取」static」目錄下的指定文件。若是你想改變這個靜態目錄的位置,你能夠在建立應用時,指定」static_folder」參數。
1 |
app = Flask(__name__, static_folder='files') |
本例中的代碼能夠在這裏下載。
Flask入門系列(三)–模板
在第一篇中,咱們講到了Flask中的Controller和Model,可是一個完整的MVC,沒有View怎麼行?前端代碼若是都靠後臺拼接而成,就太麻煩了。本篇,咱們就介紹下Flask中的View,即模板。
系列文章
模板
Flask的模板功能是基於Jinja2模板引擎實現的。讓咱們來實現一個例子吧。建立一個新的Flask運行文件(你應該不會忘了怎麼寫吧),代碼以下:
1 2 3 4 5 6 7 8 9 10 11 12 |
from flask import Flask from flask import render_template
app = Flask(__name__)
@app.route('/hello') @app.route('/hello/<name>') def hello(name=None): return render_template('hello.html', name=name)
if __name__ == '__main__': app.run(host='0.0.0.0', debug=True) |
這段代碼同上一篇的多URL路由的例子很是類似,區別就是」hello()」函數並非直接返回字符串,而是調用了」render_template()」方法來渲染模板。方法的第一個參數」hello.html」指向你想渲染的模板名稱,第二個參數」name」是你要傳到模板去的變量,變量能夠傳多個。
那麼這個模板」hello.html」在哪兒呢,變量參數又該怎麼用呢?別急,接下來咱們建立模板文件。在當前目錄下,建立一個子目錄」templates」(注意,必定要使用這個名字)。而後在」templates」目錄下建立文件」hello.html」,內容以下:
1 2 3 4 5 6 7 |
<!doctype html> <title>Hello Sample</title> {% if name %} <h1>Hello {{ name }}!</h1> {% else %} <h1>Hello World!</h1> {% endif %} |
這段代碼是否是很像HTML?接觸過其餘模板引擎的朋友們確定立馬秒懂了這段代碼。它就是一個HTML模板,根據」name」變量的值,顯示不一樣的內容。變量或表達式由」{{ }}」修飾,而控制語句由」{% %}」修飾,其餘的代碼,就是咱們常見的HTML。
讓咱們打開瀏覽器,輸入」http://localhost:5000/hello/man」,頁面上即顯示大標題」Hello man!」。咱們再看下頁面源代碼
1 2 3 4 |
<!doctype html> <title>Hello from Flask</title>
<h1>Hello man!</h1> |
果真,模板代碼進入了」Hello {{ name }}!」分支,並且變量」{{ name }}」被替換爲了」man」。Jinja2的模板引擎還有更多強大的功能,包括for循環,過濾器等。模板裏也能夠直接訪問內置對象如request, session等。對於Jinja2的細節,感興趣的朋友們能夠本身去查查。
模板繼承
通常咱們的網站雖然頁面多,可是不少部分是重用的,好比頁首,頁腳,導航欄之類的。對於每一個頁面,都要寫這些代碼,很麻煩。Flask的Jinja2模板支持模板繼承功能,省去了這些重複代碼。讓咱們基於上面的例子,在」templates」目錄下,建立一個名爲」layout.html」的模板:
1 2 3 4 5 6 7 |
<!doctype html> <title>Hello Sample</title> <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='style.css') }}"> <div class="page"> {% block body %} {% endblock %} </div> |
再修改以前的」hello.html」,把原來的代碼定義在」block body」中,並在代碼一開始」繼承」上面的」layout.html」:
1 2 3 4 5 6 7 8 |
{% extends "layout.html" %} {% block body %} {% if name %} <h1>Hello {{ name }}!</h1> {% else %} <h1>Hello World!</h1> {% endif %} {% endblock %} |
打開瀏覽器,再看下」http://localhost:5000/hello/man」頁面的源碼。
1 2 3 4 5 6 7 8 |
<!doctype html> <title>Hello Sample</title> <link rel="stylesheet" type="text/css" href="/static/style.css"> <div class="page">
<h1>Hello man!</h1>
</div> |
你會發現,雖然」render_template()」加載了」hello.html」模板,可是」layout.html」的內容也一塊兒被加載了。並且」hello.html」中的內容被放置在」layout.html」中」{% block body %}」的位置上。形象的說,就是」hello.html」繼承了」layout.html」。
HTML自動轉義
咱們看下下面的代碼:
1 2 3 |
@app.route('/') def index(): return '<div>Hello %s</div>' % '<em>Flask</em>' |
打開頁面,你會看到」Hello Flask」字樣,並且」Flask」是斜體的,由於咱們加了」em」標籤。但有時咱們並不想讓這些HTML標籤自動轉義,特別是傳遞表單參數時,很容易致使HTML注入的漏洞。咱們把上面的代碼改下,引入」Markup」類:
1 2 3 4 5 6 7 |
from flask import Flask, Markup
app = Flask(__name__)
@app.route('/') def index(): return Markup('<div>Hello %s</div>') % '<em>Flask</em>' |
再次打開頁面,」em」標籤顯示在頁面上了。Markup還有不少方法,好比」escape()」呈現HTML標籤, 「striptags()」去除HTML標籤。這裏就不一一列舉了。
咱們會在Flask進階系列裏對模板功能做更詳細的介紹。
本文中的示例代碼能夠在這裏下載。
Flask入門系列(四)–請求,響應及會話
一個完整的HTTP請求,包括了客戶端的請求Request,服務器端的響應Response,會話Session等。一個基本的Web框架必定會提供內建的對象來訪問這些信息,Flask固然也不例外。咱們來看看在Flask中該怎麼使用這些內建對象。
系列文章
Flask內建對象
Flask提供的內建對象經常使用的有request, session, g,經過request,你還能夠獲取cookie對象。這些對象不但能夠在請求函數中使用,在模板中也可使用。
請求對象request
引入flask包中的request對象,就能夠直接在請求函數中直接使用該對象了。讓咱們改進下第二篇中的login方法:
1 2 3 4 5 6 7 8 9 10 11 |
from flask import request
@app.route('/login', methods=['POST', 'GET']) def login(): if request.method == 'POST': if request.form['user'] == 'admin': return 'Admin login successfully!' else: return 'No such user!' title = request.args.get('title', 'Default') return render_template('login.html', title=title) |
在第三篇的templates目錄下,添加」login.html」文件
1 2 3 4 5 6 7 |
{% extends "layout.html" %} {% block body %} <form name="login" action="/login" method="post"> Hello {{ title }}, please login by: <input type="text" name="user" /> </form> {% endblock %} |
執行上面的例子,結果我就很少描述了。簡單解釋下,request中」method」變量能夠獲取當前請求的方法,即」GET」, 「POST」, 「DELETE」, 「PUT」等;」form」變量是一個字典,能夠獲取Post請求表單中的內容,在上例中,若是提交的表單中不存在」user」項,則會返回一個」KeyError」,你能夠不捕獲,頁面會返回400錯誤(想避免拋出這」KeyError」,你能夠用request.form.get(「user」)來替代)。而」request.args.get()」方法則能夠獲取Get請求URL中的參數,該函數的第二個參數是默認值,當URL參數不存在時,則返回默認值。request的詳細使用可參閱Flask的官方API文檔。
會話對象session
會話能夠用來保存當前請求的一些狀態,以便於在請求以前共享信息。咱們將上面的python代碼改動下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
from flask import request, session
@app.route('/login', methods=['POST', 'GET']) def login(): if request.method == 'POST': if request.form['user'] == 'admin': session['user'] = request.form['user'] return 'Admin login successfully!' else: return 'No such user!' if 'user' in session: return 'Hello %s!' % session['user'] else: title = request.args.get('title', 'Default') return render_template('login.html', title=title)
app.secret_key = '123456' |
你能夠看到,」admin」登錄成功後,再打開」login」頁面就不會出現表單了。session對象的操做就跟一個字典同樣。特別提醒,使用session時必定要設置一個密鑰」app.secret_key」,如上例。否則你會獲得一個運行時錯誤,內容大體是」RuntimeError: the session is unavailable because no secret key was set」。密鑰要儘可能複雜,最好使用一個隨機數,這樣不會有重複,上面的例子不是一個好密鑰。
咱們順便寫個登出的方法,估計我不放例子,你們也都猜到怎麼寫,就是清除字典裏的鍵值:
1 2 3 4 5 6 |
from flask import request, session, redirect, url_for
@app.route('/logout') def logout(): session.pop('user', None) return redirect(url_for('login')) |
關於」redirect」方法,咱們會在下一篇介紹。
構建響應
在以前的例子中,請求的響應咱們都是直接返回字符串內容,或者經過模板來構建響應內容而後返回。其實咱們也能夠先構建響應對象,設置一些參數(好比響應頭)後,再將其返回。修改下上例中的Get請求部分:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
from flask import request, session, make_response
@app.route('/login', methods=['POST', 'GET']) def login(): if request.method == 'POST': ... if 'user' in session: ... else: title = request.args.get('title', 'Default') response = make_response(render_template('login.html', title=title), 200) response.headers['key'] = 'value' return response |
打開瀏覽器調試,在Get請求用戶未登陸狀態下,你會看到響應頭中有一個」key」項。」make_response」方法就是用來構建response對象的,第二個參數表明響應狀態碼,缺省就是200。response對象的詳細使用可參閱Flask的官方API文檔。
Cookie的使用
提到了Session,固然也要介紹Cookie嘍,畢竟沒有Cookie,Session就根本無法用(不知道爲何?查查去)。Flask中使用Cookie也很簡單:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
from flask import request, session, make_response import time
@app.route('/login', methods=['POST', 'GET']) def login(): response = None if request.method == 'POST': if request.form['user'] == 'admin': session['user'] = request.form['user'] response = make_response('Admin login successfully!') response.set_cookie('login_time', time.strftime('%Y-%m-%d %H:%M:%S')) ... else: if 'user' in session: login_time = request.cookies.get('login_time') response = make_response('Hello %s, you logged in on %s' % (session['user'], login_time)) ...
return response |
例子愈來愈長了,此次咱們引入了」time」模塊來獲取當前系統時間。咱們在返回響應時,經過」response.set_cookie()」函數,來設置Cookie項,以後這個項值會被保存在瀏覽器中。這個函數的第三個參數(max_age)能夠設置該Cookie項的有效期,單位是秒,不設的話,在瀏覽器關閉後,該Cookie項即失效。
在請求中,」request.cookies」對象就是一個保存了瀏覽器Cookie的字典,使用其」get()」函數就能夠獲取相應的鍵值。
全局對象g
「flask.g」是Flask一個全局對象,這裏有點容易讓人誤解,其實」g」的做用範圍,就在一個請求(也就是一個線程)裏,它不能在多個請求間共享。你能夠在」g」對象裏保存任何你想保存的內容。一個最經常使用的例子,就是在進入請求前,保存數據庫鏈接。這個咱們會在介紹數據庫集成時講到。
本例中的代碼能夠在這裏下載。
Flask入門系列(五)–錯誤處理及消息閃現
本篇將補充一些Flask的基本功能,包括錯誤處理,URL重定向,日誌功能,還有一個頗有趣的消息閃現功能。
系列文章
錯誤處理
使用」abort()」函數能夠直接退出請求,返回錯誤代碼:
1 2 3 4 5 |
from flask import abort
@app.route('/error') def error(): abort(404) |
上例會顯示瀏覽器的404錯誤頁面。有時候,咱們想要在遇到特定錯誤代碼時作些事情,或者重寫錯誤頁面,能夠用下面的方法:
1 2 3 |
@app.errorhandler(404) def page_not_found(error): return render_template('404.html'), 404 |
此時,當再次遇到404錯誤時,即會調用」page_not_found()」函數,其返回」404.html」的模板頁。第二個參數表明錯誤代碼。
不過,在實際開發過程當中,咱們並不會常用」abort()」來退出,經常使用的錯誤處理方法通常都是異常的拋出或捕獲。裝飾器」@app.errorhandler()」除了能夠註冊錯誤代碼外,還能夠註冊指定的異常類型。讓咱們來自定義一個異常:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class InvalidUsage(Exception): status_code = 400
def __init__(self, message, status_code=400): Exception.__init__(self) self.message = message self.status_code = status_code
@app.errorhandler(InvalidUsage) def invalid_usage(error): response = make_response(error.message) response.status_code = error.status_code return response |
咱們在上面的代碼中定義了一個異常」InvalidUsage」,同時咱們經過裝飾器」@app.errorhandler()」修飾了函數」invalid_usage()」,裝飾器中註冊了咱們剛定義的異常類。這也就意味着,一但遇到」InvalidUsage」異常被拋出,這個」invalid_usage()」函數就會被調用。寫個路由試一試吧。
1 2 3 |
@app.route('/exception') def exception(): raise InvalidUsage('No privilege to access the resource', status_code=403) |
URL重定向
重定向」redirect()」函數的使用在上一篇logout的例子中已有出現。做用就是當客戶端瀏覽某個網址時,將其導向到另外一個網址。常見的例子,好比用戶在未登陸時瀏覽某個需受權的頁面,咱們將其重定向到登陸頁要求其登陸先。
1 2 3 4 5 6 7 8 |
from flask import session, redirect
@app.route('/') def index(): if 'user' in session: return 'Hello %s!' % session['user'] else: return redirect(url_for('login'), 302) |
「redirect()」的第二個參數時HTTP狀態碼,可取的值有301, 302, 303, 305和307,默認即302(爲何沒有304?留給你們去思考)。
日誌
提到錯誤處理,那必定要說到日誌。Flask提供logger對象,其是一個標準的Python Logger類。修改上例中的」exception()」函數:
1 2 3 4 5 |
@app.route('/exception') def exception(): app.logger.debug('Enter exception method') app.logger.error('403 error happened') raise InvalidUsage('No privilege to access the resource', status_code=403) |
執行後,你會在控制檯看到日誌信息。在debug模式下,日誌會默認輸出到標準錯誤stderr中。你能夠添加FileHandler來使其輸出到日誌文件中去,也能夠修改日誌的記錄格式,下面演示一個簡單的日誌配置代碼:
import logging
from logging.handlers import TimedRotatingFileHandler
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
server_log = TimedRotatingFileHandler('server.log','D') server_log.setLevel(logging.DEBUG) server_log.setFormatter(logging.Formatter( '%(asctime)s %(levelname)s: %(message)s' ))
error_log = TimedRotatingFileHandler('error.log', 'D') error_log.setLevel(logging.ERROR) error_log.setFormatter(logging.Formatter( '%(asctime)s: %(message)s [in %(pathname)s:%(lineno)d]' ))
app.logger.addHandler(server_log) app.logger.addHandler(error_log) |
上例中,咱們在本地目錄下建立了兩個日誌文件,分別是」server.log」記錄全部級別日誌;」error.log」只記錄錯誤日誌。咱們分別給兩個文件不一樣的內容格式。另外,咱們使用了」TimedRotatingFileHandler」並給了參數」D」,這樣日誌天天會建立一個新的文件,並將舊文件加日期後綴來歸檔。
注:執行後會生成server.log和error.log倆文件,訪問正常或錯誤頁面這倆均無內容,不知道是自己相關代碼有問題,仍是訪問方式有問題
你還能夠將錯誤信息發送郵件。更詳細的日誌使用可參閱Python logging官方文檔。
消息閃現
「Flask Message」是一個頗有意思的功能,通常一個操做完成後,咱們都但願在頁面上閃出一個消息,告訴用戶操做的結果。用戶看完後,這個消息就不復存在了。Flask提供的」flash」功能就是爲了這個。咱們仍是拿用戶登陸來舉例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
from flask import render_template, request, session, url_for, redirect, flash
@app.route('/') def index(): if 'user' in session: return render_template('hello.html', name=session['user']) else: return redirect(url_for('login'), 302)
@app.route('/login', methods=['POST', 'GET']) def login(): if request.method == 'POST': session['user'] = request.form['user'] flash('Login successfully!') return redirect(url_for('index')) else: return ''' <form name="login" action="/login" method="post"> Username: <input type="text" name="user" /> </form> ''' |
上例中,當用戶登陸成功後,就用」flash()」函數閃出一個消息。讓咱們找回第三篇中的模板代碼,在」layout.html」加上消息顯示的部分:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<!doctype html> <title>Hello Sample</title> <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='style.css') }}"> {% with messages = get_flashed_messages() %} {% if messages %} <ul class="flash"> {% for message in messages %} <li>{{ message }}</li> {% endfor %} </ul> {% endif %} {% endwith %} <div class="page"> {% block body %} {% endblock %} </div> |
上例中」get_flashed_messages()」函數就會獲取咱們在」login()」中經過」flash()」閃出的消息。從代碼中咱們能夠看出,閃出的消息能夠有多個。模板」hello.html」不用改。運行下試試。登陸成功後,是否是出現了一條」Login successfully」文字?再刷新下頁面,你會發現文字消失了。你能夠經過CSS來控制這個消息的顯示方式。
「flash()」方法的第二個參數是消息類型,可選擇的有」message」, 「info」, 「warning」, 「error」。你能夠在獲取消息時,同時獲取消息類型,還能夠過濾特定的消息類型。只需設置」get_flashed_messages()」方法的」with_categories」和」category_filter」參數便可。好比,Python部分可改成:
1 2 3 4 5 6 7 8 |
@app.route('/login', methods=['POST', 'GET']) def login(): if request.method == 'POST': session['user'] = request.form['user'] flash('Login successfully!', 'message') flash('Login as user: %s.' % request.form['user'], 'info') return redirect(url_for('index')) ... |
layout模板部分可改成:
1 2 3 4 5 6 7 8 9 10 11 |
... {% with messages = get_flashed_messages(with_categories=true, category_filter=["message","error"]) %} {% if messages %} <ul class="flash"> {% for category, message in messages %} <li class="{{ category }}">{{ category }}: {{ message }}</li> {% endfor %} </ul> {% endif %} {% endwith %} ... |
運行結果你們就本身試試吧。
本例中的代碼能夠在這裏下載。
轉眼,咱們要進入本系列的最後一篇了。一個基本的Web應用功能其實已經講完了,如今就讓咱們引入數據庫。簡單起見,咱們就使用SQLite3做爲例子。
既然前幾篇都用用戶登陸做爲例子,咱們這篇就繼續講登陸,只是登陸的信息會由數據庫來驗證。讓咱們先準備SQLite環境吧。
怎麼安裝SQLite這裏就不說了。咱們先寫個數據庫表的初始化SQL,保存在」init.sql」文件中:
1 2 3 4 5 6 7 8 9 |
drop table if exists users; create table users ( id integer primary key autoincrement, name text not null, password text not null );
insert into users (name, password) values ('visit', '111'); insert into users (name, password) values ('admin', '123'); |
運行sqlite3命令,初始化數據庫。咱們的數據庫文件就放在」db」子目錄下的」user.db」文件中。
$ sqlite3 db/user.db < init.sql
建立配置文件"config.py",保存配置信息:
1 2 3 4 |
#coding:utf8 DATABASE = 'db/user.db' # 數據庫文件位置 DEBUG = True # 調試模式 SECRET_KEY = 'secret_key_1' # 會話密鑰 |
在建立Flask應用時,導入配置信息:
1 2 3 4 5 |
from flask import Flask import config
app = Flask(__name__) app.config.from_object('config') |
這裏也能夠用"app.config.from_envvar('FLASK_SETTINGS', silent=True)"方法來導入配置信息,此時程序會讀取系統環境變量中"FLASK_SETTINGS"的值,來獲取配置文件路徑,並加載此文件。若是文件不存在,該語句返回False。參數"silent=True"表示忽略錯誤。
這裏要用到請求的上下文裝飾器,咱們會在進階系列的第一篇裏詳細介紹上下文。
1 2 3 4 5 6 7 8 9 |
@app.before_request def before_request(): g.db = sqlite3.connect(app.config['DATABASE'])
@app.teardown_request def teardown_request(exception): db = getattr(g, 'db', None) if db is not None: db.close() |
咱們在"before_request()"裏創建數據庫鏈接,它會在每次請求開始時被調用;並在"teardown_request()"關閉它,它會在每次請求關閉前被調用。
讓咱們取回上一篇登陸部分的代碼,"index()"和"logout()"請求不用修改,在"login()"請求中,咱們會查詢數據庫,驗證客戶端輸入的用戶名和密碼是否存在:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
@app.route('/login', methods=['POST', 'GET']) def login(): if request.method == 'POST': name = request.form['user'] passwd = request.form['passwd'] cursor = g.db.execute('select * from users where name=? and password=?', [name, passwd]) if cursor.fetchone() is not None: session['user'] = name flash('Login successfully!') return redirect(url_for('index')) else: flash('No such user!', 'error') return redirect(url_for('login')) else: return render_template('login.html') |
模板中加上"login.html"文件
1 2 3 4 5 6 7 8 |
{% extends "layout.html" %} {% block body %} <form name="login" action="/login" method="post"> Username: <input type="text" name="user" /><br> Password: <input type="password" name="passwd" /><br> <input type="submit" value="Submit" /> </form> {% endblock %} |
終於一個真正的登陸驗證寫完了(前幾篇都是假的),打開瀏覽器登陸下吧。由於比較懶,就不寫CSS美化了,受不了這粗糙界面的朋友們就本身調吧。
到目前爲止,Flask的基礎功能已經介紹完了,是否很想動手寫個應用啦?其實Flask還有更強大的高級功能,以後會在進階系列裏介紹。
本例中的代碼能夠在這裏下載。
讓咱們開啓Jinja2模板引擎之旅,雖然說標題是Flask中的Jinja2,其實介紹的主要是Jinja2自己,Flask是用來作例子的。若是對Flask不熟悉的朋友們建議將本博客的入門系列先看下。怎麼,不知道什麼是模板引擎?你能夠將模板比做MVC模式中的View視圖層,而模板引擎就是用來將模板同業務代碼分離,並解析模板語言的程序。你能夠耐心地看下本系列文章,就能體會到什麼是模板引擎了。
咱們在Flask入門系列第三篇中已經介紹了Jinja2模板的基本使用方式,讓咱們先回顧下,把其中的代碼拿過來。
Flask Python代碼:
1 2 3 4 5 6 7 8 9 10 11 |
from flask import Flask,render_template
app = Flask(__name__)
@app.route('/hello') @app.route('/hello/<name>') def hello(name=None): return render_template('hello.html', name=name)
if __name__ == '__main__': app.run(host='0.0.0.0', debug=True) |
模板代碼:
1 2 3 4 5 6 7 |
<!doctype html> <title>Hello Sample</title> {% if name %} <h1>Hello {{ name }}!</h1> {% else %} <h1>Hello World!</h1> {% endif %} |
咱們瞭解到,模板的表達式都是包含在分隔符」{{ }}」內的;控制語句都是包含在分隔符」{% %}」內的;另外,模板也支持註釋,都是包含在分隔符」{# #}」內,支持塊註釋。
表達式通常有這麼幾種:
有沒有以爲,這裏的表達式很像Python的語法呀?
Jinja2的控制語句主要就是條件控制語句if,和循環控制語句for,語法相似於Python。咱們能夠改動下上節的模板代碼:
1 2 3 4 5 6 7 |
{% if name and name == 'admin' %} <h1>This is admin console</h1> {% elif name %} <h1>Welcome {{ name }}!</h1> {% else %} <h1>Please login</h1> {% endif %} |
上面是一個條件控制語句的例子,注意if控制語句要用」{% endif %}」來結束。模板中沒法像代碼中同樣靠縮進來判斷代碼塊的結束。再來看個循環的例子,咱們先改下Python代碼中的」hello」函數,讓其傳兩個列表進模板。
1 2 3 4 5 6 |
def hello(name=None): return render_template('hello-1.html', name=name, digits=[1,2,3,4,5], users=[{'name':'John'}, {'name':'Tom', 'hidden':True}, {'name':'Lisa'} {'name':'Bob'}]) |
而後在模板中加上:
1 2 3 |
{% for digit in digits %} {{ digit }} {% endfor %} |
是否是列表被顯示出來了?同if語句同樣,for控制語句要用」{% endfor %}」來結束。頁面上,每一個元素之間會有空格,若是你不但願有空格,就要在」for」語句的最後,和」endfor」語句的最前面各加上一個」-「號。如:
1 2 3 |
{% for digit in digits -%} {{ digit }} {%- endfor %} |
如今,你能夠看到數字」12345″被一塊兒顯示出來了。咱們再來看個複雜的循環例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<dl> {% for user in users if not user.hidden %} {% if loop.first %} <div>User List:</div> {% endif %} <div class="{{ loop.cycle('odd', 'even') }}"> <dt>User No {{ loop.index }}:</dt> <dd>{{ user.name }}</dd> </div> {% if loop.last %} <div>Total Users: {{ loop.length }}</div> {% endif %} {% else %} <li>No users found</li> {% endfor %} </dl> |
這裏有三個知識點。首先for循環支持else語句,當待遍歷的列表」users」爲空或者爲None時,就進入else語句。
其次,在for語句後使用if關鍵字,能夠對循環中的項做過濾。本例中,全部hidden屬性爲True的user都會被過濾掉。
另外,for循環中能夠訪問Jinja2的循環內置變量。本例中,咱們會在第一項前顯示標題,最後一項後顯示總數,每一項顯示序號。另外,奇偶項的HTML div元素會有不一樣的class。若是咱們加入下面的CSS style,就能看到斑馬線。
直接在html頁面中寫入下列內容便可。
不過也可單獨寫入一個css文件路,不過須要引用才能生效
1 2 3 4 5 |
<style type="text/css"> .odd { background-color: #BDF; } </style> |
Jinja2的循環內置變量主要有如下幾個:
變量 |
內容 |
loop.index |
循環迭代計數(從1開始) |
loop.index0 |
循環迭代計數(從0開始) |
loop.revindex |
循環迭代倒序計數(從len開始,到1結束) |
loop.revindex0 |
循環迭代倒序計數(從len-1開始,到0結束) |
loop.first |
是否爲循環的第一個元素 |
loop.last |
是否爲循環的最後一個元素 |
loop.length |
循環序列中元素的個數 |
loop.cycle |
在給定的序列中輪循,如上例在」odd」和」even」兩個值間輪循 |
loop.depth |
當前循環在遞歸中的層級(從1開始) |
loop.depth0 |
當前循環在遞歸中的層級(從0開始) |
關於遞歸循環,你們看看下面的例子,我就很少介紹了:
1 2 3 4 5 6 7 8 |
{% for item in [[1,2],[3,4,5]] recursive %} Depth: {{ loop.depth }} {% if item[0] %} {{ loop(item) }} {% else %} Number: {{ item }} ; {% endif %} {% endfor %} |
另外,若是你啓用了」jinja2.ext.loopcontrols」擴展的話,你還能夠在循環中使用」{% break %}」和」{% continue %}」來控制循環執行。關於Jinja2的擴展,咱們會在本系列的第七篇和第八篇中介紹。
有時候,咱們在頁面上就是要顯示」{{ }}」這樣的符號怎麼辦?Jinja2提供了」raw」語句來忽略全部模板語法。
1 2 3 4 5 6 7 |
{% raw %} <ul> {% for item in items %} <li>{{ item }}</li> {% endfor %} </ul> {% endraw %} |
咱們將本文一開始的Flask代碼」hello()」方法改動下:
1 2 3 4 5 6 |
@app.route('/hello') @app.route('/hello/<name>') def hello(name=None): if name is None: name = '<em>World</em>' return render_template('hello.html', name=name) |
此時,訪問」http://localhost:5000/hello」,頁面上會顯示」Welcome <em>World</em>!」,也就是這個HTML標籤」<em>」被自動轉義了。正如咱們曾經提到過的,Flask會對」.html」, 「.htm」, 「.xml」, 「.xhtml」這四種類型的模板文件開啓HTML格式自動轉義。這樣也能夠防止HTML語法注入。若是咱們不想被轉義怎麼辦?
1 2 3 |
{% autoescape false %} <h1>Hello {{ name }}!</h1> {% endautoescape %} |
將」autoescape」開關設爲」false」便可,反之,設爲」true」即開啓自動轉義。使用」autoescape」開關前要啓用」jinja2.ext.autoescape」擴展,在Flask框架中,這個擴展默認已啓用。
使用」set」關鍵字給變量賦值:
1 |
{% set items = [[1,2],[3,4,5]] %} |
用法能夠參考下面的with語句
相似於Python中的」with」關鍵字,它能夠限制with語句塊內對象的做用域:
1 2 3 4 5 |
{% with foo = 1 %} {% set bar = 2 %} {{ foo + bar }} {% endwith %} {# foo and bar are not visible here #} |
使用」with」關鍵字前要啓用」jinja2.ext.with_」擴展,在Flask框架中,這個擴展默認已啓用。
1 2 3 4 |
{% with arr = ['Sunny'] %} {{ arr.append('Rainy') }} {{ arr }} {% endwith %} |
看上面這段代碼,咱們想執行列表的」append」操做,這時使用」{{ arr.append(‘Rainy’) }}」頁面會輸出」None」,換成」{% %}」來執行,程序會報錯,由於這是個表達式,不是語句。那怎麼辦?咱們能夠啓用」jinja2.ext.do」擴展。而後在模板中執行」do」語句便可:
1 2 3 4 |
{% with arr = ['Sunny'] %} {% do arr.append('Rainy') %} {{ arr }} {% endwith %} 默認jinja2沒開啓這個,須要啓用 在py文件中添加這個:app.jinja_env.add_extension("jinja2.ext.do"),表示啓用jinja2的do擴展,而後就能在html文件中使用上述語句了 |
本篇中的示例代碼能夠在這裏下載。
Flask每一個請求都有生命週期,在生命週期內請求有其上下文環境Request Context。咱們在Flask進階系列第一篇中有詳細介紹。做爲在請求中渲染的模板,天然也在請求的生命週期內,因此Flask應用中的模板可使用到請求上下文中的環境變量,及一些輔助函數。本文就會介紹下這些變量和函數。
request對象能夠用來獲取請求的方法」request.method」,表單」request.form」,請求的參數」request.args」,請求地址」request.url」等。它自己是一個字典。在模板中,你同樣能夠獲取這些內容,只要用表達式符號」{{ }}」括起來便可。
1 |
<p>{{ request.url }}</p> |
在沒有請求上下文的環境中,這個對象不可用。
session對象能夠用來獲取當前會話中保存的狀態,它自己是一個字典。在模板中,你能夠用表達式符號」{{ }}」來獲取這個對象。
Flask代碼以下,別忘了設置會話密鑰哦:
注:須要導入from flask import Flask,render_template,session
1 2 3 4 5 6 |
@app.route('/') def index(): session['user'] = 'guest' return render_template('hello.html')
app.secret_key = '123456' |
模板代碼:
1 |
<p>User: {{ session.user }}</p> |
在沒有請求上下文的環境中,這個對象不可用。
全局變量g,用來保存請求中會用到全局內容,好比數據庫鏈接。模板中也能夠訪問。
Flask代碼:
注:須要導入from flask import Flask,render_template,g
1 2 3 4 |
@app.route('/') def index(): g.db = 'mysql' return render_template('hello.html') |
模板代碼:
1 |
<p>DB: {{ g.db }}</p> |
g對象是保存在應用上下文環境中的,也只在一個請求生命週期內有效。在沒有應用上下文的環境中,這個對象不可用。
在Flask入門系列第六篇中,咱們曾介紹過如何將配置信息導入Flask應用中。導入的配置信息,就保存在」app.config」對象中。這個配置對象在模板中也能夠訪問。
1 |
<p>Host: {{ config.DEBUG }}</p> |
結果返回:Host:True,表示的是開啓了調試模式
「config」是全局對象,離開了請求生命週期也能夠訪問。
url_for()函數能夠用來快速獲取及構建URL,Flask也將此函數引入到了模板中,好比下面的代碼,就能夠獲取靜態目錄下的」style.css」文件。
1 |
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}"> |
該函數是全局的,離開了請求生命週期也能夠調用。
get_flashed_messages()函數是用來獲取消息閃現的。具體的示例咱們在入門系列第五篇中已經講過,這裏就再也不贅述了。這也是一個全局可以使用的函數。
除了Flask提供的標準上下文變量和函數,咱們還能夠本身定義。下面咱們就來先定義一個上下文變量,在Flask應用代碼中,加入下面的函數:
1 2 3 4 5 |
from flask import current_app
@app.context_processor def appinfo(): return dict(appname=current_app.name) |
函數返回的是一個字典,裏面有一個屬性」appname」,值爲當前應用的名稱。咱們曾經介紹過,這裏的」current_app」對象是一個定義在應用上下文中的代理。函數用」@app.context_processor」裝飾器修飾,它是一個上下文處理器,它的做用是在模板被渲染前運行其所修飾的函數,並將函數返回的字典導入到模板上下文環境中,與模板上下文合併。而後,在模板中」appname」就如同上節介紹的」request」, 「session」同樣,成爲了可訪問的上下文對象。咱們能夠在模板中將其輸出:
1 |
<p>Current App is: {{ appname }}</p> |
同理咱們能夠自定義上下文函數,只需將上例中返回字典的屬性指向一個函數便可,下面咱們就來定義一個上下文函數來獲取系統當前時間:
1 2 3 4 5 6 7 |
import time
@app.context_processor def get_current_time(): def get_time(timeFormat="%b %d, %Y - %H:%M:%S"): return time.strftime(timeFormat) return dict(current_time=get_time) |
咱們能夠試下在模板中將其輸出:
1 2 |
<p>Current Time is: {{ current_time() }}</p> <p>Current Day is: {{ current_time("%Y-%m-%d") }}</p> |
上下文處理器能夠修飾多個函數,也就是咱們能夠定義多個上下文環境變量和函數。
本篇中的示例代碼能夠在這裏下載。
我所瞭解的模板引擎大部分都會提供相似Jinja2過濾器的功能,只不過叫法不一樣罷了。好比PHP Smarty中的Modifiers(變量調節器或修飾器),FreeMarker中的Build-ins(內建函數),連AngularJS這樣的前端框架也提供了Filter過濾器。它們都是用來在變量被顯示或使用前,對其做轉換處理的。能夠把它認爲是一種轉換函數,輸入的參數就是其所修飾的變量,返回的就是變量轉換後的值。
回到咱們第一篇開篇的例子,咱們在模板中對變量name做以下處理:
1 |
<h1>Hello {{ name | upper }}!</h1> |
你會看到name的輸出都變成大寫了。這就是過濾器,只需在待過濾的變量後面加上」|」符號,再加上過濾器名稱,就能夠對該變量做過濾轉換。上面例子就是轉換成全大寫字母。過濾器能夠連續使用:
1 |
<h1>Hello {{ name | upper | truncate(3, True) }}!</h1> |
如今name變量不但被轉換爲大寫,並且當它的長度大於3後,只顯示前3個字符,後面默認用」…」顯示。過濾器」truncate」有3個參數,第一個是字符截取長度;第二個決定是否保留截取後的子串,默認是False,也就是當字符大於3後,只顯示」…」,截取部分也不出現;第三個是省略符號,默認是」…」。
其實從例子中咱們能夠猜到,過濾器本質上就是一個轉換函數,它的第一個參數就是待過濾的變量,在模板中使用時能夠省略去。若是它有第二個參數,模板中就必須傳進去。
Jinja2模板引擎提供了豐富的內置過濾器。這裏介紹幾個經常使用的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
{# 當變量未定義時,顯示默認字符串,能夠縮寫爲d #} <p>{{ name | default('No name', true) }}</p>
{# 單詞首字母大寫 #} <p>{{ 'hello' | capitalize }}</p>
{# 單詞全小寫 #} <p>{{ 'XML' | lower }}</p>
{# 去除字符串先後的空白字符 #} <p>{{ ' hello ' | trim }}</p>
{# 字符串反轉,返回"olleh" #} <p>{{ 'hello' | reverse }}</p>
{# 格式化輸出,返回"Number is 2" #} <p>{{ '%s is %d' | format("Number", 2) }}</p>
{# 關閉HTML自動轉義 #} <p>{{ '<em>name</em>' | safe }}</p>
{% autoescape false %} {# HTML轉義,即便autoescape關了也轉義,能夠縮寫爲e #} <p>{{ '<em>name</em>' | escape }}</p> {% endautoescape %} |
1 2 3 4 5 6 7 8 |
{# 四捨五入取整,返回13.0 #} <p>{{ 12.8888 | round }}</p>
{# 四捨五入向下截取到小數點後2位,返回12.89 #} <p>{{ 12.8888 | round(2) }}</p>
{# 向下截取到小數點後2位,返回12.88 #} <p>{{ 12.8888 | round(2, 'floor') }}</p>
{# 絕對值,返回12 #} <p>{{ -12 | abs }}</p> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
{# 取第一個元素 #} <p>{{ [1,2,3,4,5] | first }}</p>
{# 取最後一個元素 #} <p>{{ [1,2,3,4,5] | last }}</p>
{# 返回列表長度,能夠寫爲count #} <p>{{ [1,2,3,4,5] | length }}</p>
{# 列表求和 #} <p>{{ [1,2,3,4,5] | sum }}</p>
{# 列表排序,默認爲升序 #} <p>{{ [3,2,1,5,4] | sort }}</p>
{# 合併爲字符串,返回"1 | 2 | 3 | 4 | 5" #} <p>{{ [1,2,3,4,5] | join(' | ') }}</p>
{# 列表中全部元素都全大寫。這裏能夠用upper,lower,但capitalize無效 #} <p>{{ ['tom','bob','ada'] | upper }}</p> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
{% set users=[{'name':'Tom','gender':'M','age':20}, {'name':'John','gender':'M','age':18}, {'name':'Mary','gender':'F','age':24}, {'name':'Bob','gender':'M','age':31}, {'name':'Lisa','gender':'F','age':19}] %}
{# 按指定字段排序,這裏設reverse爲true使其按降序排 #} <ul> {% for user in users | sort(attribute='age', reverse=true) %} <li>{{ user.name }}, {{ user.age }}</li> {% endfor %} </ul>
{# 列表分組,每組是一個子列表,組名就是分組項的值 #} <ul> {% for group in users|groupby('gender') %} <li>{{ group.grouper }}<ul> {% for user in group.list %} <li>{{ user.name }}</li> {% endfor %}</ul></li> {% endfor %} </ul>
{# 取字典中的某一項組成列表,再將其鏈接起來 #} <p>{{ users | map(attribute='name') | join(', ') }}</p> |
更全的內置過濾器介紹能夠從Jinja2的官方文檔中找到。
Flask提供了一個內置過濾器」tojson」,它的做用是將變量輸出爲JSON字符串。這個在配合Javascript使用時很是有用。咱們延用上節字典列表操做中定義的」users」變量
1 2 3 4 |
<script type="text/javascript"> var users = {{ users | tojson | safe }}; console.log(users[0].name); </script> |
注意,這裏要避免HTML自動轉義,因此加上safe過濾器。
注:暫不知道具體用法
Jinja2還能夠對整塊的語句使用過濾器。
1 2 3 |
{% filter upper %} This is a Flask Jinja2 introduction. {% endfilter %} |
不過上述這種場景不常常用到。
內置的過濾器不知足需求怎麼辦?本身寫唄。過濾器說白了就是一個函數嘛,咱們立刻就來寫一個。回到Flask應用代碼中:
注:這個頗有用
1 2 |
def double_step_filter(l): return l[::2] |
咱們定義了一個」double_step_filter」函數,返回輸入列表的偶數位元素(第0位,第2位,..)。怎麼把它加到模板中當過濾器用呢?Flask應用對象提供了」add_template_filter」方法來幫咱們實現。咱們加入下面的代碼:
1 |
app.add_template_filter(double_step_filter, 'double_step') |
函數的第一個參數是過濾器函數,第二個參數是過濾器名稱。而後,咱們就能夠愉快地在模板中使用這個叫」double_step」的過濾器了:
1 2 |
{# 返回[1,3,5] #} <p>{{ [1,2,3,4,5] | double_step }}</p> |
Flask還提供了添加過濾器的裝飾器」template_filter」,使用起來更簡單。下面的代碼就添加了一個取子列表的過濾器。裝飾器的參數定義了該過濾器的名稱」sub」。
1 2 3 |
@app.template_filter('sub') def sub(l, start, end): return l[start:end] |
咱們在模板中能夠這樣使用它:
1 2 |
{# 返回[2,3,4] #} <p>{{ [1,2,3,4,5] | sub(1,4) }}</p> |
Flask添加過濾器的方法其實是封裝了對Jinja2環境變量的操做。上述添加」sub」過濾器的方法,等同於下面的代碼。
1 |
app.jinja_env.filters['sub'] = sub |
咱們在Flask應用中,不建議直接訪問Jinja2的環境變量。若是離開Flask環境直接使用Jinja2的話,就能夠經過」jinja2.Environment」來獲取環境變量,並添加過濾器。
本篇中的示例代碼能夠在這裏下載。
Jinja2中的測試器Test和過濾器很是類似,區別是測試器老是返回一個布爾值,它能夠用來測試一個變量或者表達式,你須要使用」is」關鍵字來進行測試。測試器通常都是跟着if控制語句一塊兒使用的。下面咱們就來深刻了解下這個測試器。
再次取回第一篇開篇的例子,咱們在模板中對變量name做以下判斷:
1 2 3 |
{% if name is lower %} <h2>"{{ name }}" are all lower case.</h2> {% endif %} |
當name變量中的字母都是小寫時,這段文字就會顯示。這就是測試器,在if語句中,變量或表達式的後面加上is關鍵字,再加上測試器名稱,就能夠對該變量或表達式做測試,並根據其測試結果的真或假,來決定是否進入if語句塊。測試器也能夠有參數,用括號括起。當其只有一個參數時,能夠省去括號。
1 2 3 |
{% if 6 is divisibleby 3 %} <h2>"divisibleby" test pass</h2> {% endif %} |
上例中,測試器」divisibleby」能夠判斷其所接收的變量是否能夠被其參數整除。由於它只有一個參數,咱們就能夠用空格來分隔測試器和其參數。上面的調用同」divisibleby(3)」效果一致。測試器也能夠配合not關鍵字一塊兒使用:
1 2 3 |
{% if 6 is not divisibleby(4) %} <h2>"not divisibleby" test pass</h2> {% endif %} |
顯然測試器本質上也是一個函數,它的第一個參數就是待測試的變量,在模板中使用時能夠省略去。若是它有第二個參數,模板中就必須傳進去。測試器函數返回的必須是一個布爾值,這樣才能夠用來給if語句做判斷。
同過濾器同樣,Jinja2模板引擎提供了豐富的內置測試器。這裏介紹幾個經常使用的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
{# 檢查變量是否被定義,也能夠用undefined檢查是否未被定義 #} {% if name is defined %} <p>Name is: {{ name }}</p> {% endif %}
{# 檢查是否全部字符都是大寫 #} {% if name is upper %} <h2>"{{ name }}" are all upper case.</h2> {% endif %}
{# 檢查變量是否爲空 #} {% if name is none %} <h2>Variable is none.</h2> {% endif %}
{# 檢查變量是否爲字符串,也能夠用number檢查是否爲數值 #} {% if name is string %} <h2>{{ name }} is a string.</h2> {% endif %}
{# 檢查數值是不是偶數,也能夠用odd檢查是否爲奇數 #} {% if 2 is even %} <h2>Variable is an even number.</h2> {% endif %}
{# 檢查變量是否可被迭代循環,也能夠用sequence檢查是不是序列 #} {% if [1,2,3] is iterable %} <h2>Variable is iterable.</h2> {% endif %}
{# 檢查變量是不是字典 #} {% if {'name':'test'} is mapping %} <h2>Variable is dict.</h2> {% endif %} |
更全的內置測試器介紹能夠從Jinja2的官方文檔中找到。
若是內置測試器不知足需求,咱們就來本身寫一個。寫法很相似於過濾器,先在Flask應用代碼中定義測試器函數,而後經過」add_template_test」將其添加爲模板測試器:
1 2 3 4 |
import re def has_number(str): return re.match(r'.*\d+', str) app.add_template_test(has_number,'contain_number') |
咱們定義了一個」has_number」函數,用正則來判斷輸入參數是否包含數字。而後調用」app.add_template_test」方法,第一個參數是測試器函數,第二個是測試器名稱。以後,咱們就能夠在模板中使用」contain_number」測試器了:
1 2 3 |
{% if name is contain_number %} <h2>"{{ name }}" contains number.</h2> {% endif %} |
同過濾器同樣,Flask提供了添加測試器的裝飾器」template_test」。下面的代碼就添加了一個判斷字符串是否以某一子串結尾的測試器。裝飾器的參數定義了該測試器的名稱」end_with」:
1 2 3 |
@app.template_test('end_with') def end_with(str, suffix): return str.lower().endswith(suffix.lower()) |
咱們在模板中能夠這樣使用它:
1 2 3 |
{% if name is end_with "me" %} <h2>"{{ name }}" ends with "me".</h2> {% endif %} |
Flask添加測試器的方法是封裝了對Jinja2環境變量的操做。上述添加」end_with」測試器的方法,等同於下面的代碼。
1 |
app.jinja_env.tests['end_with'] = " end_with " |
咱們在Flask應用中,不建議直接訪問Jinja2的環境變量。若是離開Flask環境直接使用Jinja2的話,就能夠經過」jinja2.Environment」來獲取環境變量,並添加測試器。
本文中的示例代碼能夠在這裏下載。
介紹完了過濾器和測試器,接下來要講的是Jinja2模板引擎的另外一個輔助函數功能,即全局函數Global Functions。若是說過濾器是一個變量轉換函數,測試器是一個返回布爾值的函數,那全局函數就能夠是任意函數。能夠在任一場景使用,沒有輸入和輸出值的限制。本篇咱們就來闡述下這個全局函數。
仍是取出第一篇開篇的代碼,咱們在模板中加入下面的代碼:
1 2 3 4 5 |
<ul> {% for num in range(10, 20, 2) %} <li>Number is "{{ num }}"</li> {% endfor %} </ul> |
頁面上會顯示」10,12,14,16,18″5個列表項。全局函數」range()」的做用同Python裏的同樣,返回指定範圍內的數值序列。三個參數分別是開始值,結束值(不包含),間隔。若是隻傳兩個參數,那間隔默認爲1;若是隻傳1個參數,那開始值默認爲0。
因而可知,全局函數如同其名字同樣,就是全局範圍內能夠被使用的函數。其同第二篇介紹的上下文環境中定義的函數不一樣,沒有請求生命週期的限制。
演示幾個經常使用的內置全局函數。
1 2 3 4 |
{% set user = dict(name='Mike',age=15) %} <p>{{ user | tojson | safe }}</p>
{# 顯示 '{"age": 15, "name": "Mike"}' #} |
1 2 3 4 5 6 |
{% set sep = joiner("|") %} {% for val in range(5) %} {{ sep() }} <span>{{ val }}</span> {% endfor %}
{# 顯示 "0 | 1 | 2 | 3 | 4" #} |
1 2 3 4 5 6 7 |
{% set cycle = cycler('odd', 'even') %} <ul> {% for num in range(10, 20, 2) %} <li class="{{ cycle.next() }}">Number is "{{ num }}", next line is "{{ cycle.current }}" line.</li> {% endfor %} </ul> |
基於上一節的例子,加上」cycler()」函數的使用,你會發現列表項<li>的」class」在」odd」和」even」兩個值間輪循。加入第一篇中的CSS style,就能夠看到斑馬線了。
「cycler()」函數返回的對象能夠作以下操做
更全的內置全局函數介紹能夠從Jinja2的官方文檔中找到。
咱們固然也能夠寫本身的全局函數,方法同以前介紹的過濾器啦,測試器啦都很相似。就是將Flask應用代碼中定義的函數,經過」add_template_global」將其傳入模板便可:
1 2 3 4 5 6 7 8 9 |
import re def accept_pattern(pattern_str): pattern = re.compile(pattern_str, re.S) def search(content): return pattern.findall(content)
return dict(search=search, current_pattern=pattern_str)
app.add_template_global(accept_pattern, 'accept_pattern') |
上例中的accept_pattern函數會先預編譯一個正則,而後返回的字典中包含一個查詢函數」search」,以後調用」search」函數就能夠用編譯好的正則來搜索內容了。」app.add_template_global」方法的第一個參數是自定義的全局函數,第二個是全局函數名稱。如今,讓咱們在模板中使用」accept_pattern」全局函數:
1 2 3 4 5 6 7 8 9 |
{% with pattern = accept_pattern("<li>(.*?)</li>") %} {% set founds = pattern.search("<li>Tom</li><li>Bob</li>") %} <ul> {% for item in founds %} <li>Found: {{ item }}</li> {% endfor %} </ul> <p>Current Pattern: {{ pattern.current_pattern }}</p> {% endwith %} |
「Tom」和」Bob」被抽取出來了,很牛掰的樣子。你還能夠根據須要在」accept_pattern」的返回字典裏定義更多的方法。
Flask一樣提供了添加全局函數的裝飾器」template_global」,以方便全局函數的添加。咱們來用它將第二篇中取系統當前時間的函數」current_time」定義爲全局函數。
1 2 3 4 |
import time @app.template_global('end_with') def current_time(timeFormat="%b %d, %Y - %H:%M:%S"): return time.strftime(timeFormat) |
同第二篇中的同樣,咱們在模板中能夠這樣使用它:
1 2 |
<p>Current Time is: {{ current_time() }}</p> <p>Current Day is: {{ current_time("%Y-%m-%d") }}</p> |
Flask添加全局函數的方法是封裝了對Jinja2環境變量的操做。上述添加」current_time」全局函數的方法,等同於下面的代碼。
1 |
app.jinja_env.globals['current_time'] = current_time |
咱們在Flask應用中,不建議直接訪問Jinja2的環境變量。若是離開Flask環境直接使用Jinja2的話,就能夠經過」jinja2.Environment」來獲取環境變量,並添加全局函數。
本文中的示例代碼能夠在這裏下載。
考慮到模板代碼的重用,Jinja2提供了塊 (Block)和宏 (Macro)的功能。塊功能有些相似於C語言中的宏,原理就是代碼替換;而宏的功能有些相似於函數,能夠傳入參數。本篇咱們就來介紹下塊和宏的用法。
在Flask入門系列第三篇介紹模板時,咱們提到了模板的繼承。咱們在子模板的開頭定義了」{% extend ‘parent.html’ %}」語句來聲明繼承,此後在子模板中由」{% block block_name %}」和」{% endblock %}」所包括的語句塊,將會替換父模板中一樣由」{% block block_name %}」和」{% endblock %}」所包括的部分。
這就是塊的功能,模板語句的替換。這裏要注意幾個點:
另外,咱們建議在」endblock」關鍵字後也加上塊名,好比」{% endblock block_name %}」。雖然對程序沒什麼做用,可是當有多個塊嵌套時,可讀性好不少。
若是父模板中的塊裏有內容不想被子模板替換怎麼辦?咱們可使用」super( )」方法。基於Flask入門系列第三篇的例子,咱們將父模板」layout.html」改成:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<!doctype html> <head> {% block head %} <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}"> <title>{% block title %}{% endblock %}</title> {% endblock %} </head> <body> <div class="page"> {% block body %} {% endblock %} </div> </body> |
並在子模板裏,加上」head」塊和」title」塊:
1 2 3 4 5 6 7 |
{% block title %}Block Sample{% endblock %} {% block head %} {{ super() }} <style type="text/css"> h1 { color: #336699; } </style> {% endblock %} |
父模板同子模板的」head」塊中都有內容。運行後,你能夠看到,父模板中的」head」塊語句先被加載,然後是子模板中的」head」塊語句。這就得益於咱們在子模板的」head」塊中加上了表達式」{{ super( ) }}」。效果有點像Java中的」super( )」吧。
默認狀況下,塊內語句是沒法訪問塊外做用域中的變量。好比咱們在」layout.html」加上一個循環:
1 2 3 |
{% for item in range(5) %} <li>{% block list %}{% endblock %}</li> {% endfor %} |
而後在子模板中定義」list」塊並訪問循環中的」item」變量:
1 2 3 |
{% block list %} <em>{{ item }}</em> {% endblock %} |
你會發現頁面上什麼數字也沒顯示。若是你想在塊內訪問這個塊外的變量,你就須要在塊聲明時添加」scoped」關鍵字。好比咱們在」layout.html」中這樣聲明」list」塊便可:
1 2 3 |
{% for item in range(5) %} <li>{% block list scoped %}{% endblock %}</li> {% endfor %} |
文章的開頭咱們就講過,Jinja2的宏功能有些相似於傳統程序語言中的函數,既然是函數就有其聲明和調用兩個部分。那就讓咱們先聲明一個宏:
1 2 3 |
{% macro input(name, type='text', value='') -%} <input type="{{ type }}" name="{{ name }}" value="{{ value|e }}"> {%- endmacro %} |
代碼中,宏的名稱就是」input」,它有三個參數分別是」name」, 「type」和」value」,後兩個參數有默認值。如今,讓咱們使用表達式來調用這個宏:
1 2 3 |
<p>{{ input('username', value='user') }}</p> <p>{{ input('password', 'password') }}</p> <p>{{ input('submit', 'submit', 'Submit') }}</p> |
你們能夠在頁面上看到一個文本輸入框,一個密碼輸入框及一個提交按鈕。是否是同函數同樣啊?其實它還有比函數更豐富的功能,以後咱們來介紹。
咱們先來建立個宏」list_users」:
1 2 3 4 5 6 7 8 |
{% macro list_users(users) -%} <table> <tr><th>Name</th><th>Action</th></tr> {%- for user in users %} <tr><td>{{ user.name |e }}</td>{{ caller() }}</tr> {%- endfor %} </table> {%- endmacro %} |
宏的做用就是將用戶列表顯示在表格裏,表格每一行用戶名稱後面調用了」{{ caller( ) }}」方法,這個有什麼用呢?先別急,咱們來寫調用者的代碼:
1 2 3 4 5 6 7 8 |
{% set users=[{'name':'Tom','gender':'M','age':20}, {'name':'John','gender':'M','age':18}, {'name':'Mary','gender':'F','age':24}] %}
{% call list_users(users) %} <td><input name="delete" type="button" value="Delete"></td> {% endcall %} |
與上例不一樣,這裏咱們使用了」{% call %}」語句塊來調用宏,語句塊中包括了一段生成」Delete」按鈕的代碼。運行下試試,你會發現每一個用戶名後面都出現了」Delete」按鈕,也就是」{{ caller( ) }}」部分被調用者」{% call %}」語句塊內部的內容替代了。不明覺厲吧!其實吧,這個跟函數傳個參數進去沒啥大區別,我的以爲,主要是有些時候HTML語句太複雜(如上例),不方便寫在調用參數上,因此就寫在」{% call %}」語句塊裏了。
Jinja2的宏不但能訪問調用者語句塊的內容,還能給調用者傳遞參數。嚯,這又是個什麼鬼?咱們來擴展下上面的例子。首先,咱們將表格增長一列性別,並在宏裏調用」caller()」方法時,傳入一個變量」user.gender」:
1 2 3 4 5 6 7 8 |
{% macro list_users(users) -%} <table> <tr><th>Name</th><th>Gender</th><th>Action</th></tr> {%- for user in users %} <tr><td>{{ user.name |e }}</td>{{ caller(user.gender) }}</tr> {%- endfor %} </table> {%- endmacro %} |
而後,咱們修改下調用者語句塊:
1 2 3 4 5 6 7 8 9 10 |
{% call(gender) list_users(users) %} <td> {% if gender == 'M' %} <img src="{{ url_for('static', filename='img/male.png') }}" width="20px"> {% else %} <img src="{{ url_for('static', filename='img/female.png') }}" width="20px"> {% endif %} </td> <td><input name="delete" type="button" value="Delete"></td> {% endcall %} |
你們注意到,咱們在使用」{% call %}」語句時,將其改成了」{% call(gender) … %}」,這個括號中的」gender」就是用來接受宏裏傳來的」user.gender」變量。所以咱們就能夠在」{% call %}」語句中使用這個」gender」變量來判斷用戶性別。這樣宏就成功地向調用者傳遞了參數。
上例中,咱們看到宏的內部可使用」caller( )」方法獲取調用者的內容。此外宏還提供了兩個內部變量:
這是一個列表。若是調用宏時傳入的參數多於宏聲明時的參數,多出來的沒指定參數名的參數就會保存在這個列表中。
這是一個字典。若是調用宏時傳入的參數多於宏聲明時的參數,多出來的指定了參數名的參數就會保存在這個字典中。
讓咱們回到第一個例子input宏,在調用時增長其傳入的參數,並在宏內將上述兩個變量打印出來:
1 2 3 4 5 6 |
{% macro input(name, type='text', value='') -%} <input type="{{ type }}" name="{{ name }}" value="{{ value|e }}"> <br /> {{ varargs }} <br /> {{ kwargs }} {%- endmacro %} <p>{{ input('submit', 'submit', 'Submit', 'more arg1', 'more arg2', ext='more arg3') }}</p> |
能夠看到,varargs變量存了參數列表」[‘more arg1’, ‘more arg2’]」,而kwargs字典存了參數」{‘ext’:’more arg3′}」。
一個宏能夠被不一樣的模板使用,因此咱們建議將其聲明在一個單獨的模板文件中。須要使用時導入進來便可,而導入的方法也很是相似於Python中的」import」。讓咱們將第一個例子中」input」宏的聲明放到一個」form.html」模板文件中,而後將調用的代碼改成:
1 2 3 4 |
{% import 'form.html' as form %} <p>{{ form.input('username', value='user') }}</p> <p>{{ form.input('password', 'password') }}</p> <p>{{ form.input('submit', 'submit', 'Submit') }}</p> |
運行下,效果是否是同以前的同樣?你也能夠採用下面的方式導入:
1 2 3 4 |
{% from 'form.html' import input %} <p>{{ input('username', value='user') }}</p> <p>{{ input('password', 'password') }}</p> <p>{{ input('submit', 'submit', 'Submit') }}</p> |
這裏咱們再介紹一個Jinja2模板中代碼重用的功能,就是包含 (Include),使用的方法就是」{% include %}」語句。其功能就是將另外一個模板加載到當前模板中,並直接渲染在當前位置上。它同導入」import」不同,」import」以後你還須要調用宏來渲染你的內容,」include」是直接將目標模板渲染出來。它同block塊繼承也不同,它一次渲染整個模板文件內容,不分塊。
咱們能夠建立一個」footer.html」模板,並在」layout.html」中包含這個模板:
1 2 3 4 |
<body> ... {% include 'footer.html' %} </body> |
當」include」的模板文件不存在時,程序會拋出異常。你能夠加上」ignore missing」關鍵字,這樣若是模板不存在,就會忽略這段」{% include %}」語句。
1 |
{% include 'footer.html' ignore missing %} |
「{% include %}」語句還能夠跟一個模板列表:
1 |
{% include ['footer.html','bottom.html','end.html'] ignore missing %} |
上例中,程序會按順序尋找模板文件,第一個被找到的模板即被加載,而其後的模板都會被忽略。若是都沒找到,那整個語句都會被忽略。
本篇中的示例代碼能夠在這裏下載。
一個強大的工具通常都支持擴展或插件的開發功能,來容許第三方經過開發新擴展或插件,擴充工具自己功能,並能夠貢獻給社區。Jinja2也不例外,Jinja2自己提供了一部分擴展,你能夠在程序中啓用。同時,你還能夠建立本身的擴展,來擴充模板引擎功能。本篇會先介紹Jinja2自帶的擴展」jinja2.ext.i18n」的使用,自定義擴展的開發會放在下一篇闡述。
任什麼時候候使用Jinja2時,都須要先建立Jinja2環境,因此啓用擴展的方法就是在建立環境時指定:
1 2 |
from jinja2 import Environment jinja_env = Environment(extensions=['jinja2.ext.i18n','jinja2.ext.do']) |
可是你在使用Flask時,其已經有了一個Jinja2環境,你不能再建立一個,因此你須要想辦法添加擴展。Flask對於擴展不像過濾器或測試器那樣封裝了添加方法和裝飾器,這樣你就只能直接訪問Flask中的Jinja2環境變量來添加。
1 2 3 4 |
from flask import Flask app = Flask(__name__) app.jinja_env.add_extension('jinja2.ext.i18n') app.jinja_env.add_extension('jinja2.ext.do') |
注:Flask默認已加載了」jinja2.ext.autoescape」和」jinja2.ext.with_」擴展。
在本系列第一篇中,咱們已經介紹了四個Jinja2內置擴展的使用:」jinja2.ext.autoescape」, 「jinja2.ext.with_」, 「jinja2.ext.do」和」jinja2.ext.loopcontrols」。除了這幾個之外,Jinja2還有一個很是重要的擴展,就是提供本地化功能的」jinja2.ext.i18n」。它能夠與」gettext」或」babel」聯合使用,接下來咱們採用」gettext」來介紹怎麼使用這個本地化擴展。
注:本地化這個其實就是翻譯頁面語言,這個詳看Flask-Babel
建議你們先去了解下Python gettext相關知識,篇幅關係本文就不許備細講。這裏咱們使用Python源代碼(記住不是安裝包)中」Tools/i18n」目錄下的工具來建立翻譯文件。
$ python pygettext.py
上述命令會在當前目錄下生成一個名爲」message.pot」的翻譯文件模板,內容以下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR ORGANIZATION # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. # msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "POT-Creation-Date: 2016-02-22 21:45+CST\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=CHARSET\n" "Content-Transfer-Encoding: ENCODING\n" "Generated-By: pygettext.py 1.5\n" |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
# Jinja2 i18n Extention Sample # Copyright (C) 2016 bjhee.com # Billy J. Hee <billy@bjhee.com>, 2016. # msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "POT-Creation-Date: 2016-02-22 21:45+CST\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: UTF-8\n" "Generated-By: pygettext.py 1.5\n" |
修改完後,將其另存爲翻譯文件」lang.po」。
1 2 |
msgid "Hello World!" msgstr "世界,你好!" |
將其加在文件末尾。這裏」msgid」指定了待翻譯的文字,而」msgstr」就是翻譯後的文字。
咱們依然使用」Tools/i18n」目錄提供的工具,」msgfmt.py」:
$ python msgfmt.py lang.po
執行完後,當前目錄生成了」lang.mo」文件。注意,只有這個」*.mo」文件才能被應用程序識別。另外,推薦一個工具Poedit,很強的圖形化po編輯工具,也能夠用來生成mo文件,很是好用,Mac和Windows下都能用。
咱們在當前Flask工程下建立子目錄」locale/zh_CN/LC_MESSAGES/」,並將剛纔生成的」lang.po」和」lang.mo」文件放到這個目錄下。這裏」locale」子目錄的名字能夠更改,其餘的我的建議不要改。
讓咱們在Flask應用代碼中啓用」jinja2.ext.i18n」,並加載剛建立的翻譯文件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#coding:utf8 import gettext from flask import Flask,render_template app = Flask(__name__) # 加載擴展 app.jinja_env.add_extension('jinja2.ext.i18n')
# 'lang'表示翻譯文件名爲"lang.mo",'locale'表示全部翻譯文件都在"locale"子目錄下, # 'zh_CN'表示二級子目錄,含義上講就是加載中文翻譯。因此下面的代碼會加載文件: # "locale/zh_CN/LC_MESSAGES/lang.mo" gettext.install('lang', 'locale', unicode=True) translations = gettext.translation('lang', 'locale', languages=['zh_CN']) translations.install(True) app.jinja_env.install_gettext_translations(translations) |
這個」install_gettext_translations()」方法就是Jinja2提供來加載」gettext」翻譯文件對象的。加載完後,你就能夠在模板中使用本地化功能了。方法有兩種:」{% trans %}」語句或」gettext()」方法。咱們先來試下」{% trans %}」語句,在模板文件中,咱們加上:
1 |
<h1>{% trans %}Hello World!{% endtrans %}</h1> |
運行下,有沒有看到頁面上打印了」世界,你好!」,恭喜你,成功了!使用」gettext()」方法以下,效果同」{% trans %}」語句同樣。
1 |
<h1>{{ gettext('Hello World!') }}</h1> |
Jinja2還提供了」_( )」方法來替代」gettext( )」,代碼看起來很簡潔,我的推薦使用這個方法。
1 |
<h1>{{ _('Hello World!') }}</h1> |
上面的例子是在程序中指定本地化語言,你也能夠在請求上下文中判斷請求頭」Accept-Language」的內容,來動態的設置本地化語言。
有時候,待翻譯的文字內有一個變量必須在運行時才能肯定,怎麼辦?我能夠在翻譯文字上加參數。首先,你要在po文件中定義帶參數的翻譯文字,並生成mo文件:
1 2 |
msgid "Hello %(user)s!" msgstr "%(user)s,你好!" |
而後,你就能夠在模板中,使用」{% trans %}」語句或」gettext()」方法來顯示它:
1 2 |
<h1>{% trans user=name %}Hello {{ user }}!{% endtrans %}</h1> <h1>{{ _('Hello %(user)s!')|format(user=name) }}</h1> |
上例中,咱們把模板中的變量」name」賦給了翻譯文字中的變量」user」。翻譯文字上能夠有多個變量。
Jinja2從2.5版本開始,支持新的gettext樣式,使得帶參數的本地化更簡潔,上面的例子在新樣式中能夠寫成:
1 |
<h1>{{ _('Hello %(user)s!', user=name) }}</h1> |
不過使用新樣式前,你必須先啓用它。還記得咱們介紹過Jinja2加載翻譯文件的方法嗎?對,就是」install_gettext_translations()」。調用它時,加上」newstyle=True」參數便可。
1 |
app.jinja_env.install_gettext_translations(translations, newstyle=True) |
英文有個特色就是名詞有單/複數形式,通常複數都是單數後面加s,而中文就不區分了,哎,老外就是麻煩。所謂外國人創造的Python gettext,天然也對單/複數提供了特殊的支持。讓咱們如今po文件中,加上下面的內容,並生成mo文件:
1 2 3 4 |
msgid "%(num)d item" msgid_plural "%(num)d items" msgstr[0] "%(num)d個物品" msgstr[1] "%(num)d個物品集" |
什麼意思呢,這個」msgid_plural」就是指定了它上面」msgid」文字的複數形式。而」msgstr」的[0], [1]分別對應了單/複數形式翻譯後的內容。爲何這麼寫?你別管了,照着寫就是了。
在模板中,咱們加上下面的代碼:
1 2 3 |
{% set items = [1,2,3,4,5] %} {{ ngettext('%(num)d item', '%(num)d items', items|count) }}<br /> {{ ngettext('%(num)d item', '%(num)d items', items|first) }} |
你會很驚奇的發現,當」num」變量爲5時,頁面顯示」5個物品集」;而當」num」變量爲1時,頁面顯示」1個物品」。也就是程序自動匹配單/複數。很神奇吧!
原本準備在本篇把擴展都介紹完的,發現單寫個」i18n」後篇幅就很長了,只好把自定義擴展部分另起一篇。
本篇中的示例代碼能夠在這裏下載。
說實話,關於自定義擴展的開發,Jinja2的官方文檔寫得真心的簡單。到目前爲止網上可參考的資料也很是少,你必須得好好讀下源碼,還好依然有樂於奉獻的大牛們分享了些文章來幫助我理解怎麼開發擴展。本文我就徹底借鑑網上前人的例子,來給你們演示一個Jinja2的自定義擴展的開發方法。
Pygments是Python提供語法高亮的工具,官網是pygments.org。咱們在介紹Jinja2的自定義擴展時爲何要介紹Pygments呢?由於Jinja2的功能已經很強了,我一時半會想不出該開發哪一個有用的擴展,寫個沒意義的擴展嘛,又怕誤導了讀者。恰巧網上找到了一位叫Larry的外國友人開發了一個基於Pygments的代碼語法高亮擴展,感受很是實用。他的代碼使用了MIT License,那就我放心拿過來用了,不過仍是要註明下這位Larry纔是原創。
你須要先執行」pip install pygments」命令安裝Pygments包。代碼中用到Pygments的部分很是簡單,主要就是調用」pygments.highlight( )」方法來生成HTML文檔。Pygments強的地方是它不把樣式寫在HTML當中,這樣就給了咱們很大的靈活性。開始寫擴展前,讓咱們預先經過代碼
1 2 |
from pygments.formatters import HtmlFormatter HtmlFormatter(style='vim').get_style_defs('.highlight') |
生成樣式內容並將其保存在」static/css/style.css」文件中。這個css文件就是用來高亮語法的。
想深刻了解Pygments的朋友們,能夠先把官方文檔看一下。
咱們在Flask應用目錄下,建立一個」pygments_ext.py」文件,內容以下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
#coding:utf8 from jinja2 import nodes from jinja2.ext import Extension
from pygments import highlight from pygments.formatters import HtmlFormatter from pygments.lexers import guess_lexer, get_lexer_by_name
# 建立一個自定義擴展類,繼承jinja2.ext.Extension class PygmentsExtension(Extension): # 定義該擴展的語句關鍵字,這裏表示模板中的{% code %}語句會該擴展處理 tags = set(['code'])
def __init__(self, environment): # 初始化父類,必須這樣寫 super(PygmentsExtension, self).__init__(environment)
# 在Jinja2的環境變量中添加屬性, # 這樣在Flask中,就能夠用app.jinja_env.pygments來訪問 environment.extend( pygments=self, pygments_support=True )
# 重寫jinja2.ext.Extension類的parse函數 # 這是處理模板中{% code %}語句的主程序 def parse(self, parser): # 進入此函數時,即表示{% code %}標籤被找到了 # 下面的代碼會獲取當前{% code %}語句在模板文件中的行號 lineno = next(parser.stream).lineno
# 獲取{% code %}語句中的參數,好比咱們調用{% code 'python' %}, # 這裏就會返回一個jinja2.nodes.Const類型的對象,值爲'python' lang_type = parser.parse_expression()
# 將參數封裝爲列表 args = [] if lang_type is not None: args.append(lang_type)
# 下面的代碼能夠支持兩個參數,參數之間用逗號分隔,不過本例中用不到 # 這裏先檢查當前處理流的位置是否是個逗號,是的話就再獲取一個參數 # 不是的話,就在參數列表最後加個空值對象 # if parser.stream.skip_if('comma'): # args.append(parser.parse_expression()) # else: # args.append(nodes.Const(None))
# 解析從{% code %}標誌開始,到{% endcode %}爲止中間的全部語句 # 將解析完後的內容存在body裏,並將當前流位置移到{% endcode %}以後 body = parser.parse_statements(['name:endcode'],drop_needle=True)
# 返回一個CallBlock類型的節點,並將其以前取得的行號設置在該節點中 # 初始化CallBlock節點時,傳入咱們自定義的"_pygmentize"方法的調用, # 兩個空列表,還有剛纔解析後的語句內容body return nodes.CallBlock(self.call_method('_pygmentize', args), [], [], body).set_lineno(lineno)
# 這個自定義的內部函數,包含了本擴展的主要邏輯。 # 其實上面parse()函數內容,大部分擴展均可以重用 def _pygmentize(self, lang_type, caller): # 初始化HTML格式器 formatter = HtmlFormatter(linenos='table')
# 獲取{% code %}語句中的內容 # 這裏caller()對應了上面調用CallBlock()時傳入的body content = caller()
# 將模板語句中解析到了lang_type設置爲咱們要高亮的語言類型 # 若是這個變量不存在,則讓Pygmentize猜想可能的語言類型 lexer = None if lang_type is None: lexer = guess_lexer(content) else: lexer = get_lexer_by_name(lang_type)
# 將{% code %}語句中的內容高亮,即添加各類<span>, class等標籤屬性 return highlight(content, lexer, formatter) |
這段程序解釋起來太麻煩,我就把註釋都寫在代碼裏了。總的來講,擴展中核心部分就在」parse()」函數裏,而最關鍵的就是這個」parser」對象,它是一個」jinja2.parser.Parser」的對象。建議你們能夠參考下它的源碼。咱們使用的主要方法有:
在」parse()」函數最後,咱們建立了一個」nodes.CallBlock」的塊節點對象,並將其返回。初始化時,咱們先傳入了」_pygmentize()」方法的調用;而後兩個空列表分別對應了字段和屬性,本例中用不到,因此設空;再傳入解析後的語句塊」body」。」CallBlock」節點初始化完後,還要記得將當前行號設置進去。接下來,咱們對於語句塊的全部操做,均可以寫在」_pygmentize()」方法裏了。
「_pygmentize()」裏的內容我就很少介紹了,只須要記得聲明這個方法時,最後必定要接收一個參數caller,它是個回調函數,能夠獲取以前建立」CallBlock」節點時傳入的語句塊內容。
擴展寫完了,其實也沒幾行代碼,就是註釋多了點。如今咱們在Flask應用代碼中將其啓用:
1 2 3 4 5 |
from flask import Flask,render_template from pygments_ext import PygmentsExtension
app = Flask(__name__) app.jinja_env.add_extension(PygmentsExtension) |
而後讓咱們在模板中試一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<head> <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}"> </head> <body> <p>A sample of JS code</p> {% autoescape false %} {% code 'javascript' %} var name = 'World'; function foo() { console.log('Hello ' + name); } {% endcode %} {% endautoescape %} </body> |
運行下,頁面上這段代碼是否是有VIM的效果呀?這裏咱們引入了剛纔建立在」static/css」目錄下」style.css」樣式文件,另外千萬別忘了要將自動轉義關掉,否則你會看到一堆的HTML標籤。
另外提醒下你們,網上有文章說,對於單條語句,也就是不須要結束標誌的語句,」parse()」函數裏無需調用」nodes.CallBlock」,只需返回」return self.call_method(xxx)」便可。別相信他,看看源碼就知道,這個方法返回的是一個」nodes.Expr」表達式對象,而」parse()」必須返回一個」nodes.Stmt」語句對象。
本篇中的示例代碼能夠在這裏下載。