Flask基礎全套

雲崖先生發表於2020-12-30

Flask簡介

   Flask是主流PythonWeb三大框架之一,其特點是短小精悍以及功能強大從而獲得眾多Pythoner的追捧,相比於Django它更加簡單更易上手,Flask擁有非常強大的三方庫,提供各式各樣的模組對其本身進行擴充:

   Flask擴充套件模組

   下面是FlaskDjango本身的一些區別:

 FlaskDjango
閘道器介面(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()

檢視

請求相關

   Flaskrequest物件不是通過引數傳遞,而是通過匯入:

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簡介

   jinja2Flask中預設的模板語言,相比於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>

分支迴圈

   iffor都用{% %}進行包裹,與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

   請求走了就會觸發,類似於Djangoprocess_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的作用就是為了將功能和主服務分開。

   說的直白點就是構建專案目錄,劃分內建的裝飾器作用域等,類似於Djangoapp的概念。

  

小型專案

   下面有一個基本的專案目錄,如下所示:

- 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相似,讓每個藍圖物件都擁有自己的templatesstatic

- 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物件,由於selfapp,所以找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

   這個pushAppContext中的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

原始碼流程圖

   image-20201213233611182

   img

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})

   image-20201216191335472

欄位介紹

   欄位的繼承,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為欄位進行驗證時,可指定如下的內建驗證規則:

驗證函式說明
Email 驗證電子郵件地址
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介面,與Djangoredis快取差不多。

Flask-SQLALchemy

   Flask-SQLALchemySQLALchemyFlask之間的粘合劑。讓FlaskSQLALchemy之間的關係更為緊密:

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"

相關文章