Flask簡介
Flask
是主流PythonWeb
三大框架之一,其特點是短小精悍以及功能強大從而獲得眾多Pythoner
的追捧,相比於Django
它更加簡單更易上手,Flask
擁有非常強大的三方庫,提供各式各樣的模組對其本身進行擴充:
下面是Flask
與Django
本身的一些區別:
Flask | Django | |
---|---|---|
閘道器介面(WSGI) | werkzeug | wsgiref |
模板語言(Template) | Jinja2 | DjangoTemplate |
ORM | SQLAlchemy | DjangoORM |
下載Flask
:
pip install flask
werkzeug模組
Flask
本質就是對werkzeug
模組進行一些更高層次的封裝,就如同Django
是對wsgiref
模組做了一些更高層次封裝一樣。所以先來看一下werkzeug
模組如何使用:
from werkzeug.wrappers import Request, Response
from werkzeug.serving import run_simple
@Request.application
def index(request):
return Response("Hello werkzeug")
if __name__ == '__main__':
run_simple("localhost", 5000, index)
簡單入門
基本使用
使用Flask
的小案例:
from flask import Flask
# 1.建立Flask物件例項,填入構造引數
app = Flask(__name__)
# 2.檢視函式中書寫route以及view
@app.route("/")
def index():
return "Hello Flask!"
# 3.啟動監聽,等待連結請求 預設埠號:5000,可在run()時新增形參
if __name__ == '__main__':
app.run()
# app.run(Thread=True) # 開啟多執行緒
構造引數
對於建立Flask
例項物件,傳入的構造引數有以下選項:
形參 | 描述 | 預設值 |
---|---|---|
import_name | 為Flask物件取名,一般為__name__即可 | 無 |
static_url_path | 模板中訪問的靜態檔案存放目錄,預設情況下與static_folder同名 | None |
static_folder | 靜態檔案存放的目錄名稱,預設當前專案中的static目錄 | static |
static_host | 遠端靜態檔案所用的Host地址 | None |
host_matching | 如果不是特別需要的話,慎用,否則所有的route都需要host=""的引數 | False |
subdomain_matching | SERVER_NAME子域名,暫時未GET到其作用 | False |
template_folder | template模板目錄, 預設當前專案中的templates目錄 | templates |
instance_path | 指向另一個Flask例項的路徑 | None |
instance_relative_config | 是否載入另一個例項的配置 | False |
root_path | 主模組所在的目錄的絕對路徑,預設專案目錄 | None |
Flask配置項
如同Django
中的settings.py
一樣,在Flask
中也擁有它自己的一些配置項。通過以下方式可對配置項進行修改。
debug模式
一般來說對於Flask
的開發模式都是用app.debug=True
來完成的:
app = Flask(__name__)
app.debug = True
當然你也可以依照下面的方式進行修改。
config修改
對Flask
例項直接進行config
的字典操作修改配置項:
app = Flask(__name__)
app.config["DEBUG"] = True
from_pyfile
以py
檔案形式進行配置:
app = Flask(__name__)
app.config.from_pyfile("flask_settings.py")
# flask_settings.py
DEBUG = True
from_object
以class
與類屬性的方式書寫配置項:
app = Flask(__name__)
app.config.from_object("flask_settings.DevelopmentConfig")
# flask_settings.py
class BaseConfig(object):
"""
抽象類,只用於繼承
"""
DEBUG = False
TESTING = False
# 其他配置項
class ProductionConfig(BaseConfig):
"""
上線時的配置項
"""
DATABASE_URI = 'mysql://user@localhost/foo'
class DevelopmentConfig(BaseConfig):
"""
開發時的配置項
"""
DEBUG = True
其他配置
通過環境變數配置:
app.config.from_envvar("環境變數名稱")
# 環境變數的值為python檔名稱名稱,內部呼叫from_pyfile方法
通過JSON
格式檔案配置:
app.config.from_json("json檔名稱")
# JSON檔名稱,必須是json格式,因為內部會執行json.loads
通過字典格式配置:
app.config.from_mapping({'DEBUG':True})
配置項大全
以下是Flask
的配置項大全:
'DEBUG': False, # 是否開啟Debug模式
'TESTING': False, # 是否開啟測試模式
'PROPAGATE_EXCEPTIONS': None, # 異常傳播(是否在控制檯列印LOG) 當Debug或者testing開啟後,自動為True
'PRESERVE_CONTEXT_ON_EXCEPTION': None, # 一兩句話說不清楚,一般不用它
'SECRET_KEY': None, # 之前遇到過,在啟用Session的時候,一定要有它
'PERMANENT_SESSION_LIFETIME': 31, # days , Session的生命週期(天)預設31天
'USE_X_SENDFILE': False, # 是否棄用 x_sendfile
'LOGGER_NAME': None, # 日誌記錄器的名稱
'LOGGER_HANDLER_POLICY': 'always',
'SERVER_NAME': None, # 服務訪問域名
'APPLICATION_ROOT': None, # 專案的完整路徑
'SESSION_COOKIE_NAME': 'session', # 在cookies中存放session加密字串的名字
'SESSION_COOKIE_DOMAIN': None, # 在哪個域名下會產生session記錄在cookies中
'SESSION_COOKIE_PATH': None, # cookies的路徑
'SESSION_COOKIE_HTTPONLY': True, # 控制 cookie 是否應被設定 httponly 的標誌,
'SESSION_COOKIE_SECURE': False, # 控制 cookie 是否應被設定安全標誌
'SESSION_REFRESH_EACH_REQUEST': True, # 這個標誌控制永久會話如何重新整理
'MAX_CONTENT_LENGTH': None, # 如果設定為位元組數, Flask 會拒絕內容長度大於此值的請求進入,並返回一個 413 狀態碼
'SEND_FILE_MAX_AGE_DEFAULT': 12, # hours 預設快取控制的最大期限
'TRAP_BAD_REQUEST_ERRORS': False,
# 如果這個值被設定為 True ,Flask不會執行 HTTP 異常的錯誤處理,而是像對待其它異常一樣,
# 通過異常棧讓它冒泡地丟擲。這對於需要找出 HTTP 異常源頭的可怕除錯情形是有用的。
'TRAP_HTTP_EXCEPTIONS': False,
# Werkzeug 處理請求中的特定資料的內部資料結構會丟擲同樣也是“錯誤的請求”異常的特殊的 key errors 。
# 同樣地,為了保持一致,許多操作可以顯式地丟擲 BadRequest 異常。
# 因為在除錯中,你希望準確地找出異常的原因,這個設定用於在這些情形下除錯。
# 如果這個值被設定為 True ,你只會得到常規的回溯。
'EXPLAIN_TEMPLATE_LOADING': False,
'PREFERRED_URL_SCHEME': 'http', # 生成URL的時候如果沒有可用的 URL 模式話將使用這個值
'JSON_AS_ASCII': True,
# 預設情況下 Flask 使用 ascii 編碼來序列化物件。如果這個值被設定為 False ,
# Flask不會將其編碼為 ASCII,並且按原樣輸出,返回它的 unicode 字串。
# 比如 jsonfiy 會自動地採用 utf-8 來編碼它然後才進行傳輸。
'JSON_SORT_KEYS': True,
#預設情況下 Flask 按照 JSON 物件的鍵的順序來序來序列化它。
# 這樣做是為了確保鍵的順序不會受到字典的雜湊種子的影響,從而返回的值每次都是一致的,不會造成無用的額外 HTTP 快取。
# 你可以通過修改這個配置的值來覆蓋預設的操作。但這是不被推薦的做法因為這個預設的行為可能會給你在效能的代價上帶來改善。
'JSONIFY_PRETTYPRINT_REGULAR': True,
'JSONIFY_MIMETYPE': 'application/json',
'TEMPLATES_AUTO_RELOAD': None,
路由
路由引數
所有路由中的引數如下:
@app.route("/index", methods=["POST", "GET"], endpoint="別名", defaults={"預設引數": 1}, strict_slashes=True,
redirect_to="/", subdomain=None)
詳細描述:
引數 | 描述 |
---|---|
methods | 訪問方式,預設只支援GET |
endpoint | 別名、預設為函式名,不可重複。預設為函式名 |
defaults | 當檢視函式擁有一個形參時,可將它作為預設引數傳遞進去 |
strict_slashes | 是否嚴格要求路徑訪問,如定義的時候是/index,訪問是/index/,預設是嚴格訪問 |
redirect_to | 301永久重定向,如函式help的redirect_to是/doc,則訪問help將跳轉到doc函式 |
subdomain | 通過指定的域名進行訪問,在瀏覽器中輸入域名即可,本地需配置hosts檔案 |
轉換器
Flask
中擁有Django3
中的轉換器來捕捉使用者請求的位址列引數:
轉換器 | 含義 |
---|---|
default | 接收字串,預設的轉換器 |
string | 接收字串,和預設的一樣 |
any | 可以指定多個路徑 |
int | 接收整數 |
float | 接收浮點數和整數 |
uuid | 唯一標識碼 |
path | 和字串一樣,但是它可以配置/,字串不可以 |
如下所示:
http://localhost:5000/article/2020-01-29
@app.route("/article/<int:year>-<int:month>-<int:day>", methods=["POST", "GET"])
def article(year, month, day):
# 相當於有命分組,必須使用同樣的變數名接收
# 並且還會自動轉換型別,int捕捉到的就都是int型別
return f"{year}-{month}-{day}"
正則匹配
由於引數捕捉只支援轉換器,所以我們可以自定義一個轉換器讓其能夠支援正則匹配:
from flask import Flask, url_for
from werkzeug.routing import BaseConverter
app = Flask(import_name=__name__)
class RegexConverter(BaseConverter):
"""
自定義URL匹配正規表示式
"""
def __init__(self, map, regex):
super(RegexConverter, self).__init__(map)
self.regex = regex
def to_python(self, value):
"""
路由匹配時,匹配成功後傳遞給檢視函式中引數的值
:param value:
:return:
"""
return int(value)
def to_url(self, value):
"""
使用url_for反向生成URL時,傳遞的引數經過該方法處理,返回的值用於生成URL中的引數
:param value:
:return:
"""
val = super(RegexConverter, self).to_url(value)
return val
# 新增到flask中
app.url_map.converters['regex'] = RegexConverter
@app.route('/index/<regex("\d+"):nid>')
def index(nid):
print(url_for('index', nid='888'))
return 'Index'
if __name__ == '__main__':
app.run()
反向解析
使用url_for()
可在檢視中反向解析出url
:
# url_for(endpoint, **values)
print(url_for("article", **{"year": 2010, "month": 11, "day": 11}))
print(url_for("article", year=2010, month=11, day=11))
如果在模板中,也可以使用url_for()
進行反向解析:
<a href='{{ url_for("article", year=2010, month=11, day=11)) }}'>點我</a>
app.add_url_rule
可以發現,Flask
的路由與Django
的有非常大的區別,但是通過app.add_url_rule
也可以做到和Django
相似。
但是這樣的做法很少,函式簽名如下:
def add_url_rule(
self,
rule, # 規則
endpoint=None, # 別名
view_func=None, # 檢視函式
provide_automatic_options=None, # 控制是否自動新增options
**options
):
實際應用如下:
from flask import Flask
app = Flask(__name__)
def index():
return "index"
def home(name):
return "Welcome Home, %s" % name
routers = [
("/index", None, index),
("/home/<string:name>", None, home),
]
for rule in routers:
app.add_url_rule(*rule)
if __name__ == '__main__':
app.run()
檢視
請求相關
Flask
的request
物件不是通過引數傳遞,而是通過匯入:
from flask import request
下面是一些常用的屬性與方法:
屬性/方法 | 描述 |
---|---|
request.headers | 檢視所有的請求頭 |
request.method | 存放請求方式 |
request.form | 存放form表單中的序列化資料,一般來說就是POST請求的資料 |
request.args | 存放url裡面的序列化資料,一般來說就是GET請求的資料 |
request.data | 檢視傳過來所有解析不了的內容 |
request.json | 檢視前端傳過來的json格式資料,內部會自動反序列化 |
request.values.to_dict() | 存放url和from中的所有資料 |
request.cookies | 前端傳過來的cookies |
request.path | 路由地址,如:/index |
request.full_path | 帶引數的請求路由地址,如:/index?name=yunya |
request.url | 全部地址,如:http://127.0.0.1:5000/index?name=yunya |
request.host | 主機位,如:127.0.0.1:5000 |
request.host_url | 將主機位轉換成url,如:http://127.0.0.1:5000/ |
request.url_root | 域名 |
file = request.files | 前端傳過來的檔案 |
file.filename | 返回檔名稱 |
file.save() | 儲存檔案 |
操作演示:
from flask import Flask
from flask import request
app = Flask(__name__)
@app.route('/index',methods=["POST","GET"])
def index():
print(request.method)
if request.method == "GET":
return "GET"
elif request.method == "POST":
return "POST"
else:
return "ERROR"
if __name__ == '__main__':
app.run()
返回響應
返回響應一般有五種:
返回響應 | 描述 |
---|---|
return 'string' | 返回字串 |
return render_template() | 返回模板檔案 |
return redirect() | 302,重定向,可填入別名或者路由匹配地址 |
return jsonify() | 返回Json格式資料 |
return Response物件 | 直接返回一個物件,常用於取消XSS攻擊預防、設定返回頭等 |
注意,在Flask中,都會返回csrftoken,它存放在瀏覽器的cookie中。當Flask模板渲染的頁面傳送請求時會自動攜帶csrftoken,這與Django並不相同
此外,如果返回的物件不是字串、不是元組也不是Response物件,它會將值傳遞給Flask.force_type類方法,將它轉換成為一個響應物件
如下所示:
from flask import Flask
app = Flask(__name__)
@app.route('/templateTest')
def templateTest():
# 返回模板
from flask import render_template
return render_template("result.html")
@app.route('/redirectTest')
def redirectTest():
# 302重定向
from flask import redirect
return redirect("templateTest")
@app.route('/jsonTest')
def jsonTest():
# 返回json資料
from flask import jsonify
message = {"book": "flask", "price": 199, "publish": "BeiJing"}
return jsonify(message)
@app.route('/makeResponseTest')
def makeResponseTest():
# 返回Response物件
from flask import make_response
# 取消XSS攻擊預防
from flask import Markup
element = Markup("<a href='https://www.google.com'>點我一下</a>")
response = make_response(element)
# 操作cookie
response.set_cookie("key", "oldValue")
response.delete_cookie("key")
response.set_cookie("key", "newValue")
# 操作返回頭
response.headers["jwt"] = "ajfkdasi#@#kjdfsas9f(**jfd"
return response
if __name__ == '__main__':
app.run()
session
在Flask
中,session
也是通過匯入來操縱的,而不是通過request
物件。
需要注意的是在Flask
中`session
的儲存時長為31天,並且預設是儲存在記憶體中,並未做任何持久化處理。
如果想做持久化處理,則可以通過其他的一些第三方模組。
操作 | 描述 |
---|---|
session.get("key",None) | 獲取session |
session["key"]=value | 設定session |
session.pop("key",None) | 刪除session |
如下案例所示:
from flask import Flask
from flask import request
from flask import session
from flask import Markup
from flask import render_template
from flask import redirect
# 第一步,匯入session
app = Flask(__name__)
# 第二步,加鹽,也可以在配置檔案中加鹽
app.secret_key = "salt"
@app.route('/home')
def home():
username = session.get("username")
print(username)
if username:
return "歡迎回家%s"%username
return redirect("login")
@app.route('/login',methods=["GET","POST"])
def login():
if request.method == "GET":
return render_template("login.html")
if request.method == "POST":
username = request.form.get("username")
if username:
session["username"] = username
return "您已登入" + Markup("<a href='/home'>返回home</a>")
return redirect("login")
if __name__ == '__main__':
app.run()
<form method="POST">
<p><input type="text" name="username" placeholder="username"></p>
<p><input type="text" name="password" placeholder="password"></p>
<button type="submit">登入</button>
</form>
flash
訊息閃現flash
是基於session
來做的,它只會允許值被取出一次,內部通過pop()
實現。
使用方式如下:
flash("data", category="sort")
# 存入資料以及分類
get_flashed_messages(with_categories=False, category_filter=())
# 取出flash中的資料
# with_categories為True時返回一個tuple
# category_filter指定資料類別,如不指定則代表取出所有
如下所示:
from flask import Flask
from flask import flash
from flask import get_flashed_messages
app = Flask(__name__)
app.secret_key = "salt"
@app.route('/set_flash')
def set_flash():
flash(message="dataA",category="sortA")
flash(message="dataB",category="sortB")
return "OK!!"
@app.route('/get_flash/<string:choice>')
def get_flash(choice):
if choice == "all":
all_data = get_flashed_messages() # 取所有閃現訊息
return str(all_data) # ['dataA', 'dataB']
elif choice == "sortA":
sortA_data = get_flashed_messages(category_filter=("sortA",)) # 取類別A的所有閃現訊息
return str(sortA_data) # ['dataA']
elif choice == "sortB":
sortB_data = get_flashed_messages(category_filter=("sortB",)) # 取類別B的所有閃現訊息
return str(sortB_data) # ['dataB']
else:
return "ERROR"
if __name__ == '__main__':
app.run()
FBV
如果不是做前後端分離,那麼Flask
應用最多的還是FBV
:
from flask import Flask
app = Flask(__name__)
@app.route('/index')
def index():
return "index"
if __name__ == '__main__':
app.run()
CBV
使用CBV
必須匯入views.MethodView
且繼承它,初此之外必須使用app.add_url_rule
新增路由與檢視的關係對映:
from flask import Flask
from flask.views import MethodView
app = Flask(__name__)
class Home(MethodView):
methods = ["GET", "POST"] # 該類中允許的請求方式
decorators = [] # 裝飾器新增在這裡
def dispatch_request(self, *args, **kwargs):
print("首先執行該方法")
return super(Home, self).dispatch_request(*args, **kwargs)
def get(self):
return "Home,Get"
def post(self):
return "Home,Post"
app.add_url_rule("/home",view_func=Home.as_view(name="home"))
if __name__ == '__main__':
app.run()
RESTAPI
如果專案是前後端分離的,則需要藉助第三方模組flask-restful
,詳情查閱官網:
檔案上傳案例
儲存上傳檔案的案例:
from flask import Flask, request
app = Flask(__name__)
'''因為是檔案,所以只能是POST方式'''
@app.route("/upload", methods=["POST"])
def upload():
"""接受前端傳送來的檔案"""
file_obj = request.files.get("pic")
if file_obj is None:
# 表示沒有傳送檔案
return "未上傳檔案"
'''
將檔案儲存到本地(即當前目錄)
直接使用上傳的檔案物件儲存
'''
file_obj.save('pic.jpg') # 和前端上傳的檔案型別要相同
return "上傳成功"
# 將檔案儲存到本地(即當前目錄) 普通的儲存方法
# with open("./pic.jpg",'wb') as f:
# data = file_obj.read()
# f.write(data)
# return "上傳成功"
if __name__ == '__main__':
app.run(debug=True)
其他的一些補充知識:
file_obj.stream # 檔案流,即檔案的二進位制物件
from werkzeug.datastructures import FileStorage # 檢視詳情,檔案物件的具體方法
模板
jinja2簡介
jinja2
是Flask
中預設的模板語言,相比於DjangoTemplate
它更加的符合Python
語法。
如在模板傳參中,如果檢視中傳入是一個dict
,那麼在DTL
中只能通過.
的方式進行深度獲取,而在jinja2
中則可以通過[]
的方式進行獲取。
此外,在DTL
中如果檢視傳入一個function
則會自動加括號進行呼叫,而在jinja2
中就不會進行自動呼叫而是要自己手動加括號進行呼叫。
總而言之,jinja2
相比於DTL
來說更加的人性化。
模板傳參
模板傳參可以通過k=v
的方式傳遞,也可以通過**dict
的方式進行解包傳遞:
@app.route('/index')
def index():
context = {
"name": "雲崖",
"age": 18,
"hobby": ["籃球", "足球"]
}
return render_template("index.html", **context)
# return render_template("index.html", name="雲崖", age=18)
渲染,通過{{}}
進行:
<body>
<p>{{name}}</p>
<p>{{age}}</p>
<p>{{hobby.0}}-{{hobby[1]}}</p>
</body>
內建過濾器
常用的內建過濾器如下:
過濾器 | 描述 |
---|---|
escape | 轉義字元 |
safe | 關閉XSS預防,關閉轉義 |
striptags | 刪除字串中所有的html標籤,如果有多個空格連續,將替換為一個空格 |
first | 返回容器中的第一個元素 |
last | 返回容器中的最後一個元素 |
length | 返回容器總長度 |
abs | 絕對值 |
int | 轉換為int型別 |
float | 轉換為float型別 |
join | 字串拼接 |
lower | 轉換為小寫 |
upper | 轉換為大寫 |
capitialize | 把值的首字母轉換成大寫,其他子母轉換為小寫 |
title | 把值中每個單詞的首字母都轉換成大寫 |
trim | 把值的首尾空格去掉 |
round | 預設對數字進行四捨五入,也可以用引數進行控制 |
replace | 替換 |
format | 格式化字串 |
truncate | 擷取length長度的字串 |
default | 相當於or,如果渲染變數沒有值就用default中的值 |
使用內建過濾器:
<p>{{gender | default("性別不詳")}}</p>
分支迴圈
if
和for
都用{% %}
進行包裹,與DTL
中使用相似。
在for
中擁有以下變數,用來獲取當前的遍歷狀態:
for迴圈的遍歷 | 描述 |
---|---|
loop.index | 當前遍歷次數,從1開始計算 |
loop.index0 | 當前遍歷次數,從0開始計算 |
loop.first | 第一次遍歷 |
loop.last | 最後一次遍歷 |
loop.length | 遍歷物件的長度 |
loop.revindex | 到迴圈結束的次數,從1開始 |
loop.revindex0 | 到迴圈結束的次數,從0開始 |
下面是一則示例:
<body>
{% for item in range(10) %}
{% if loop.first %}
<p>第一次遍歷開始--->{{loop.index}}</p>
{% elif loop.last %}
<p>最後一次遍歷開始-->{{loop.index}}</p>
<p>遍歷了一共{{loop.length}}次</p>
{% else %}
<p>{{loop.index}}</p>
{% endif %}
{% endfor %}
</body>
結果如下:
第一次遍歷開始--->1
2
3
4
5
6
7
8
9
最後一次遍歷開始-->10
遍歷了一共10次
巨集的使用
在模板中的巨集類似於Python
中的函式,可對其進行傳值:
<body>
<!--定義巨集,後面是預設的引數-->
{% macro input(name, value="", type="text") %}
<input name="{{ name }}" value="{{ value }}" type="{{ type }}">
{% endmacro %}
<!--使用巨集-->
<form action="">
<p>username:{{ input("username") }}</p>
<p>password:{{ input("pwd", type="password")}}</p>
<p>{{ input(value="login", type="submit") }}</p>
</form>
</body>
可以在一個模板中專門定義巨集,其他模板中再進行匯入:
# 匯入方式一
# with context可以把後端傳到當前模板的變數傳到定義的巨集裡面
{% import "macros.html" as macro with context %}
<form>
<p>使用者名稱:{{ macro.input('username') }}</p>
<p>密碼:{{ macro.input('password',type="password" )}}</p>
<p> {{ macro.input(value="提交",type="submit" )}}</p>
</form>
# 匯入方式二
{% from "macros.html" import input as input_field %}
<form>
<p>使用者名稱:{{ input_field('username') }}</p>
<p>密碼:{{ input_field('password',type="password" )}}</p>
<p> {{ input_field(value="提交",type="submit" )}}</p>
</form>
定義變數
在模板中可通過{% set %}
和{% with %}
定義變數。
{% set %}是全域性變數,可在當前模板任意位置使用
{% with %}是區域性變數,只能在{% with %}語句塊中使用
<body>
{% set name="雲崖" %}
<p>名字是:{{name}}</p>
{% with age=18 %}
<p>年齡是:{{age}}</p>
{% endwith %}
</body>
模板繼承
使用{% extends %}
引入一個定義號的模板。
使用{% blocak %}
和{% endblock %}
定義塊
使用{{ super() }}
引入原本的模板塊內容
定義模板如下:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>jinja2學習</title>
{% block style %}
{% endblock %}
</head>
<body>
<header>
{% block header %}
<p>頭部資訊</p>
{% endblock %}
</header>
<main>
{% block main %}
<p>主體資訊</p>
{% endblock %}
</main>
<footer>
<p>頁面尾部</p>
</footer>
{% block script %}
{% endblock %}
</body>
</html>
匯入模板並使用:
{% extends "base.html" %}
{% block style %}
<style>
h1{
color:red;
}
</style>
{% endblock %}
{% block header %}
<!--呼叫父模板內容-->
{{ super() }}
{% endblock %}
{% block main %}
<h1>HELLO,歡迎來到Jinja2學習</h1>
{% endblock %}
{% block script %}
<script>
"use strict;"
console.log("HELLO,WORLD")
</script>
{% endblock %}
中介軟體
在Flask
中的中介軟體使用非常少。由於Flask
是基於werkzeug
模組來完成的,所以按理說我們只需要在werkzeug
的啟動流程中新增程式碼即可。
下面是中介軟體的使用方式,如果想了解它的原理在後面的原始碼分析中會有涉及。
在Flask
請求來臨時會執行wsgi_app
這樣的一個方法,所以就在這個方法上入手:
from flask import Flask
from flask import render_template
app = Flask(__name__)
# 中介軟體
class Middleware(object):
def __init__(self, old_wsgi_app):
self.old_wsgi_app = old_wsgi_app # 原本要執行的wsgi_app方法
def __call__(self, environ, start_response):
print("書寫程式碼...中介軟體。請求來時")
result = self.old_wsgi_app(environ, start_response)
print("書寫程式碼...中介軟體。請求走時")
return result
@app.route('/index')
def index():
return "Hello,world"
if __name__ == '__main__':
app.wsgi_app = Middleware(app.wsgi_app) # 傳入要原本執行的wsgi_app
app.run()
裝飾器
如何新增裝飾器
由於Flask
的每個檢視函式頭頂上都有一個裝飾器,且具有endpoint
不可重複的限制。
所以我們為單獨的某一個檢視函式新增裝飾器時一定要將其新增在下方(執行順序自下而上),此外還要使用functools.wraps
修改裝飾器inner()
讓每個裝飾器的inner.__name__
都不相同,來突破endpoint
不可重複的限制。
如下所示,為單獨的某一個介面書寫頻率限制的裝飾器:
from flask import Flask
from functools import wraps
app = Flask(__name__)
def flow(func):
@wraps(func) # 如果不加這個,路由的別名一致都是inner就會丟擲異常。func.__name__
def inner(*args,**kwargs):
# 書寫邏輯,用random代替。如果是0就代表不讓通過
import random
access = random.randint(0,1)
if access:
result = func(*args,**kwargs)
return result
else:
return "頻率太快了"
return inner
@app.route('/backend')
@flow
def backend():
return "backend"
@app.route('/index')
@flow
def index():
return "index"
if __name__ == '__main__':
app.run()
befor_request
每次請求來的時候都會走它,由於Flask
的中介軟體比較弱雞,所以這種方式更常用。
類似於Django
中介軟體中的process_request
,如果有多個順序是從上往下,可以用它做session
認證。
如果返回的不是None,就攔截請求
@app.before_request
def before(*args,**kwargs):
if request.path=='/login':
return None
else:
name=session.get('user')
if not name:
return redirect('/login')
else:
return None
after_request
請求走了就會觸發,類似於Django
的process_response
,如果有多個,順序是從下往上執行:
必須傳入一個引數,就是檢視的return值
@app.after_request
def after(response):
print('我走了')
return response
before_first_request
目啟動起來第一次會走,以後都不會走了,也可以配多個(專案啟動初始化的一些操作)
如果返回的不是None,就攔截請求
@app.before_first_request
def first():
print('我的第一次')
teardown_request
每次檢視函式執行完了都會走它。
可以用來記錄出錯日誌:
@app.teardown_request
def ter(e):
print(e)
print('我是teardown_request ')
errorhandler
繫結錯誤的狀態碼,只要碼匹配就走它。
常用於重寫404
頁面等:
@app.errorhandler(404)
def error_404(arg):
return render_template('error.html',message='404錯誤')
template_global
定義全域性的標籤,如下所示:
@app.template_global()
def add(a1, a2):
return a1 + a2
# 在模板中:{{ add(3,4) }}
template_filter
定義全域性過濾器,如下所示:
@app.template_filter()
def db(a1, a2, a3): # 第一個值永遠都是|左邊的值
return a1 + a2 + a3
# 在模板中{{ 1|db(2,3)}}
多request順序
如果存在多個berfor_request
與多個after_request
那麼執行順序是怎樣的?
from flask import Flask
from functools import wraps
app = Flask(__name__)
@app.before_request
def before_fist():
print("第一個before_request")
@app.before_request
def before_last():
print("第二個before_request")
@app.after_request
def before_fist(response):
print("第一個after_request")
return response
@app.after_request
def before_last(response):
print("第二個after_request")
return response
@app.route('/index')
def index():
return "index"
if __name__ == '__main__':
app.run()
"""
第一個before_request
第二個before_request
第二個after_request
第一個after_request
"""
如果第一個before_request
就返回了非None
進行攔截,執行順序則和Django
的不一樣,Django
會返回同級的process_response
,而Flask
還必須要走所有的after_request
的:
@app.before_request
def before_fist():
print("第一個before_request")
return "攔截了"
第一個before_request
第二個after_request
第一個after_request
藍圖
藍圖Blueprint
的作用就是為了將功能和主服務分開。
說的直白點就是構建專案目錄,劃分內建的裝飾器作用域等,類似於Django
中app
的概念。
小型專案
下面有一個基本的專案目錄,如下所示:
- mysite
- mysite # 包,專案根目錄
- views # 資料夾,檢視相關
- index.py
- backend.py
- templates
- index # 資料夾,index相關的模板
- backend # 資料夾,backend相關的資源
- static
- index
- backend
- __init__.py
- manage.py # 啟動檔案
- settings.py # 配置檔案
這樣的目錄結構看起來就比較清晰,那麼如何對它進行管理呢?就可以使用藍圖:
# backend.py
from flask import Blueprint
from flask import render_template
bck = Blueprint("bck", __name__) # 建立藍圖物件 bck
@bck.route("/login") # 路由使用藍圖物件bck為字首,而不是app
def login():
return render_template("backend/backend_login.html")
# index.py
from flask import Blueprint
from flask import render_template
idx = Blueprint("idx", __name__)
@idx.route("/login")
def login():
return render_template("index/index_login.html")
# __init__.py
from flask import Flask
from .views.backend import bck
from .views.index import idx
def create_app():
app = Flask(import_name=__name__)
app.register_blueprint(bck) # 註冊藍圖物件 bck
app.register_blueprint(idx) # 註冊藍圖物件 idx
return app
# manage.py
from mysite import create_app
if __name__ == '__main__':
app = create_app()
app.run()
url字首
啟動服務後發現兩個功能區的login
都是相同的url
,導致後註冊的藍圖物件永遠無法訪問登入頁面。
在__init__.py
中註冊藍圖物件的程式碼中新增字首:
from flask import Flask
from .views.backend import bck
from .views.index import idx
def create_app():
app = Flask(import_name=__name__)
app.register_blueprint(bck, url_prefix="/backend/")
app.register_blueprint(idx, url_prefix="/index/")
return app
訪問時:
http://127.0.0.1:5000/backend/login
http://127.0.0.1:5000/index/login
藍圖資源
每個藍圖應用的資源都不相同,如下:
templates/index # 這是index訪問的模板路徑
templates/backend # 這是backend訪問的模板路徑
static/index
static/backend
如何指定他們的資源呢?其實在建立藍圖物件的時候就可以指定:
from flask import Blueprint
from flask import render_template
# 使用相對路徑
bck = Blueprint("bck",__name__, template_folder="../templates/backend", static_folder="../static/backend",)
@bck.route("/login")
def login():
return render_template("backend_login.html") # 注意不同藍圖物件之間的模板應該儘量不重名,重名可能導致一些錯誤
如果是靜態資源的訪問,並不會加上字首/backend
:
<img src="/static/backend/logo@2x.png" alt="">
藍圖裝飾器
藍圖裝飾器分為全域性裝飾器和區域性裝飾器兩種:
全域性裝飾器全域性有效:
def create_app():
app = Flask(import_name=__name__)
app.register_blueprint(bck, url_prefix="/backend/",)
app.register_blueprint(idx, url_prefix="/index/")
@app.before_request
def func():
print("全域性有效")
return app
區域性裝飾器只在當前藍圖物件bck
有效:
bck = Blueprint("bck",__name__, template_folder="../templates/backend",static_folder="../static/backend",)
@bck.before_request
def func():
print("區域性有效")
大型專案
構建大型專案,就完全可以將它做的和Django
相似,讓每個藍圖物件都擁有自己的templates
與static
。
- mysite
- mysite
- index # 包,單獨的一個藍圖物件
- static # 資料夾
- templates # 資料夾
- views.py
- __init__.py # 建立藍圖物件,指定template與static
- backend
- static
- templates
- views.py
- __init__.py
- manage.py # 啟動檔案
- settings.py # 配置檔案
多app應用
一個Flask
程式允許多個例項,如下所示:
from werkzeug.wsgi import DispatcherMiddleware
from werkzeug.serving import run_simple
from flask import Flask
app01 = Flask('app01')
app02 = Flask('app02')
@app01.route('/index')
def index():
return "app01"
@app02.route('/index')
def index2():
return "app02"
app = DispatcherMiddleware(app01, {
'/app01': app01,
'/app02': app02,
})
#預設使用app01的路由,也就是訪問 http://127.0.0.1:5000/index 返回app01
#當以app01開頭時候使用app01的路由,也就是http://127.0.0.1:5000/app01/index 返回app01
#當以app02開頭時候使用app02的路由,也就是http://127.0.0.1:5000/app02/index 返回app02
if __name__ == "__main__":
run_simple('127.0.0.1', 5000, app)
解決跨域
解決跨域請求,可以用第三方外掛,也可以自定義響應頭:
@app.after_request # 解決CORS跨域請求
def cors(response):
response.headers['Access-Control-Allow-Origin'] = "*"
if request.method == "OPTIONS":
response.headers["Access-Control-Allow-Headers"] = "Origin,Content-Type,Cookie,Accept,Token,authorization"
return response
上下文機制
全域性變數
在Flask
專案啟動時,會自動初始化一些全域性變數。其中有幾個變數尤為重要,可通過以下命令檢視:
from flask import globals
就是下面的6個變數,將貫穿整個HTTP
請求流程。
_request_ctx_stack = LocalStack()
_app_ctx_stack = LocalStack()
current_app = LocalProxy(_find_app)
request = LocalProxy(partial(_lookup_req_object, "request")) # from flask import request 拿的就是它
session = LocalProxy(partial(_lookup_req_object, "session")) # from flask import session 拿的就是它
g = LocalProxy(partial(_lookup_app_object, "g"))
偏函式
上面的6個變數中有兩個變數執行了類的例項化,並且有傳入了一個偏函式。
偏函式的作用在於不用傳遞一個引數,設定好後自動傳遞:
from functools import partial
def add(x, y):
return x + y
add = partial(add,1) # 自動傳遞第一個引數為1,返回一個新的函式
result = add(2)
print(result) # 3
列表實現棧
棧是一種後進先出的資料結構,使用列表可以實現一個棧:
class Stack(object):
def __init__(self):
self.__stack = []
def push(self, value):
self.__stack.append(value)
@property
def top(self):
try:
return self.__stack[-1]
except IndexError as e:
return None
stack = Stack()
stack.push(1)
print(stack.top)
在Flask
原始碼中多次有構建這個棧的地方(目前來看至少兩處)。
Local
Local
物件在全域性變數中會例項化兩次,作用是例項化出一個字典,用於存放執行緒中的東西:
_request_ctx_stack = LocalStack()
_app_ctx_stack = LocalStack()
我們來看看它的原始碼:
try: # 匯入協程獲取pid,或者是執行緒模組獲取pid的函式
from greenlet import getcurrent as get_ident
except ImportError:
try:
from thread import get_ident
except ImportError:
from _thread import get_ident
class Local(object):
# 只能 . 這裡面的
__slots__ = ("__storage__", "__ident_func__")
def __init__(self):
object.__setattr__(self, "__storage__", {})
object.__setattr__(self, "__ident_func__", get_ident)
# 返回可迭代物件,這裡可以看出__storage__是一個字典
def __iter__(self):
return iter(self.__storage__.items())
def __call__(self, proxy):
return LocalProxy(self, proxy)
def __release_local__(self):
self.__storage__.pop(self.__ident_func__(), None)
# 通過pid返回字典中的一個name對應的value
def __getattr__(self, name):
try:
return self.__storage__[self.__ident_func__()][name]
except KeyError:
raise AttributeError(name)
# 構建出一個字典,{pid:{name:value}}
def __setattr__(self, name, value):
ident = self.__ident_func__()
storage = self.__storage__
try:
storage[ident][name] = value
except KeyError:
storage[ident] = {name: value}
def __delattr__(self, name):
try:
del self.__storage__[self.__ident_func__()][name]
except KeyError:
raise AttributeError(name)
LocalStack
用於操縱Local
中構建的字典:
class LocalStack(object):
# 例項化
def __init__(self):
self._local = Local()
def __release_local__(self):
self._local.__release_local__()
@property
def __ident_func__(self):
return self._local.__ident_func__
@__ident_func__.setter
def __ident_func__(self, value):
object.__setattr__(self._local, "__ident_func__", value)
def __call__(self):
def _lookup():
rv = self.top
if rv is None:
raise RuntimeError("object unbound")
return rv
return LocalProxy(_lookup)
# 向Local字典中新增一個名為stack的列表
# {pid:{"stack":[]}}
def push(self, obj):
rv = getattr(self._local, "stack", None)
if rv is None:
self._local.stack = rv = []
rv.append(obj)
return rv
# 訊息閃現的實現原理,獲取或者移除
def pop(self):
stack = getattr(self._local, "stack", None)
if stack is None:
return None
elif len(stack) == 1:
release_local(self._local)
return stack[-1]
else:
return stack.pop()
# 只獲取
@property
def top(self):
try:
return self._local.stack[-1]
except (AttributeError, IndexError):
return None
LocalProxy
訪問Local
時用LocalProxy
,實際上是一個代理物件:
current_app = LocalProxy(_find_app)
request = LocalProxy(partial(_lookup_req_object, "request"))
session = LocalProxy(partial(_lookup_req_object, "session"))
g = LocalProxy(partial(_lookup_app_object, "g"))
這四句話代表四個意思,使用最多的範圍如下:
from flask import request
from flask import session
from flask import g
from flask import current_app
原始碼如下:
@implements_bool
class LocalProxy(object):
__slots__ = ("__local", "__dict__", "__name__", "__wrapped__")
def __init__(self, local, name=None):
object.__setattr__(self, "_LocalProxy__local", local)
object.__setattr__(self, "__name__", name)
if callable(local) and not hasattr(local, "__release_local__"):
object.__setattr__(self, "__wrapped__", local)
def _get_current_object(self):
if not hasattr(self.__local, "__release_local__"):
return self.__local()
try:
return getattr(self.__local, self.__name__)
except AttributeError:
raise RuntimeError("no object bound to %s" % self.__name__)
@property
def __dict__(self):
try:
return self._get_current_object().__dict__
except RuntimeError:
raise AttributeError("__dict__")
def __repr__(self):
try:
obj = self._get_current_object()
except RuntimeError:
return "<%s unbound>" % self.__class__.__name__
return repr(obj)
def __bool__(self):
try:
return bool(self._get_current_object())
except RuntimeError:
return False
def __unicode__(self):
try:
return unicode(self._get_current_object()) # noqa
except RuntimeError:
return repr(self)
def __dir__(self):
try:
return dir(self._get_current_object())
except RuntimeError:
return []
def __getattr__(self, name):
if name == "__members__":
return dir(self._get_current_object())
return getattr(self._get_current_object(), name)
def __setitem__(self, key, value):
self._get_current_object()[key] = value
def __delitem__(self, key):
del self._get_current_object()[key]
if PY2:
__getslice__ = lambda x, i, j: x._get_current_object()[i:j]
def __setslice__(self, i, j, seq):
self._get_current_object()[i:j] = seq
def __delslice__(self, i, j):
del self._get_current_object()[i:j]
__setattr__ = lambda x, n, v: setattr(x._get_current_object(), n, v)
__delattr__ = lambda x, n: delattr(x._get_current_object(), n)
__str__ = lambda x: str(x._get_current_object())
__lt__ = lambda x, o: x._get_current_object() < o
__le__ = lambda x, o: x._get_current_object() <= o
__eq__ = lambda x, o: x._get_current_object() == o
__ne__ = lambda x, o: x._get_current_object() != o
__gt__ = lambda x, o: x._get_current_object() > o
__ge__ = lambda x, o: x._get_current_object() >= o
__cmp__ = lambda x, o: cmp(x._get_current_object(), o) # noqa
__hash__ = lambda x: hash(x._get_current_object())
__call__ = lambda x, *a, **kw: x._get_current_object()(*a, **kw)
__len__ = lambda x: len(x._get_current_object())
__getitem__ = lambda x, i: x._get_current_object()[i]
__iter__ = lambda x: iter(x._get_current_object())
__contains__ = lambda x, i: i in x._get_current_object()
__add__ = lambda x, o: x._get_current_object() + o
__sub__ = lambda x, o: x._get_current_object() - o
__mul__ = lambda x, o: x._get_current_object() * o
__floordiv__ = lambda x, o: x._get_current_object() // o
__mod__ = lambda x, o: x._get_current_object() % o
__divmod__ = lambda x, o: x._get_current_object().__divmod__(o)
__pow__ = lambda x, o: x._get_current_object() ** o
__lshift__ = lambda x, o: x._get_current_object() << o
__rshift__ = lambda x, o: x._get_current_object() >> o
__and__ = lambda x, o: x._get_current_object() & o
__xor__ = lambda x, o: x._get_current_object() ^ o
__or__ = lambda x, o: x._get_current_object() | o
__div__ = lambda x, o: x._get_current_object().__div__(o)
__truediv__ = lambda x, o: x._get_current_object().__truediv__(o)
__neg__ = lambda x: -(x._get_current_object())
__pos__ = lambda x: +(x._get_current_object())
__abs__ = lambda x: abs(x._get_current_object())
__invert__ = lambda x: ~(x._get_current_object())
__complex__ = lambda x: complex(x._get_current_object())
__int__ = lambda x: int(x._get_current_object())
__long__ = lambda x: long(x._get_current_object()) # noqa
__float__ = lambda x: float(x._get_current_object())
__oct__ = lambda x: oct(x._get_current_object())
__hex__ = lambda x: hex(x._get_current_object())
__index__ = lambda x: x._get_current_object().__index__()
__coerce__ = lambda x, o: x._get_current_object().__coerce__(x, o)
__enter__ = lambda x: x._get_current_object().__enter__()
__exit__ = lambda x, *a, **kw: x._get_current_object().__exit__(*a, **kw)
__radd__ = lambda x, o: o + x._get_current_object()
__rsub__ = lambda x, o: o - x._get_current_object()
__rmul__ = lambda x, o: o * x._get_current_object()
__rdiv__ = lambda x, o: o / x._get_current_object()
if PY2:
__rtruediv__ = lambda x, o: x._get_current_object().__rtruediv__(o)
else:
__rtruediv__ = __rdiv__
__rfloordiv__ = lambda x, o: o // x._get_current_object()
__rmod__ = lambda x, o: o % x._get_current_object()
__rdivmod__ = lambda x, o: x._get_current_object().__rdivmod__(o)
__copy__ = lambda x: copy.copy(x._get_current_object())
__deepcopy__ = lambda x, memo: copy.deepcopy(x._get_current_object(), memo)
基本概念
在Flask
中,每一次HTTP
請求的到來都會執行一些操作。
舉個例子,Django
裡面request
是通過形參的方式傳遞進檢視函式,這個很好實現,那麼Flask
中的request
則是通過匯入的方式作用於檢視函式,這意味著每次request
中的資料都要進行更新。
它是如何做到的呢?這個就是Flask
的精髓,上下文管理。
上面說過,Local
物件會例項化兩次:
_app_ctx_stack = LocalStack()
_request_ctx_stack = LocalStack()
它的實現原理是這樣的,每一次HTTP
請求來臨都會建立一個執行緒,Local
物件就會依照這個執行緒的pid
來構建出一個字典,這裡用掉的物件是_request_ctx_stack
,它內部有一個叫做__storage__
的變數,最終會搞成下面的資料格式:
{
pid001:{stack:[<app_ctx = RequestContext request,session]}, # 儲存request物件以及session
pid002:{stack:[<app_ctx = RequestContext request,session]},
}
而除開_request_ctx_stack
外還會有一個叫做_app_ctx_stack
的東西,它會存放當前Flask
例項app
以及一個g
物件:
{
pid001:{stack:[<app_ctx = flask.ctx.AppContext app,g>]},
pid002:{stack:[<app_ctx = flask.ctx.AppContext app,g>]},
}
每一次請求來的時候都會建立這樣的兩個字典,請求走的時候進行銷燬。
在每次匯入request/session
時都會從上面的這個__storage__
字典中,拿出當前執行緒對應的pid
中的request/session
,以達到更行的目的。
類 | 功能 |
---|---|
Local | 構建大字典 |
Localstack | 構建stack這個列表實現的棧 |
LocalProxy | 控制獲取stack列表中棧的資料,如匯入時引入request,怎麼樣將stack中的request拿出來 |
Flask流程之__call__
Flask
基本請求流程是建立在werkzeug
之上:
from werkzeug.wrappers import Request, Response
from werkzeug.serving import run_simple
@Request.application
def index(request):
return Response("Hello werkzeug")
if __name__ == '__main__':
run_simple("localhost", 5000, index)
可以看到,werkzeug
在開啟服務後,會執行一個叫run_simple
的函式,並且會呼叫被裝飾器包裝過後的index
函式。
也就意味著,在run_simple
傳參時,第三個引數形參名application
會加括號進行呼叫。
如果你傳入一個類,它將執行__init__
方法,如果你傳入一個例項物件,它將執行其類的__call__
方法。
如下所示:
from werkzeug.serving import run_simple
class Test:
def __init__(self,*args,**kwargs):
print("run init")
if __name__ == '__main__':
run_simple("localhost", 5000, Test)
# run init
示例二:
from werkzeug.serving import run_simple
class Test:
def __init__(self,*args,**kwargs):
super(Test, self).__init__(*args,**kwargs)
def __call__(self, *args, **kwargs):
print("run call")
test = Test()
if __name__ == '__main__':
run_simple("localhost", 5000, test)
# run call
OK,現在牢記一點,如果傳入的是函式,執行其類的__call__
。
接下來我們看Flask
程式:
from flask import Flask
app = Flask(__name__)
@app.route('/index')
def index():
return "index"
if __name__ == '__main__':
app.run()
當請求來時,會執行run
方法,我們朝裡看原始碼,直接拉到run
方法的下面,在run
方法中呼叫了run_simple
方法,其中的第三個引數就是當前的Flask
例項物件app
:
try:
run_simple(host, port, self, **options)
finally:
self._got_first_request = False
所以到了這裡,例項呼叫父類的__call__
(Flask
類本身),繼續看app.__call__
,例項本身沒有找其類的。
# app.__call__ 滑鼠左鍵點進去
def __call__(self, environ, start_response):
return self.wsgi_app(environ, start_response)
可以看見,在這裡它呼叫的是app.wsgi_app
方法,這也就能解釋Flask
中介軟體為什麼重寫下面這段程式碼。
app.wsgi_app = Middleware(app.wsgi_app) # 傳入要原本執行的wsgi_app
Flask流程之wsgi_app
原生的Flask.wsgi_app
中的程式碼是整個Flask
框架中的核心,如下所示:
def wsgi_app(self, environ, start_response):
# 引數 self就是Flask例項物件app,environ是werkzeug的原生HTTP請求物件
ctx = self.request_context(environ) # 對environ這個原生的請求物件進行封裝,封裝成一個request物件,可以擁有request.method/.args/.form等方法
error = None
try:
try:
# 上下文管理,將ctx放入Local大字典中,並且會更新session,從HTTP請求中拿出session,此外還會將當前例項app,也就是self放入Local的另一個大字典中,當然還有g物件
ctx.push()
# 執行檢視函式
response = self.full_dispatch_request()
# 捕獲異常
except Exception as e:
error = e
response = self.handle_exception(e)
except:
error = sys.exc_info()[1]
raise
# 返回請求
return response(environ, start_response)
finally:
if self.should_ignore_error(error):
error = None
# 清除上下文管理內容
ctx.auto_pop(error)
Flask流程之Resquest
看下面這一行程式碼,它其實是例項化一個物件,用於封裝environ
這個原始的HTTP
請求:
def wsgi_app(self, environ, start_response):
# self:app,也就是Flask例項
ctx = self.request_context(environ)
點開它,發現會返回一個例項物件:
def request_context(self, environ):
# self:app,也就是Flask例項
return RequestContext(self, environ) # 注意這裡傳參,__init__的第二個引數是app例項
去找它的__init__
方法:
class RequestContext(object):
def __init__(self, app, environ, request=None, session=None):
# self:ReuqetContext物件 app是Flask例項
self.app = app
if request is None:
request = app.request_class(environ) # 執行這裡、封裝request
self.request = request
self.url_adapter = None
try:
self.url_adapter = app.create_url_adapter(self.request) # 建立url與檢視函式對應關係,這裡不看
except HTTPException as e:
self.request.routing_exception = e
self.flashes = None
self.session = session # 注意 session 是一個None
先看上面的一句,封裝request
物件,由於self
是app
,所以找Flask
類中的request_class
,它是一個類屬性:
request_class = Request
加括號進行呼叫,並且傳遞了environ
,所以要進行例項化,找它的__init__
方法,發現是一個多繼承類:
class Request(RequestBase, JSONMixin):
# 該類本身未實現__init__,去找它的父類,從左到右找
找它的父類,RequestBase
,也是一個多繼承類:
class Request(
BaseRequest,
AcceptMixin,
ETagRequestMixin,
UserAgentMixin,
AuthorizationMixin,
CORSRequestMixin,
CommonRequestDescriptorsMixin,
):
# 該類本身未實現__init__,去找它的父類,從上到下
再繼續向上找,找BaseRequest
類:
class BaseRequest(object):
def __init__(self, environ, populate_request=True, shallow=False):
# self:Request,因為是Request物件要例項化
self.environ = environ
if populate_request and not shallow:
self.environ["werkzeug.request"] = self
self.shallow = shallow
# 該類還實現了args\form\files等方法
# 大體意思就是說呼叫這些方法的時候會將environ中的資料解析出來
然後將結果返回給ctx
,這個ctx
就是RequestContext
的例項物件,裡面有個Request
例項物件request
,還有個session
,不過是None
:
<ctx=RequestContext request,session=None>
Flask流程之ctx.push
接著往下看程式碼,記住現在的線索:
def wsgi_app(self, environ, start_response):
ctx = self.request_context(environ)
# <ctx=RequestContext request,session=None>
error = None
try:
try:
ctx.push() # 接下來著重看這裡
response = self.full_dispatch_request()
except Exception as e:
執行ctx.push
,這個方法可以說非常的繞。
def push(self):
# 引數:self是ctx,也就是 <ctx=RequestContext request,session=None>
# _request_ctx_stack = LocalStack() 全域性變數,已經做好了
top = _request_ctx_stack.top # None
if top is not None and top.preserved: # False 不走這裡
top.pop(top._preserved_exc)
# _app_ctx_stack = LocalStack() 全域性變數,已經做好了
app_ctx = _app_ctx_stack.top # None
if app_ctx is None or app_ctx.app != self.app: # 會走這裡,因為第一個條件成立
app_ctx = self.app.app_context() # 走這裡,實際上就是例項化
app_ctx.push()
self._implicit_app_ctx_stack.append(app_ctx)
else:
self._implicit_app_ctx_stack.append(None)
if hasattr(sys, "exc_clear"):
sys.exc_clear()
_request_ctx_stack.push(self)
if self.session is None:
session_interface = self.app.session_interface
self.session = session_interface.open_session(self.app, self.request)
if self.session is None:
self.session = session_interface.make_null_session(self.app)
if self.url_adapter is not None:
self.match_request()
現在來看 app_ctx
到底是個神馬玩意兒:
def app_context(self):
# self:Flask例項化物件,app
return AppContext(self)
繼續走:
class AppContext(object):
def __init__(self, app):
# self:AppContext的例項物件,app就是Flask的例項物件
self.app = app # AppContext例項物件的app就是Flask的例項物件
self.url_adapter = app.create_url_adapter(None)
self.g = app.app_ctx_globals_class() # 一個空的g物件
self._refcnt = 0
現在回來,第二個重要點來了:
app_ctx = _app_ctx_stack.top # None
if app_ctx is None or app_ctx.app != self.app: # 會走這裡,因為第一個條件成立
app_ctx = self.app.app_context() # <app_ctx = flask.ctx.AppContext app,g> app是當前Flask例項物件,g是一個空的玩意兒,詳細程式碼不用看了
app_ctx.push() # 又執行push
self._implicit_app_ctx_stack.append(app_ctx)
else:
self._implicit_app_ctx_stack.append(None)
Flask流程之app_ctx.push
這個push
是AppContext
中的push
:
def push(self):
# self: <app_ctx = flask.ctx.AppContext app,g>
self._refcnt += 1
if hasattr(sys, "exc_clear"):
sys.exc_clear()
_app_ctx_stack.push(self) # 存入
appcontext_pushed.send(self.app) # 這裡不看了
其實就是往Local
的大字典中進行存放,不過是通過LocalStack
這個類的push
方法:
def push(self, obj):
rv = getattr(self._local, "stack", None) # 觸發Local.__getattr__返回一個None
if rv is None: # 走這裡,觸發Local.__setattr__
self._local.stack = rv = []
rv.append(obj) # 直接存,觸發Local.__setattr__
return rv
資料結構:
{
pid001:{stack:[<app_ctx = flask.ctx.AppContext app,g>]},
}
然後返回app_ctx.push
:
app_ctx = _app_ctx_stack.top # None
if app_ctx is None or app_ctx.app != self.app: # 會走這裡,因為第一個條件成立
app_ctx = self.app.app_context() # <app_ctx = flask.ctx.AppContext app,g> app是當前Flask例項物件,g是一個空的玩意兒,詳細程式碼不用看了
app_ctx.push() # 存入成功
self._implicit_app_ctx_stack.append(app_ctx) # 走這裡了 ctx類RequestContext中有一個列表,把他存進來
else:
self._implicit_app_ctx_stack.append(None)
Flask流程之_request_ctx_stack.push
繼續向下看ctx.push
中的程式碼,這裡主要是封裝請求上下文:
def push(self):
# self:ctx物件 <ctx=RequestContext request,session=None>
# _request_ctx_stack = LocalStack() 全域性變數,已經做好了
top = _request_ctx_stack.top # None
if top is not None and top.preserved: # False 不走這裡
top.pop(top._preserved_exc)
# _app_ctx_stack = LocalStack() 全域性變數,已經做好了
app_ctx = _app_ctx_stack.top # None
if app_ctx is None or app_ctx.app != self.app: # 會走這裡,因為第一個條件成立
app_ctx = self.app.app_context() # 走這裡,實際上就是例項化
app_ctx.push() # 存入Local中
self._implicit_app_ctx_stack.append(app_ctx) # 存入ctx的列表中
else:
self._implicit_app_ctx_stack.append(None)
if hasattr(sys, "exc_clear"): # 不管
sys.exc_clear()
_request_ctx_stack.push(self) # 重點是這裡,和上面相同,又建立了一個字典。放進去,需要注意的是這裡是_request_ctx_stack.push,是另一個不同的Local例項化物件
"""
{
pid001:{stack:[<app_ctx = RequestContext request,session=None]},
}
"""
if self.session is None: # 設定session
session_interface = self.app.session_interface
self.session = session_interface.open_session(self.app, self.request) # 開啟session,從request中讀取出cookie然後進行load反序列化
if self.session is None:
self.session = session_interface.make_null_session(self.app)
"""
現在session就不是None了
{
pid001:{stack:[<app_ctx = RequestContext request,session]},
}
"""
if self.url_adapter is not None:
self.match_request()
Flask流程之session
儲存session
,從Cookie
中獲取資料,反序列化後儲存到session
中。
class SecureCookieSessionInterface(SessionInterface):
serializer = session_json_serializer
session_class = SecureCookieSession
def get_signing_serializer(self, app):
if not app.secret_key:
return None
signer_kwargs = dict(
key_derivation=self.key_derivation, digest_method=self.digest_method
)
return URLSafeTimedSerializer(
app.secret_key,
salt=self.salt,
serializer=self.serializer,
signer_kwargs=signer_kwargs,
)
def open_session(self, app, request):
s = self.get_signing_serializer(app)
if s is None:
return None
val = request.cookies.get(app.session_cookie_name)
if not val:
return self.session_class()
max_age = total_seconds(app.permanent_session_lifetime)
try:
data = s.loads(val, max_age=max_age)
return self.session_class(data)
except BadSignature:
return self.session_class()
def save_session(self, app, session, response):
domain = self.get_cookie_domain(app)
path = self.get_cookie_path(app)
# If the session is modified to be empty, remove the cookie.
# If the session is empty, return without setting the cookie.
if not session:
if session.modified:
response.delete_cookie(
app.session_cookie_name, domain=domain, path=path
)
return
# Add a "Vary: Cookie" header if the session was accessed at all.
if session.accessed:
response.vary.add("Cookie")
if not self.should_set_cookie(app, session):
return
httponly = self.get_cookie_httponly(app)
secure = self.get_cookie_secure(app)
samesite = self.get_cookie_samesite(app)
expires = self.get_expiration_time(app, session)
val = self.get_signing_serializer(app).dumps(dict(session))
response.set_cookie(
app.session_cookie_name,
val,
expires=expires,
httponly=httponly,
domain=domain,
path=path,
secure=secure,
samesite=samesite,
)
匯入原始碼
如果使用from flask import request
它會通過LocalProxy
去拿到Local
中的ctx
物件然後進行解析,拿到request
物件。
request = LocalProxy(partial(_lookup_req_object, "request")) # 例項化
在這裡可以看見例項化了一個LocalProxy
物件:
@implements_bool
class LocalProxy(object):
def __init__(self, local, name=None):
# local:偏函式
# name:None
object.__setattr__(self, "_LocalProxy__local", local) # self.__local = local 雙下開頭會改變名字
object.__setattr__(self, "__name__", name) # None
if callable(local) and not hasattr(local, "__release_local__"): # 執行,如果偏函式可執行,並且偏函式沒有屬性__release_local__時執行
object.__setattr__(self, "__wrapped__", local) # 當前例項增加屬性,指向偏函式
當要使用request.method
時,觸發LocalProxy
的__getattr__
方法:
def __getattr__(self, name):
# name:method
if name == "__members__":
return dir(self._get_current_object())
return getattr(self._get_current_object(), name) # 執行這裡,name = method
在_get_current_object
中執行self.__local()
:
def _get_current_object(self):
if not hasattr(self.__local, "__release_local__"):
return self.__local() # self.__local就是偏函式,偏函式自動傳參。request
try:
return getattr(self.__local, self.__name__)
except AttributeError:
raise RuntimeError("no object bound to %s" % self.__name__)
偏函式,第二個引數是request
,也就是說_lookup_req_object
的引數預設就是request
:
def _lookup_req_object(name):
# name:request字串
top = _request_ctx_stack.top # 返回RequestContext物件,裡面有request和session
if top is None:
raise RuntimeError(_request_ctx_err_msg)
return getattr(top, name) # 返回的就是request物件,從RequestContext物件中拿到request物件
拿到request
物件後繼續看__getattr__
方法,獲取method
:
def __getattr__(self, name):
# name:method
if name == "__members__":
return dir(self._get_current_object())
return getattr(self._get_current_object(), name) # 執行這裡,name = method
Flask流程之pop
wsgi_app
返回和清棧還沒有看:
def wsgi_app(self, environ, start_response):
ctx = self.request_context(environ) # 封裝request、session到RequestContext物件
error = None
try:
try:
ctx.push() # 封裝app上下文和請求上下文,填充session內容
response = self.full_dispatch_request() # 執行檢視函式
except Exception as e:
error = e
response = self.handle_exception(e)
except: # noqa: B001
error = sys.exc_info()[1]
raise
return response(environ, start_response) # 封裝返回物件
finally:
if self.should_ignore_error(error): # 清除上下文,兩個字典中的內容
error = None
ctx.auto_pop(error)
最主要就是看清棧:
def auto_pop(self, exc):
if self.request.environ.get("flask._preserve_context") or (
exc is not None and self.app.preserve_context_on_exception
):
self.preserved = True
self._preserved_exc = exc
else:
self.pop(exc) # 看這裡就行了
接著看pop
方法:
def pop(self, exc=_sentinel):
app_ctx = self._implicit_app_ctx_stack.pop() # 清除Flask例項中存放的app和g物件, [<app_ctx = flask.ctx.AppContext app,g>]
try:
clear_request = False
if not self._implicit_app_ctx_stack:
# 這裡都會執行,但是不看
self.preserved = False
self._preserved_exc = None
if exc is _sentinel:
exc = sys.exc_info()[1]
self.app.do_teardown_request(exc)
if hasattr(sys, "exc_clear"):
sys.exc_clear()
request_close = getattr(self.request, "close", None)
if request_close is not None:
request_close()
clear_request = True
finally:
# 清除請求上下文中的資料
rv = _request_ctx_stack.pop() # # LocalProxy.pop()
# 修改request中的werkzeug.request為None,本身是Request物件本身
if clear_request:
rv.request.environ["werkzeug.request"] = None
# 清除應用上下文中的資料
if app_ctx is not None:
app_ctx.pop(exc) # LocalProxy.pop()
assert rv is self, "Popped wrong request context. (%r instead of %r)" % (
rv,
self,
)
LocalProxy.pop
中的程式碼:
def pop(self):
stack = getattr(self._local, "stack", None)
if stack is None:
return None
elif len(stack) == 1:
release_local(self._local) # Local.__storage__.pop(self.__ident_func__(), None)
return stack[-1] # 如果只剩下一個,就返回
else:
return stack.pop() # 清理乾淨 {pid:{"statck":[]}}
Flask流程之before_request
before_request
實現挺簡單的,用一個列表,將所有被裝飾函式放進來。再執行檢視函式之前把列表中所有被berore_request
裝飾的函式先執行一遍:
@setupmethod
def before_request(self, f):
self:app,Flask例項
f:被裝飾函式
self.before_request_funcs.setdefault(None, []).append(f) # 從與i個字典中獲取None,如果沒獲取到就是一個空列表,後面使用了append代表None對應的k就是一個列表。
# 這句話的意思就是說,從一個字典中{None:[func,func,func]}出一個列表,獲取不到就建立一個空列表{None:[]},並且把被裝飾的函式f新增進去
return f
在wsgi_app
中檢視原始碼:
# 檢視執行檢視函式這一句
response = self.full_dispatch_request()
點進去看,執行檢視函式前發生了什麼:
def full_dispatch_request(self):
self.try_trigger_before_first_request_functions() # 先執行這berfor_first_request裝飾的函式
try:
request_started.send(self)
rv = self.preprocess_request() # 在執行berfor_first_request裝飾的函式
if rv is None:
rv = self.dispatch_request() # 開始執行檢視函式,rv=返回值
except Exception as e:
rv = self.handle_user_exception(e)
return self.finalize_request(rv)
關鍵程式碼:
def preprocess_request(self):
# self:app,Flask例項物件
bp = _request_ctx_stack.top.request.blueprint # 獲取藍圖
funcs = self.url_value_preprocessors.get(None, ())
if bp is not None and bp in self.url_value_preprocessors:
funcs = chain(funcs, self.url_value_preprocessors[bp])
for func in funcs:
func(request.endpoint, request.view_args)
funcs = self.before_request_funcs.get(None, ()) # 獲取列表,[func1,func2],key是None
if bp is not None and bp in self.before_request_funcs: # 如果藍圖存在,將藍圖的全域性before_request也新增到列表中
funcs = chain(funcs, self.before_request_funcs[bp])
for func in funcs: # 運河,執行
rv = func()
if rv is not None: # 返回值如果不是None就攔截
return rv
而使用after_request
裝上後的函式也會被放到一個列表中,其他的具體實現都差不多:
@setupmethod
def after_request(self, f):
self:app,Flask例項
f:被裝飾函式
self.after_request_funcs.setdefault(None, []).append(f)
return f
原始碼流程圖
g的作用
一次請求流程中的一些共同資料,可以用g
進行儲存:
from flask import Flask,g
app = Flask(__name__)
@app.before_request
def func():
g.message = "僅當次請求有效"
@app.route('/index')
def index():
print(g.message)
return "index"
if __name__ == '__main__':
app.run()
current_app的作用
可以匯入當前的配置:
from flask import Flask,current_app
app = Flask(__name__)
@app.before_request
def func():
if current_app.debug == False:
current_app.debug = True
print("修改成功")
@app.route('/index')
def index():
return "index"
if __name__ == '__main__':
app.run()
WTforms
wtforms
類似於Django
中的forms
元件,用於資料驗證和生成HTML
官方文件:https://wtforms.readthedocs.io/en/stable/index.html#
基本使用
首先進行安裝:
pip3 install wtforms
一個簡單的註冊示例:
from flask import Flask
from flask import request
from flask import render_template
from flask import Markup
from wtforms import Form # 必須繼承
from wtforms import validators # 自定義認證器
from wtforms import widgets # HTML生成外掛
from wtforms import fields # 欄位匯入
class LoginForm(Form):
name = fields.StringField(
label="使用者名稱",
widget=widgets.TextInput(),
render_kw={"class": "form-control"},
validators=[
validators.DataRequired(message="使用者名稱不能為空"),
validators.Length(max=8, min=3, message="使用者名稱長度必須大於%(max)d且小於%(min)d")
]
)
pwd = fields.PasswordField(
label="密碼",
widget=widgets.PasswordInput(),
render_kw={"class": "form-control",},
validators=[
validators.DataRequired(message="密碼不能為空"),
validators.Length(max=18, min=4, message="密碼長度必須大於%(max)d且小於%(min)d"),
validators.Regexp(regex="\d+", message="密碼必須是數字"),
]
)
app = Flask(__name__,template_folder="templates")
@app.route('/login',methods=["GET","POST"])
def login():
if request.method == "GET":
form = LoginForm()
return render_template("login.html",**{"form":form})
form = LoginForm(formdata=request.form)
if form.validate():
print("使用者提交的資料用過格式驗證,值為:%s" % form.data)
return "登入成功"
else:
print(form.errors, "錯誤資訊")
return render_template("login.html", **{"form":form})
if __name__ == '__main__':
app.run()
前端渲染:
<form method="POST" novalidate>
{% for item in form %}
<p> {{item.label}}:{{item}}</p>
<p style="color: red">{{item.errors[0]}}</p>
{% endfor %}
<p>
<button type="submit">提交</button>
</p>
</form>
Form例項化
以下是Form
類的例項化引數:
引數 | 描述 |
---|---|
formdata | 需要被驗證的form表單資料 |
obj | 如果formdata為空或未提供,則檢查此物件的屬性是否與表單欄位名稱匹配,這些屬性將用於欄位值 |
prefix | 欄位字首匹配,當傳入該引數時,所有驗證欄位必須以這個開頭(無太大意義) |
data | 當formdata引數和obj引數都有時候,可以使用該引數傳入字典格式的待驗證資料或者生成html的預設值,列如:{'usernam':'admin’} |
meta | 用於覆蓋當前已經定義的form類的meta配置,引數格式為字典 |
使用data
來構建預設值,常用於文章編輯等,需要填入原本資料庫中查詢出的文章。
下面用預設使用者做演示:
def login():
if request.method == "GET":
form = LoginForm(data={"name":"預設使用者"})
return render_template("login.html",**{"form":form})
欄位介紹
欄位的繼承,fields
是常用類,它繼承了其他的一些類:
from wtforms.fields.core import *
from wtforms.fields.simple import *
from wtforms.fields.core import Label, Field, SelectFieldBase, Flags
from wtforms.utils import unset_value as _unset_value
一般都欄位都會預設生成一種HTML
標籤,但是也可以通過widget
進行更改,下面是常用的一些欄位:
欄位型別 | 描述 |
---|---|
StringField | 文字欄位, 相當於type型別為text的input標籤 |
TextAreaField | 多行文字欄位 |
PasswordField | 密碼文字欄位 |
HiddenField | 隱藏文字欄位 |
DateField | 文字欄位, 值為datetime.date格式 |
DateTimeField | 文字欄位, 值為datetime.datetime格式 |
IntegerField | 文字欄位, 值為整數 |
DecimalField | 文字欄位, 值為decimal.Decimal |
FloatField | 文字欄位, 值為浮點數 |
BooleanField | 核取方塊, 值為True 和 False |
RadioField | 一組單選框 |
SelectField | 下拉選單 |
SelectMultipleField | 下拉選單, 可選擇多個值 |
FileField | 檔案上傳欄位 |
SubmitField | 表單提交按鈕 |
FormFiled | 把表單作為欄位嵌入另一個表單 |
FieldList | 子組指定型別的欄位 |
每個欄位可以為其配置一些額外的屬性,如下所示:
欄位屬性 | 描述 |
---|---|
label | 欄位別名 |
validators | 驗證規則列表 |
filters | 過濾器列表 |
description | 欄位詳細描述 |
default | 預設值 |
widget | 自定義外掛,替換預設生成的HTML標籤 |
render_kw | 為生成的HTML標籤配置屬性 |
choices | 核取方塊的型別 |
如下所示:
class FormLearn(Form):
field = fields.StringField(
label="測試欄位",
widget=widgets.TextInput(), # 使用外掛,代替預設生成標籤
validators=[
validators.DataRequired(message="必填"),
validators.Length(max=12,min=3,message="長度驗證"),
],
description="這是一個測試欄位",
default="預設值",
render_kw={"style":"width:60px"}, # 設定鍵值對
)
內建驗證
使用validators
為欄位進行驗證時,可指定如下的內建驗證規則:
驗證函式 | 說明 |
---|---|
驗證電子郵件地址 | |
EqualTo | 比較兩個欄位的值,常用於要求輸入兩次密碼進行確認的情況 |
IPAddress | 驗證IPv4網路地址 |
Length | 驗證輸入字串的長度 |
NumberRange | 驗證輸入的值在數字範圍內 |
Optional | 無輸入值時跳過其他驗證函式 |
DataRequired | 確保欄位中有資料 |
Regexp | 使用正規表示式驗證輸入值 |
URL | 驗證URL |
AnyOf | 確保輸入值在可選值列表中 |
NoneOf | 確保輸入值不在可選列表中 |
EqualTo
是常用的驗證方式,驗證兩次密碼是否輸入一致:
class FormLearn(Form):
password = fields.StringField(
label="使用者密碼",
widget=widgets.PasswordInput(),
validators=[
validators.DataRequired(message="必填"),
validators.Length(min=8, max=16, message="必須小於8位大於16位")
],
)
re_password = fields.StringField(
label="密碼驗證",
widget=widgets.PasswordInput(),
validators=[
validators.EqualTo(fieldname="password", message="兩次密碼輸入不一致", )
],
render_kw={"placeholder": "重新輸入密碼"},
)
Meta配置
Meta
主要用於自定義wtforms
的功能,用的比較少,大多都是配置選項,以下是配置引數:
from wtforms import Form # 必須繼承
from wtforms import fields # 欄位匯入
from wtforms.csrf.core import CSRF # 自帶的CSRF驗證和生成
from hashlib import md5 # 加密
class MyCSRF(CSRF):
def setup_form(self, form):
self.csrf_context = form.meta.csrf_context()
self.csrf_secret = form.meta.csrf_secret
return super(MyCSRF, self).setup_form(form)
def generate_csrf_token(self, csrf_token):
gid = self.csrf_secret + self.csrf_context
token = md5(gid.encode('utf-8')).hexdigest()
return token
def validate_csrf_token(self, form, field):
if field.data != field.current_token:
raise ValueError('Invalid CSRF')
class FormLearn(Form):
username = fields.StringField(label="使用者名稱")
password = fields.PasswordField(label="密碼")
class Meta:
# CSRF相關
csrf = False # 是否自動生成CSRF標籤
csrf_field_name = "csrf_token" # 生成的CSRF標籤名字
csrf_secret = "2d728321*fd&" # 自動生成標籤的值,加密用csrf_context
csrf_context = lambda x:request.url
csrf_class = MyCSRF # 生成和比較的CSRF標籤
# 其他配置
locales = ('zh', 'en') # 是否支援本地化 locales = False
cache_translations = True # 是否對本地化進行快取
translations_cache = {} # 儲存本地化快取資訊的欄位
鉤子函式
一般都用區域性鉤子:
class FormLearn(Form):
username = fields.StringField(label="使用者名稱")
password = fields.PasswordField(label="密碼")
def validate_username(self,obj):
"""
:param obj: 欄位物件,屬性data就是使用者輸入的內容
:return: 如果不進行返回,則預設返回obj物件
"""
if len(obj.data) < 6:
# raise validators.ValidationError("使用者名稱太短了") # 繼續後續驗證
raise validators.StopValidation("使用者名稱太短了") # 不再繼續後續驗證
return obj
def validate_password(self,obj):
print("區域性鉤子")
return obj
自定義驗證
自定義驗證規則:
from wtforms import validators
class ValidatorsRule(object):
"""自定義驗證規則"""
def __call__(self, form, field):
"""
:param form:
:param field: 使用field.data,取出使用者輸入的資訊
:return: 當return是None,則驗證通過
"""
import re
error_char = re.search(r"\W", field.data).group(0) # 取出第一個匹配結果
if error_char:
raise validators.StopValidation("提交資料含有特殊字元,如:%s"%error_char") # 不再繼續後續驗證
# raise validators.ValidationError("提交資料含有特殊字元,如:%s"%error_char) # 繼續後續驗證
使用:
class LoginForm(Form):
name = simple.StringField(
label="使用者名稱",
widget=widgets.TextInput(),
render_kw={"class": "form-control"},
validators=[
validators.DataRequired(message="使用者名稱不能為空"),
validators.Length(max=8, min=3, message="使用者名稱長度必須大於%(max)d且小於%(min)d"),
ValidatorsRule(), # 使用自定義驗證
]
)
外掛大全
以下程式碼包含所有可能用到的外掛:
from flask import Flask,render_template,redirect,request
from wtforms import Form
from wtforms.fields import core
from wtforms.fields import html5
from wtforms.fields import simple
from wtforms import validators
from wtforms import widgets
app = Flask(__name__,template_folder="templates")
app.debug = True
=======================simple===========================
class RegisterForm(Form):
name = simple.StringField(
label="使用者名稱",
validators=[
validators.DataRequired()
],
widget=widgets.TextInput(),
render_kw={"class":"form-control"},
default="wd"
)
pwd = simple.PasswordField(
label="密碼",
validators=[
validators.DataRequired(message="密碼不能為空")
]
)
pwd_confim = simple.PasswordField(
label="重複密碼",
validators=[
validators.DataRequired(message='重複密碼不能為空.'),
validators.EqualTo('pwd',message="兩次密碼不一致")
],
widget=widgets.PasswordInput(),
render_kw={'class': 'form-control'}
)
========================html5============================
email = html5.EmailField( #注意這裡用的是html5.EmailField
label='郵箱',
validators=[
validators.DataRequired(message='郵箱不能為空.'),
validators.Email(message='郵箱格式錯誤')
],
widget=widgets.TextInput(input_type='email'),
render_kw={'class': 'form-control'}
)
===================以下是用core來呼叫的=======================
gender = core.RadioField(
label="性別",
choices=(
(1,"男"),
(1,"女"),
),
coerce=int # 傳入時自動轉換位int型別,否則是str型別
)
city = core.SelectField(
label="城市",
choices=(
("bj","北京"),
("sh","上海"),
)
)
hobby = core.SelectMultipleField(
label='愛好',
choices=(
(1, '籃球'),
(2, '足球'),
),
coerce=int
)
favor = core.SelectMultipleField(
label="喜好",
choices=(
(1, '籃球'),
(2, '足球'),
),
widget = widgets.ListWidget(prefix_label=False),
option_widget = widgets.CheckboxInput(),
coerce = int,
default = [1, 2]
)
def __init__(self,*args,**kwargs): #這裡的self是一個RegisterForm物件
'''
解決資料庫不及時更新的問題
重寫__init__方法
'''
super(RegisterForm,self).__init__(*args, **kwargs) #繼承父類的init方法
self.favor.choices =((1, '籃球'), (2, '足球'), (3, '羽毛球')) #把RegisterForm這個類裡面的favor重新賦值,實現動態改變核取方塊中的選項
def validate_pwd_confim(self,field,):
'''
自定義pwd_config欄位規則,例:與pwd欄位是否一致
:param field:
:return:
'''
# 最開始初始化時,self.data中已經有所有的值
if field.data != self.data['pwd']:
# raise validators.ValidationError("密碼不一致") # 繼續後續驗證
raise validators.StopValidation("密碼不一致") # 不再繼續後續驗證
@app.route('/register',methods=["GET","POST"])
def register():
if request.method=="GET":
form = RegisterForm(data={'gender': 1}) #預設是1,
return render_template("register.html",form=form)
else:
form = RegisterForm(formdata=request.form)
if form.validate(): #判斷是否驗證成功
print('使用者提交資料通過格式驗證,提交的值為:', form.data) #所有的正確資訊
else:
print(form.errors) #所有的錯誤資訊
return render_template('register.html', form=form)
if __name__ == '__main__':
app.run()
原始碼解析
Flask-session
、通過第三方外掛Flask-session
能夠將session
存放至其他地方,而不是隻能存放在記憶體中:
pip install Flask-session
使用的時候:
from flask import Flask
from flask import session
from flask_session import Session
import redis
app = Flask(__name__)
app.config["SESSION_TYPE"] = "redis"
app.config["SESSION_REDIS"] = redis.Redis(host="127.0.0.1",port=6379,password="") # 有密碼就填上
app.config["SESSION_KEY_PREFIX"] = "session" # 字首
Session(app) # 修改flask的預設session介面
@app.route('/set')
def set():
session["key"] = "value"
return "ok"
@app.route('/get')
def get():
res = session.get("key")
return res
if __name__ == '__main__':
app.run()
原理也很簡單,替換掉了預設的session
介面,與Django
的redis
快取差不多。
Flask-SQLALchemy
Flask-SQLALchemy
是SQLALchemy
與Flask
之間的粘合劑。讓Flask
與SQLALchemy
之間的關係更為緊密:
pip install flask-sqlalchemy
使用Flask-SQLALchemy
也非常簡單,首先是建立專案:
- mysite # 專案根目錄
- mysite # 包
- static
- templates
- views
index.py
- __init__.py
- models.py
- manage.py
- settings.py
程式碼如下,做主藍圖,使用flaskSQLAlchemy
模組:
# __init__.py
from flask import Flask
from flask_session import Session
from flask_sqlalchemy import SQLAlchemy
# 第一步,例項化
db = SQLAlchemy() # db包含了所有需要的東西,如commit,remove,Base類等
from .models import * # 匯入模型類
from .views.index import index # 防止迴圈匯入
def create_app():
app = Flask(import_name=__name__, template_folder='../templates', static_folder='../static',
static_url_path='/static')
# 載入配置檔案
app.config.from_pyfile("../settings.py")
# 將db註冊到app中,必須在註冊藍圖之前
db.init_app(app)
# 配置Session
Session(app)
# 註冊藍圖
app.register_blueprint(index, url_prefix="/index/") # 註冊藍圖物件 index
return app
settings.py
中的配置項:
import redis
# sqlalchemy相關配置
SQLALCHEMY_DATABASE_URI = "mysql+pymysql://root@127.0.0.1:3306/db2?charset=utf8"
SQLALCHEMY_POOL_SIZE = 5 # 連結池的連線數量
SQLALCHEMY_POOL_TIMEOUT = 10 # 連結池連線超時時間
SQLALCHEMY_POOL_RECYCLE = 60*60*4 # 關閉連線的時間:預設Mysql是2小時
SQLALCHEMY_MAX_OVERFLOW = 3 # 控制在連線池達到最大值後可以建立的連線數。當這些額外的連線回收到連線池後將會被斷開和拋棄
SQLALCHEMY_TRACK_MODIFICATIONS = False # 追蹤物件的修改並且傳送訊號
# session相關配置
SESSION_TYPE = 'redis' # session型別為redis
SESSION_KEY_PREFIX = 'session:' # 儲存到session中的值的字首
SESSION_PERMANENT = True # 如果設定為False,則關閉瀏覽器session就失效。
SESSION_USE_SIGNER = False # 是否對傳送到瀏覽器上 session:cookie值進行加密
SESSION_REDIS = redis.Redis(host="127.0.0.1",port=6379,password="")
然後是書寫模型類:
# models.py
from . import db
class UserProfile(db.Model): # 必須繼承Base
__tablename__ = 'userprofile'
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64), unique=True, nullable=False)
password = db.Column(db.String(64), unique=True, nullable=False)
email = db.Column(db.String(128), unique=True, nullable=False)
def __repr__(self):
return '<user %s>' % self.username
檢視:
# index.py
from flask import Blueprint
from .. import db
from .. import models
index = Blueprint("index", __name__)
@index.route("/model")
def model():
import uuid
db.session.add( # 使用session新增資料
models.UserProfile(
username="user%s" % (uuid.uuid4()),
password="password%s" % str(uuid.uuid4()),
email="%s@gamil.com" % str(uuid.uuid4())
)
)
db.session.commit() # 提交
result = db.session.query(models.UserProfile).all() # 查詢資料
db.session.remove() # 關閉
print(result)
return "ok"
啟動檔案:
# manage.py
from mysite import create_app
from mysite import db
if __name__ == '__main__':
app = create_app()
with app.app_context(): # 執行指令碼建立資料庫表
# db.drop_all()
db.create_all()
app.run()
Flask-Script
該外掛的作用通過指令碼的形式啟動Flask
專案,同時還具有自定義指令碼命令的功能。
下載安裝:
pip install flask-script
在啟動檔案中進行使用:
from flask_script import Manager
from mysite import create_app
from mysite import db
if __name__ == '__main__':
app = create_app()
manager = Manager(app) # 註冊
with app.app_context(): # 執行指令碼建立資料庫表
# db.drop_all()
db.create_all()
manager.run() # 使用manager.run啟動flask專案
啟動命令:
python manage.py runserver -h 127.0.0.1 -p 5000
你可以自定義一些啟動指令碼,如下所示:
@manager.command
def cmd(args):
print(args)
# 命令:python manage.py cmd 12345
# 結果:12345
也可以使用關鍵字傳參的方式,進行指令碼的啟動:
@manager.option('-n', '--name', dest='name')
@manager.option('-u', '--url', dest='url')
def cmd(name, url):
print(name, url)
# 命令:python manage.py cmd -n test -u www.xxx.com
# 結果:test www.xxx.com
自定義指令碼可以配置是否建立資料庫,配合Flask-SQLAlchemy
,如下所示:
from flask_script import Manager
from mysite import create_app
from mysite import db
if __name__ == '__main__':
app = create_app()
manager = Manager(app) # 註冊
@manager.command
def create_tables():
with app.app_context(): # 執行指令碼建立資料庫表
# db.drop_all()
db.create_all()
manager.run() # 使用manager.run啟動flask專案
# 命令:python manage.py create_tables
Flask-Migrate
該外掛的作用類似於Django
中對model
的命令列操作,由於原生Flask-SQLALchemy
不支援表結構的修改,所以用該外掛的命令列來彌補。
值得一提的是,該外掛依賴於Flask-Script
:
pip install flask-migrate
在啟動檔案中進行匯入:
from flask_script import Manager
from flask_migrate import Migrate, MigrateCommand
from mysite import create_app
if __name__ == '__main__':
app = create_app()
manager = Manager(app) # 註冊 flask-scripts元件
Migrate(app) # 註冊 flask-migrate元件
manager.add_command("db",MigrateCommand)
manager.run() # 使用manager.run啟動flask專案
命令列:
命令 | 描述 |
---|---|
python 啟動檔案.py db init | 初始化model |
python 啟動檔案.py db migrate | 型別於makemigrations,生成模型類 |
python 啟動檔案.py db upgrade | 類似於migrate,將模型類對映到物理表中 |
在第一次使用時,三條命令都敲一遍。
如果修改了表結構,只用敲第二條,第三條命令即可,彌補Flask-SQLALchemy
不能修改表結構的缺點。
最後記錄
一個完整基礎的Flask
專案基礎架構:
- mysite # 專案根目錄
- mysite # 包
- static
- templates
- views
index.py
- __init__.py
- models.py
- manage.py
- settings.py
# __init__.py
from flask import Flask
from flask_session import Session
from flask_sqlalchemy import SQLAlchemy
# 第一步,例項化
db = SQLAlchemy()
from .models import *
from .views.index import index # 防止迴圈匯入
def create_app():
app = Flask(import_name=__name__, template_folder='../templates', static_folder='../static',
static_url_path='/static')
# 載入配置檔案
app.config.from_pyfile("../settings.py")
# 將db註冊到app中,必須在註冊藍圖之前
db.init_app(app)
# 配置Session
Session(app)
# 註冊藍圖
app.register_blueprint(index, url_prefix="/index/") # 註冊藍圖物件 index
return app
# models.py
from . import db
class UserProfile(db.Model):
__tablename__ = 'userprofile'
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64), unique=True, nullable=False)
password = db.Column(db.String(64), unique=True, nullable=False)
# email = db.Column(db.String(128), unique=True, nullable=False)
def __repr__(self):
return '<user %s>' % self.username
# manage.py
from flask_script import Manager
from flask_migrate import Migrate, MigrateCommand
from mysite import create_app
from mysite import db
if __name__ == '__main__':
app = create_app()
manager = Manager(app) # 註冊 flask-scripts元件
Migrate(app,db) # 註冊 flask-migrate元件
manager.add_command("db",MigrateCommand)
manager.run() # 使用manager.run啟動flask專案
# settings.py
import redis
# sqlalchemy相關配置
SQLALCHEMY_DATABASE_URI = "mysql+pymysql://root@127.0.0.1:3306/db2?charset=utf8"
SQLALCHEMY_POOL_SIZE = 5 # 連結池的連線數量
SQLALCHEMY_POOL_TIMEOUT = 10 # 連結池連線超時時間
SQLALCHEMY_POOL_RECYCLE = 60*60*4 # 關閉連線的時間:預設Mysql是2小時
SQLALCHEMY_MAX_OVERFLOW = 3 # 控制在連線池達到最大值後可以建立的連線數。當這些額外的連線回收到連線池後將會被斷開和拋棄
SQLALCHEMY_TRACK_MODIFICATIONS = False # 追蹤物件的修改並且傳送訊號
# session相關配置
SESSION_TYPE = 'redis' # session型別為redis
SESSION_KEY_PREFIX = 'session:' # 儲存到session中的值的字首
SESSION_PERMANENT = True # 如果設定為False,則關閉瀏覽器session就失效。
SESSION_USE_SIGNER = False # 是否對傳送到瀏覽器上 session:cookie值進行加密
SESSION_REDIS = redis.Redis(host="127.0.0.1",port=6379,password="")
# index.py
from flask import Blueprint
from .. import db
from .. import models
index = Blueprint("index", __name__)
@index.route("/model")
def model():
# 插入資料
import uuid
db.session.add(
models.UserProfile(
username="user%s" % (uuid.uuid4()),
password="password%s" % str(uuid.uuid4()),
email="%s@gamil.com" % str(uuid.uuid4())
)
)
db.session.commit() # 提交
result = db.session.query(models.UserProfile).all()
db.session.remove() # 關閉
print(result)
return "ok"