基於類別的視圖

本頁介紹如何使用 ViewMethodView 類別來編寫基於類別的視圖。

基於類別的視圖是一個充當視圖函數的類別。由於它是一個類別,因此可以使用不同的參數創建該類別的不同實例,以更改視圖的行為。這也稱為通用、可重用或可插拔的視圖。

一個有用的例子是定義一個類別,該類別基於它初始化的資料庫模型創建一個 API。

對於更複雜的 API 行為和自訂,請研究 Flask 的各種 API 擴展。

基本可重用視圖

讓我們逐步瀏覽一個將視圖函數轉換為視圖類別的例子。我們從一個查詢使用者列表,然後渲染範本以顯示列表的視圖函數開始。

@app.route("/users/")
def user_list():
    users = User.query.all()
    return render_template("users.html", users=users)

這適用於使用者模型,但是假設您還有更多模型需要列表頁面。您需要為每個模型編寫另一個視圖函數,即使唯一會改變的是模型和範本名稱。

相反,您可以編寫一個 View 子類別,它將查詢模型並渲染範本。作為第一步,我們將把視圖轉換為一個沒有任何自訂的類別。

from flask.views import View

class UserList(View):
    def dispatch_request(self):
        users = User.query.all()
        return render_template("users.html", objects=users)

app.add_url_rule("/users/", view_func=UserList.as_view("user_list"))

View.dispatch_request() 方法相當於視圖函數。調用 View.as_view() 方法將創建一個視圖函數,可以使用 add_url_rule() 方法在應用程式上註冊。as_view 的第一個參數是用於使用 url_for() 引用視圖的名稱。

注意

您不能像使用基本視圖函數那樣使用 @app.route() 修飾類別。

接下來,我們需要能夠為不同的模型和範本註冊相同的視圖類別,使其比原始函數更有用。該類別將接受兩個參數,模型和範本,並將它們存儲在 self 上。然後 dispatch_request 可以引用這些,而不是硬編碼的值。

class ListView(View):
    def __init__(self, model, template):
        self.model = model
        self.template = template

    def dispatch_request(self):
        items = self.model.query.all()
        return render_template(self.template, items=items)

請記住,我們使用 View.as_view() 創建視圖函數,而不是直接創建類別。傳遞給 as_view 的任何額外參數都會在創建類別時傳遞。現在我們可以註冊相同的視圖來處理多個模型。

app.add_url_rule(
    "/users/",
    view_func=ListView.as_view("user_list", User, "users.html"),
)
app.add_url_rule(
    "/stories/",
    view_func=ListView.as_view("story_list", Story, "stories.html"),
)

URL 變數

URL 捕獲的任何變數都會作為關鍵字參數傳遞給 dispatch_request 方法,就像它們對於常規視圖函數一樣。

class DetailView(View):
    def __init__(self, model):
        self.model = model
        self.template = f"{model.__name__.lower()}/detail.html"

    def dispatch_request(self, id)
        item = self.model.query.get_or_404(id)
        return render_template(self.template, item=item)

app.add_url_rule(
    "/users/<int:id>",
    view_func=DetailView.as_view("user_detail", User)
)

視圖生命週期和 self

預設情況下,每次處理請求時都會創建視圖類別的新實例。這意味著在請求期間將其他資料寫入 self 是安全的,因為下一個請求將看不到它,這與其他形式的全局狀態不同。

但是,如果您的視圖類別需要執行大量複雜的初始化,則為每個請求執行它是沒有必要的,並且可能效率低下。為了避免這種情況,請將 View.init_every_request 設置為 False,這將只創建一個類別實例並將其用於每個請求。在這種情況下,寫入 self 是不安全的。如果您需要在請求期間存儲資料,請改用 g

ListView 示例中,在請求期間沒有任何東西寫入 self,因此創建單個實例更有效。

class ListView(View):
    init_every_request = False

    def __init__(self, model, template):
        self.model = model
        self.template = template

    def dispatch_request(self):
        items = self.model.query.all()
        return render_template(self.template, items=items)

對於每個 as_view 調用,仍然會創建不同的實例,但不會為對這些視圖的每個請求創建。

視圖裝飾器

視圖類別本身不是視圖函數。視圖裝飾器需要應用於 as_view 返回的視圖函數,而不是類別本身。將 View.decorators 設置為要應用的裝飾器列表。

class UserList(View):
    decorators = [cache(minutes=2), login_required]

app.add_url_rule('/users/', view_func=UserList.as_view())

如果您沒有設置 decorators,您可以改為手動應用它們。這相當於

view = UserList.as_view("users_list")
view = cache(minutes=2)(view)
view = login_required(view)
app.add_url_rule('/users/', view_func=view)

請記住,順序很重要。如果您習慣了 @decorator 風格,這相當於

@app.route("/users/")
@login_required
@cache(minutes=2)
def user_list():
    ...

方法提示

常見的模式是使用 methods=["GET", "POST"] 註冊視圖,然後檢查 request.method == "POST" 以決定要做什麼。設置 View.methods 相當於將方法列表傳遞給 add_url_ruleroute

class MyView(View):
    methods = ["GET", "POST"]

    def dispatch_request(self):
        if request.method == "POST":
            ...
        ...

app.add_url_rule('/my-view', view_func=MyView.as_view('my-view'))

這相當於以下內容,除非進一步的子類別可以繼承或更改方法。

app.add_url_rule(
    "/my-view",
    view_func=MyView.as_view("my-view"),
    methods=["GET", "POST"],
)

方法分派和 API

對於 API,為每個 HTTP 方法使用不同的函數可能很有幫助。MethodView 擴展了基本的 View,以便根據請求方法分派到類別的不同方法。每個 HTTP 方法都映射到類別中具有相同(小寫)名稱的方法。

MethodView 會根據類別定義的方法自動設置 View.methods。它甚至知道如何處理覆蓋或定義其他方法的子類別。

我們可以創建一個通用的 ItemAPI 類別,它為給定的模型提供 get(詳細資訊)、patch(編輯)和 delete 方法。GroupAPI 可以提供 get(列表)和 post(創建)方法。

from flask.views import MethodView

class ItemAPI(MethodView):
    init_every_request = False

    def __init__(self, model):
        self.model = model
        self.validator = generate_validator(model)

    def _get_item(self, id):
        return self.model.query.get_or_404(id)

    def get(self, id):
        item = self._get_item(id)
        return jsonify(item.to_json())

    def patch(self, id):
        item = self._get_item(id)
        errors = self.validator.validate(item, request.json)

        if errors:
            return jsonify(errors), 400

        item.update_from_json(request.json)
        db.session.commit()
        return jsonify(item.to_json())

    def delete(self, id):
        item = self._get_item(id)
        db.session.delete(item)
        db.session.commit()
        return "", 204

class GroupAPI(MethodView):
    init_every_request = False

    def __init__(self, model):
        self.model = model
        self.validator = generate_validator(model, create=True)

    def get(self):
        items = self.model.query.all()
        return jsonify([item.to_json() for item in items])

    def post(self):
        errors = self.validator.validate(request.json)

        if errors:
            return jsonify(errors), 400

        db.session.add(self.model.from_json(request.json))
        db.session.commit()
        return jsonify(item.to_json())

def register_api(app, model, name):
    item = ItemAPI.as_view(f"{name}-item", model)
    group = GroupAPI.as_view(f"{name}-group", model)
    app.add_url_rule(f"/{name}/<int:id>", view_func=item)
    app.add_url_rule(f"/{name}/", view_func=group)

register_api(app, User, "users")
register_api(app, Story, "stories")

這會產生以下視圖,一個標準的 REST API!

網址

方法

描述

/users/

GET

列出所有使用者

/users/

POST

創建新使用者

/users/<id>

GET

顯示單個使用者

/users/<id>

PATCH

更新使用者

/users/<id>

DELETE

刪除使用者

/stories/

GET

列出所有故事

/stories/

POST

創建新故事

/stories/<id>

GET

顯示單個故事

/stories/<id>

PATCH

更新故事

/stories/<id>

DELETE

刪除故事