想要用 Python 和 Flask 開(kāi)發(fā)應(yīng)用嗎?讓我們來(lái)邊看例子邊學(xué)習(xí)。本教程中我們將會(huì)創(chuàng)建 一個(gè)微博應(yīng)用。這個(gè)應(yīng)用只支持單一用戶,只能創(chuàng)建文本條目,也沒(méi)有不能訂閱和評(píng)論, 但是已經(jīng)具備一個(gè)初學(xué)者需要掌握的功能。這個(gè)應(yīng)用只需要使用 Flask 和 SQLite , SQLite 是 Python 自帶的。
如果你想要事先下載完整的源代碼或者用于比較,請(qǐng)查看示例源代碼 。
我們把教程中的博客應(yīng)用稱為 flaskr ,當(dāng)然你也可以隨便取一個(gè)沒(méi)有 Web-2.0 氣息的名字 ;) 它的基本功能如下:
我們直接在應(yīng)用中使用 SQLite3 ,因?yàn)樵谶@種規(guī)模的應(yīng)用中 SQlite3 已經(jīng)夠用了。如果 是大型應(yīng)用,那么就有必要使用能夠好的處理數(shù)據(jù)庫(kù)連接的 SQLAlchemy 了,它可以 同時(shí)對(duì)應(yīng)多種數(shù)據(jù)庫(kù),并做其他更多的事情。如果你的數(shù)據(jù)更適合使用 NoSQL 數(shù)據(jù)庫(kù), 那么也可以考慮使用某種流行的 NoSQL 數(shù)據(jù)庫(kù)。
這是教程應(yīng)用完工后的截圖:
http://wiki.jikexueyuan.com/project/flask-guide/images/5-1.png" alt="" />
在開(kāi)始之前需要為應(yīng)用創(chuàng)建下列文件夾:
/flaskr
/static
/templates
flaskr 文件夾不是一個(gè) Python 包,只是一個(gè)用來(lái)存放我們文件的地方。我們將把以后要用到的數(shù)據(jù)庫(kù)模式和主模塊放在這個(gè)文件夾中。 static 文件夾中的文件是用于供應(yīng)用用戶通過(guò) HTTP 訪問(wèn)的文件,主要是 CSS 和 javascript 文件。 Flask 將會(huì)在 templates 文件夾中搜索 Jinja2 模板,所有在教程中的模板都放在 templates 文件夾中。
首先我們要?jiǎng)?chuàng)建數(shù)據(jù)庫(kù)模式。本應(yīng)用只需要使用一張表,并且由于我們使用 SQLite , 所以這一步非常簡(jiǎn)單。把以下內(nèi)容保存為 schema.sql 文件并放在我們上一步創(chuàng)建的 flaskr 文件夾中就行了:
drop table if exists entries;
create table entries (
id integer primary key autoincrement,
title text not null,
text text not null
);
這個(gè)模式只有一張名為 entries 的表,表中的字段為 id 、 title 和 text 。 id 是主鍵,是自增整數(shù)型字段,另外兩個(gè)字段是非空的字符串型字段。
現(xiàn)在我們已經(jīng)準(zhǔn)備好了數(shù)據(jù)庫(kù)模式了,下面來(lái)創(chuàng)建應(yīng)用模塊。我們把模塊命名為 flaskr.py ,并放在 flaskr 文件夾中。為了方便初學(xué)者學(xué)習(xí),我們把庫(kù)的導(dǎo)入與相關(guān)配置放在了一起。對(duì)于小型應(yīng)用來(lái)說(shuō),可以把配置直接放在模塊中。但是更加清晰的 方案是把配置放在一個(gè)獨(dú)立的 .ini 或 .py 文件中,并在模塊中導(dǎo)入配置的值。
在 flaskr.py 文件中:
# all the imports
import sqlite3
from flask import Flask, request, session, g, redirect, url_for, \
abort, render_template, flash
# configuration
DATABASE = '/tmp/flaskr.db'
DEBUG = True
SECRET_KEY = 'development key'
USERNAME = 'admin'
PASSWORD = 'default'
接著創(chuàng)建真正的應(yīng)用,并用同一文件中的配置來(lái)初始化,在 flaskr.py 文件中:
# create our little application :)
app = Flask(__name__)
app.config.from_object(__name__)
from_object() 會(huì)查看給定的對(duì)象(如果該對(duì)象是一個(gè)字符串就會(huì)直接導(dǎo)入它),搜索對(duì)象中所有變量名均為大字字母的變量。在我們的應(yīng)用中,已經(jīng)將配 置寫(xiě)在前面了。你可以把這些配置放到一個(gè)獨(dú)立的文件中。
通常,從一個(gè)配置文件中導(dǎo)入配置是比較好的做法,我們使用 from_envvar() 來(lái)完成這個(gè)工作,把上面的 from_object() 一行替換為:
app.config.from_envvar('FLASKR_SETTINGS', silent=True)
這樣做就可以設(shè)置一個(gè) FLASKR_SETTINGS 的環(huán)境變量來(lái)指定一個(gè)配置文件,并根據(jù)該文件來(lái)重載缺省的配置。 silent 開(kāi)關(guān)的作用是告訴 Flask 如果沒(méi)有這個(gè)環(huán)境變量 不要報(bào)錯(cuò)。
secret_key (密鑰)用于保持客戶端會(huì)話安全,請(qǐng)謹(jǐn)慎地選擇密鑰,并盡可能的使 復(fù)雜而且不容易被猜到。 DEBUG 標(biāo)志用于開(kāi)關(guān)交互調(diào)試器。因?yàn)檎{(diào)試模式允許用戶執(zhí)行服務(wù)器上的代碼,所以永遠(yuǎn)不要在生產(chǎn)環(huán)境中打開(kāi)調(diào)試模式 !
我們還添加了一個(gè)方便連接指定數(shù)據(jù)庫(kù)的方法。這個(gè)方法可以用于在請(qǐng)求時(shí)打開(kāi)連接,也可以用于 Python 交互終端或代碼中。以后會(huì)派上用場(chǎng)。
def connect_db():
return sqlite3.connect(app.config['DATABASE'])
最后,在文件末尾添加以單機(jī)方式啟動(dòng)服務(wù)器的代碼:
if __name__ == '__main__':
app.run()
到此為止,我們可以順利運(yùn)行應(yīng)用了。輸入以下命令開(kāi)始運(yùn)行:
python flaskr.py
你會(huì)看到服務(wù)器已經(jīng)運(yùn)行的信息,其中包含應(yīng)用訪問(wèn)地址。
因?yàn)槲覀冞€沒(méi)創(chuàng)建視圖,所以當(dāng)你在瀏覽器中訪問(wèn)這個(gè)地址時(shí),會(huì)得到一個(gè) 404 頁(yè)面未 找到錯(cuò)誤。很快我們就會(huì)談到視圖,但我們先要弄好數(shù)據(jù)庫(kù)。
想讓你的服務(wù)器被公開(kāi)訪問(wèn)?詳見(jiàn)外部可見(jiàn)的服務(wù)器 。
如前所述 Flaskr 是一個(gè)數(shù)據(jù)庫(kù)驅(qū)動(dòng)的應(yīng)用,更準(zhǔn)確地說(shuō)是一個(gè)關(guān)系型數(shù)據(jù)庫(kù)驅(qū)動(dòng)的 應(yīng)用。關(guān)系型數(shù)據(jù)庫(kù)需要一個(gè)數(shù)據(jù)庫(kù)模式來(lái)定義如何儲(chǔ)存信息,因此必須在第一次運(yùn)行 服務(wù)器前創(chuàng)建數(shù)據(jù)庫(kù)模式。
使用 sqlite3 命令通過(guò)管道導(dǎo)入 schema.sql 創(chuàng)建模式:
sqlite3 /tmp/flaskr.db < schema.sql
上述方法的不足之處是需要額外的 sqlite3 命令,但這個(gè)命令不是每個(gè)系統(tǒng)都有的。而且還必須提供數(shù)據(jù)庫(kù)的路徑,容易出錯(cuò)。因此更好的方法是在應(yīng)用中添加一個(gè)數(shù)據(jù)庫(kù)初始化函數(shù)。
添加的方法是:首先從 contextlib 庫(kù)中導(dǎo)入 contextlib.closing() 函數(shù),即在 flaskr.py 文件的導(dǎo)入部分添加如下內(nèi)容:
from contextlib import closing
接下來(lái),可以創(chuàng)建一個(gè)用來(lái)初始化數(shù)據(jù)庫(kù)的 init_db 函數(shù),其中我們使用了先前創(chuàng)建的 connect_db 函數(shù)。把這個(gè)初始化函數(shù)放在 flaskr.py 文件中的connect_db 函數(shù) 下面:
def init_db():
with closing(connect_db()) as db:
with app.open_resource('schema.sql', mode='r') as f:
db.cursor().executescript(f.read())
db.commit()
closing() 幫助函數(shù)允許我們?cè)?with 代碼塊保持?jǐn)?shù)據(jù)庫(kù)連接打開(kāi)。應(yīng)用對(duì)象的 open_resource() 方法支持也支持這個(gè)功能, 可以在 with 代碼塊中直接使用。這個(gè)函數(shù)打開(kāi)一個(gè)位于來(lái)源位置(你的 flaskr 文件夾)的文件并允許你讀取文件的內(nèi)容。這里我們用于在數(shù)據(jù)庫(kù)連接上執(zhí)行代碼。
當(dāng)我們連接到數(shù)據(jù)庫(kù)時(shí),我們得到一個(gè)提供指針的連接對(duì)象(本例中的 db )。這個(gè) 指針有一個(gè)方法可以執(zhí)行完整的代碼。最后我們提供要做的修改。 SQLite 3 和其他事務(wù)型數(shù)據(jù)庫(kù)只有在顯式提交時(shí)才會(huì)真正提交。
現(xiàn)在可以創(chuàng)建數(shù)據(jù)庫(kù)了。打開(kāi) Python shell ,導(dǎo)入,調(diào)用函數(shù):
>>> from flaskr import init_db
>>> init_db()
如果出現(xiàn)表無(wú)法找到的問(wèn)題,請(qǐng)檢查是否寫(xiě)錯(cuò)了函數(shù)名稱(應(yīng)該是 init_db ), 是否寫(xiě)錯(cuò)了表名(例如單數(shù)復(fù)數(shù)錯(cuò)誤)。
現(xiàn)在我們已經(jīng)學(xué)會(huì)如何打開(kāi)并在代碼中使用數(shù)據(jù)庫(kù)連接,但是如何優(yōu)雅地在請(qǐng)求時(shí)使用它呢?我們會(huì)在每一個(gè)函數(shù)中用到數(shù)據(jù)庫(kù)連接,因此有必要在請(qǐng)求之前初始化連接,并在請(qǐng)求之后關(guān)閉連接。
Flask 中可以使用 before_request() 、 after_request() 和 teardown_request() 裝飾器達(dá)到這個(gè)目的:
@app.before_request
def before_request():
g.db = connect_db()
@app.teardown_request
def teardown_request(exception):
db = getattr(g, 'db', None)
if db is not None:
db.close()
g.db.close()
使用 before_request() 裝飾的函數(shù)會(huì)在請(qǐng)求之前調(diào)用,且不傳遞 參數(shù)。使用 after_request() 裝飾的函數(shù)會(huì)在請(qǐng)求之后調(diào)用,且 傳遞發(fā)送給客戶端響應(yīng)對(duì)象。它們必須傳遞響應(yīng)對(duì)象,所以在出錯(cuò)的情況下就不會(huì)執(zhí)行。 因此我們就要用到 teardown_request() 裝飾器了。這個(gè)裝飾器下 的函數(shù)在響應(yīng)對(duì)象構(gòu)建后被調(diào)用。它們不允許修改請(qǐng)求,并且它們的返回值被忽略。如果 請(qǐng)求過(guò)程中出錯(cuò),那么這個(gè)錯(cuò)誤會(huì)傳遞給每個(gè)函數(shù);否則傳遞 None 。
我們把數(shù)據(jù)庫(kù)連接保存在 Flask 提供的特殊的 g 對(duì)象中。這個(gè)對(duì)象與 每一個(gè)請(qǐng)求是一一對(duì)應(yīng)的,并且只在函數(shù)內(nèi)部有效。不要在其它對(duì)象中儲(chǔ)存類似信息, 因?yàn)樵诙嗑€程環(huán)境下無(wú)效。這個(gè)特殊的 g 對(duì)象會(huì)在后臺(tái)神奇的工作,保證系統(tǒng)正常運(yùn)行。
若想更好地處理這種資源,請(qǐng)參閱在 Flask 中使用 SQLite 3 。
我該把這些代碼放在哪里?
如果你按教程一步一步讀下來(lái),那么可能會(huì)疑惑應(yīng)該把這個(gè)步驟和以后的代碼放在哪 里?比較有條理的做法是把這些模塊級(jí)別的函數(shù)放在一起,并把新的 before_request 和 teardown_request 函數(shù)放在前文的 init_db 函數(shù) 下面(即按照教程的順序放置)。
如果你已經(jīng)暈頭轉(zhuǎn)向了,那么你可以參考一下 示例源代碼 。在 Flask 中,你可以把應(yīng)用的所有代碼都放在同一個(gè) Python 模塊中。但是你沒(méi)有必要這樣做,尤其是當(dāng)你的應(yīng)用變大了的時(shí)候,更不應(yīng)當(dāng)這樣。
現(xiàn)在數(shù)據(jù)庫(kù)連接弄好了,接著開(kāi)始寫(xiě)視圖函數(shù)。我們共需要四個(gè)視圖函數(shù):
這個(gè)視圖顯示所有數(shù)據(jù)庫(kù)中的條目。它綁定應(yīng)用的根地址,并從數(shù)據(jù)庫(kù)中讀取 title 和 text 字段。 id 最大的記錄(最新的條目)在最上面。從指針?lè)祷氐挠涗浖且粋€(gè)包含 select 語(yǔ)句查詢結(jié)果的元組。對(duì)于教程應(yīng)用這樣的小應(yīng)用,做到這樣就已經(jīng)夠好了。但是你可能想要把結(jié)果轉(zhuǎn)換為字典,具體做法參見(jiàn)簡(jiǎn)化查詢 中的例子。
這個(gè)視圖會(huì)把條目作為字典傳遞給 show_entries.html 模板,并返回渲染結(jié)果:
@app.route('/')
def show_entries():
cur = g.db.execute('select title, text from entries order by id desc')
entries = [dict(title=row[0], text=row[1]) for row in cur.fetchall()]
return render_template('show_entries.html', entries=entries)
這個(gè)視圖可以讓一個(gè)登錄后的用戶添加一個(gè)新條目。本視圖只響應(yīng) POST 請(qǐng)求,真正的表單顯示在 show_entries 頁(yè)面中。如果一切順利,我們會(huì) flash() 一個(gè)消息給下一個(gè)請(qǐng)求并重定向回到 show_entries 頁(yè)面:
@app.route('/add', methods=['POST'])
def add_entry():
if not session.get('logged_in'):
abort(401)
g.db.execute('insert into entries (title, text) values (?, ?)',
[request.form['title'], request.form['text']])
g.db.commit()
flash('New entry was successfully posted')
return redirect(url_for('show_entries'))
注意,我們?cè)诒疽晥D中檢查了用戶是否已經(jīng)登錄(即檢查會(huì)話中是否有 logged_in 鍵,且對(duì)應(yīng)的值是否為 True )。
請(qǐng)像示例代碼一樣確保在構(gòu)建 SQL 語(yǔ)句時(shí)使用問(wèn)號(hào)。否則當(dāng)你使用字符串構(gòu)建 SQL 時(shí)容易遭到 SQL 注入攻擊。更多內(nèi)容參見(jiàn) 在 Flask 中使用 SQLite 3 。
這些函數(shù)用于用戶登錄和注銷。登錄視圖根據(jù)配置中的用戶名和密碼驗(yàn)證用戶并在會(huì)話中設(shè)置 logged_in 鍵值。如果用戶通過(guò)驗(yàn)證,鍵值設(shè)為 True ,那么用戶會(huì)被重定向到 show_entries 頁(yè)面。另外閃現(xiàn)一個(gè)信息,告訴用戶已登錄成功。如果出現(xiàn)錯(cuò)誤,模板會(huì) 提示錯(cuò)誤信息,并讓用戶重新登錄:
@app.route('/login', methods=['GET', 'POST'])
def login():
error = None
if request.method == 'POST':
if request.form['username'] != app.config['USERNAME']:
error = 'Invalid username'
elif request.form['password'] != app.config['PASSWORD']:
error = 'Invalid password'
else:
session['logged_in'] = True
flash('You were logged in')
return redirect(url_for('show_entries'))
return render_template('login.html', error=error)
登出視圖則正好相反,把鍵值從會(huì)話中刪除。在這里我們使用了一個(gè)小技巧:如果你使用字典的 pop() 方法并且傳遞了第二個(gè)參數(shù)(鍵的缺省值),那么當(dāng)字典中有 這個(gè)鍵時(shí)就會(huì)刪除這個(gè)鍵,否則什么也不做。這樣做的好處是我們不用檢查用戶是否已經(jīng)登錄了。
@app.route('/logout')
def logout():
session.pop('logged_in', None)
flash('You were logged out')
return redirect(url_for('show_entries'))
現(xiàn)在開(kāi)始寫(xiě)模板。如果我們現(xiàn)在訪問(wèn) URL ,那么會(huì)得到一個(gè) Flask 無(wú)法找到模板文件的 異常。 Flask 使用 Jinja2 模板語(yǔ)法并默認(rèn)開(kāi)啟自動(dòng)轉(zhuǎn)義。也就是說(shuō)除非用 Markup 標(biāo)記一個(gè)值或在模板中使用 |safe 過(guò)濾器,否則 Jinja2 會(huì)把如 < 或 > 之類的特殊字符轉(zhuǎn)義為與其 XML 等價(jià)字符。
我們還使用了模板繼承以保存所有頁(yè)面的布局統(tǒng)一。
請(qǐng)把以下模板放在 templates 文件夾中:
這個(gè)模板包含 HTML 骨架、頭部和一個(gè)登錄鏈接(如果用戶已登錄則變?yōu)橐粋€(gè)注銷鏈接 )。如果有閃現(xiàn)信息,那么還會(huì)顯示閃現(xiàn)信息。 {% block body %} 塊會(huì)被子模板中同名( body )的塊替換。
session 字典在模板中也可以使用。你可以使用它來(lái)檢驗(yàn)用戶是否已經(jīng) 登錄。注意,在 Jinja 中可以訪問(wèn)對(duì)象或字典的不存在的屬性和成員。如例子中的 'logged_in' 鍵不存在時(shí)代碼仍然能正常運(yùn)行:
<!doctype html>
<title>Flaskr</title>
<link rel=stylesheet type=text/css href="{{ url_for('static', filename='style.css') }}">
<div class=page>
<h1>Flaskr</h1>
<div class=metanav>
{% if not session.logged_in %}
<a href="{{ url_for('login') }}">log in</a>
{% else %}
<a href="{{ url_for('logout') }}">log out</a>
{% endif %}
</div>
{% for message in get_flashed_messages() %}
<div class=flash>{{ message }}</div>
{% endfor %}
{% block body %}{% endblock %}
</div>
這個(gè)模板擴(kuò)展了上述的 layout.html 模板,用于顯示信息。注意, for 遍歷了我們通過(guò) render_template() 函數(shù)傳遞的所有信息。模板還告訴表單使用 POST 作為 HTTP 方法向 add_entry 函數(shù)提交數(shù)據(jù):
{% extends "layout.html" %}
{% block body %}
{% if session.logged_in %}
<form action="{{ url_for('add_entry') }}" method=post class=add-entry>
<dl>
<dt>Title:
<dd><input type=text size=30 name=title>
<dt>Text:
<dd><textarea name=text rows=5 cols=40></textarea>
<dd><input type=submit value=Share>
</dl>
</form>
{% endif %}
<ul class=entries>
{% for entry in entries %}
<li><h2>{{ entry.title }}</h2>{{ entry.text|safe }}
{% else %}
<li><em>Unbelievable. No entries here so far</em>
{% endfor %}
</ul>
{% endblock %}
最后是簡(jiǎn)單顯示用戶登錄表單的登錄模板:
{% extends "layout.html" %}
{% block body %}
<h2>Login</h2>
{% if error %}<p class=error><strong>Error:</strong> {{ error }}{% endif %}
<form action="{{ url_for('login') }}" method=post>
<dl>
<dt>Username:
<dd><input type=text name=username>
<dt>Password:
<dd><input type=password name=password>
<dd><input type=submit value=Login>
</dl>
</form>
{% endblock %}
現(xiàn)在萬(wàn)事俱備,只剩給應(yīng)用添加一些樣式了。只要把以下內(nèi)容保存為 static 文件夾中的 style.css 文件就行了:
body { font-family: sans-serif; background: #eee; }
a, h1, h2 { color: #377ba8; }
h1, h2 { font-family: 'Georgia', serif; margin: 0; }
h1 { border-bottom: 2px solid #eee; }
h2 { font-size: 1.2em; }
.page { margin: 2em auto; width: 35em; border: 5px solid #ccc;
padding: 0.8em; background: white; }
.entries { list-style: none; margin: 0; padding: 0; }
.entries li { margin: 0.8em 1.2em; }
.entries li h2 { margin-left: -1em; }
.add-entry { font-size: 0.9em; border-bottom: 1px solid #ccc; }
.add-entry dl { font-weight: bold; }
.metanav { text-align: right; font-size: 0.8em; padding: 0.3em;
margin-bottom: 1em; background: #fafafa; }
.flash { background: #cee5F5; padding: 0.5em;
border: 1px solid #aacbe2; }
.error { background: #f0d6d6; padding: 0.5em; }
現(xiàn)在你已經(jīng)完成了整個(gè)應(yīng)用,一切都運(yùn)行正常。為了方便以后進(jìn)行完善修改,添加自動(dòng)測(cè)試不失為一個(gè)好主意。本教程中的應(yīng)用已成為測(cè)試 Flask 應(yīng)用文檔中演示如何進(jìn)行 單元測(cè)試的例子,可以去看看測(cè)試 Flask 應(yīng)用是多么容易
? Copyright 2013, Armin Ronacher. Created using Sphinx.