Flask教程第九章:分頁

天降攻城獅發表於2019-02-22

本文轉載自:https://www.jianshu.com/p/44b4bca60637

這是Flask Mega-Tutorial系列的第九部分,我將告訴你如何對資料列表進行分頁。

第八章我已經做了幾個資料庫更改,以支援在社交網路非常流行的“粉絲”機制。 有了這個功能,接下來我準備好刪除一開始就使用的模擬使用者動態了。 在本章中,應用將開始接受來自使用者的動態更新,並將其釋出到網站首頁和個人主頁。

本章的GitHub連結為:BrowseZipDiff.

釋出使用者動態

讓我們從簡單的事情開始吧。 首頁需要有一個表單,使用者可以在其中鍵入新動態。 我建立一個表單類:

class PostForm(FlaskForm):
    post = TextAreaField(`Say something`, validators=[
        DataRequired(), Length(min=1, max=140)])
    submit = SubmitField(`Submit`)

然後,我將該表單新增到網站首頁的模板中:

{% extends "base.html" %}

{% block content %}
    <h1>Hi, {{ current_user.username }}!</h1>
    <form action="" method="post">
        {{ form.hidden_tag() }}
        <p>
            {{ form.post.label }}<br>
            {{ form.post(cols=32, rows=4) }}<br>
            {% for error in form.post.errors %}
            <span style="color: red;">[{{ error }}]</span>
            {% endfor %}
        </p>
        <p>{{ form.submit() }}</p>
    </form>
    {% for post in posts %}
    <p>
    {{ post.author.username }} says: <b>{{ post.body }}</b>
    </p>
    {% endfor %}
{% endblock %}

模板中的變更和處理以前的表單類似。最後的部分是將表單處理邏輯新增到檢視函式中:

from app.forms import PostForm
from app.models import Post

@app.route(`/`, methods=[`GET`, `POST`])
@app.route(`/index`, methods=[`GET`, `POST`])
@login_required
def index():
    form = PostForm()
    if form.validate_on_submit():
        post = Post(body=form.post.data, author=current_user)
        db.session.add(post)
        db.session.commit()
        flash(`Your post is now live!`)
        return redirect(url_for(`index`))
    posts = [
        {
            `author`: {`username`: `John`},
            `body`: `Beautiful day in Portland!`
        },
        {
            `author`: {`username`: `Susan`},
            `body`: `The Avengers movie was so cool!`
        }
    ]
    return render_template("index.html", title=`Home Page`, form=form,
                           posts=posts)

我們來一個個地解讀該檢視函式的變更:

  • 匯入PostPostForm
  • 關聯到index檢視函式的兩個路由都新增接受POST請求,以便檢視函式處理接收的表單資料
  • 處理表單的邏輯會為post表插入一條新的資料
  • 模板新增接受form物件,以便渲染文字輸入框

在繼續之前,我想提一些與Web表單處理相關的重要內容。 請注意,在處理表單資料後,我通過傳送重定向到主頁來結束請求。 我可以輕鬆地跳過重定向,並允許函式繼續向下進入模板渲染部分,因為這已經是主頁檢視函式了。

那麼,為什麼重定向呢? 通過重定向來響應Web表單提交產生的POST請求是一種標準做法。 這有助於緩解在Web瀏覽器中執行重新整理命令的煩惱。 當你點選重新整理鍵時,所有的網頁瀏覽器都會重新發出最後的請求。 如果帶有表單提交的POST請求返回一個常規的響應,那麼重新整理將重新提交表單。 因為這不是預期的行為,所以瀏覽器會要求使用者確認重複的提交,但是大多數使用者卻很難理解瀏覽器詢問的內容。不過,如果一個POST請求被重定向響應,瀏覽器現在被指示傳送GET請求來獲取重定向中指定的頁面,所以現在最後一個請求不再是’POST’請求了, 重新整理命令就能以更可預測的方式工作。

這個簡單的技巧叫做Post/Redirect/Get模式。 它避免了使用者在提交網頁表單後無意中重新整理頁面時插入重複的動態。

展示使用者動態

如果你還記得,我建立過幾條模擬的使用者動態,展示在主頁已經有一段時間了。 這些模擬物件是在index檢視函式中顯式建立的一個簡單的Python列表:

    posts = [
        { 
            `author`: {`username`: `John`}, 
            `body`: `Beautiful day in Portland!` 
        },
        { 
            `author`: {`username`: `Susan`}, 
            `body`: `The Avengers movie was so cool!` 
        }
    ]

但是現在我在User模型中有了followed_posts()方法,它可以返回給定使用者希望看到的使用者動態的查詢結果集。 所以現在我可以用真正的使用者動態替換模擬的使用者動態:

@app.route(`/`, methods=[`GET`, `POST`])
@app.route(`/index`, methods=[`GET`, `POST`])
@login_required
def index():
    # ...
    posts = current_user.followed_posts().all()
    return render_template("index.html", title=`Home Page`, form=form,
                           posts=posts)

User類的followed_posts方法返回一個SQLAlchemy查詢物件,該物件被配置為從資料庫中獲取使用者感興趣的使用者動態。 在這個查詢中呼叫all()會觸發它的執行,返回值是包含所有結果的列表。 所以我最終得到了一個與我迄今為止一直使用的模擬使用者動態非常相似的結構。 它們非常接近,模板甚至不需要改變。

更容易地發現和關注使用者

相信你已經留意到了,應用沒有一個很好的途徑來讓使用者可以找到其他使用者進行關注。實際上,現在根本沒有辦法在頁面上檢視到底有哪些使用者存在。我將會使用少量簡單的變更來解決這個問題。

我將會建立一個新的“發現”頁面。該頁面看起來像是主頁,但是卻不是隻顯示已關注使用者的動態,而是展示所有使用者的全部動態。新增的發現檢視函式如下:

@app.route(`/explore`)
@login_required
def explore():
    posts = Post.query.order_by(Post.timestamp.desc()).all()
    return render_template(`index.html`, title=`Explore`, posts=posts)

你有沒有注意到這個檢視函式中的奇怪之處? render_template()引用了我在應用的主頁面中使用的index.html模板。 這個頁面與主頁非常相似,所以我決定重用這個模板。 但與主頁不同的是,在發現頁面不需要一個發表使用者動態表單,所以在這個檢視函式中,我沒有在模板呼叫中包含form引數。

要防止index.html模板在嘗試呈現不存在的Web表單時崩潰,我將新增一個條件,只在傳入表單引數後才會呈現該表單:

{% extends "base.html" %}

{% block content %}
    <h1>Hi, {{ current_user.username }}!</h1>
    {% if form %}
    <form action="" method="post">
        ...
    </form>
    {% endif %}
    ...
{% endblock %}

該頁面也需要新增到導航欄中:

        <a href="{{ url_for(`explore`) }}">Explore</a>

還記得我在第六章中介紹的用於個人主頁渲染使用者動態的_post.html子模板嗎? 這是一個包含在個人主頁模板中的小模板,它獨立於其他模板,因此也可以被這些模板呼叫。 我現在要做一個小小的改進,將使用者動態作者的使用者名稱顯示為一個連結:

    <table>
        <tr valign="top">
            <td><img src="{{ post.author.avatar(36) }}"></td>
            <td>
                <a href="{{ url_for(`user`, username=post.author.username) }}">
                    {{ post.author.username }}
                </a>
                says:<br>{{ post.body }}
            </td>
        </tr>
    </table>

然後在主頁和發現頁中使用這個子模板來渲染使用者動態:

    ...
    {% for post in posts %}
        {% include `_post.html` %}
    {% endfor %}
    ...

子模板期望存在一個名為post的變數,才能正常工作。該變數是上層模板中通過迴圈產生的。

通過這些細小的變更,應用的使用者體驗得到了大大的提升。現在,使用者可以訪問發現頁來檢視陌生使用者的動態,並通過這些使用者動態來關注使用者,而需要的操作僅僅是點選使用者名稱跳轉到其個人主頁並點選關注連結。令人歎為觀止!對吧?

此時,我建議你在應用上再次嘗試一下這個功能,以便體驗最後的使用者介面的完善。

使用者動態

使用者動態的分頁

應用看起來更完善了,但是在主頁顯示所有使用者動態遲早會出問題。如果一個使用者有成千上萬條關注的使用者動態時,會發生什麼?你可以想象得到,管理這麼大的使用者動態列表將會變得相當緩慢和低效。

為了解決這個問題,我會將使用者動態進行分頁。這意味著一開始顯示的只是所有使用者動態的一部分,並提供連結來訪問其餘的使用者動態。Flask-SQLAlchemy的paginate()方法原生就支援分頁。例如,我想要獲取使用者關注的前20個動態,我可以將all()結束呼叫替換成如下的查詢:

>>> user.followed_posts().paginate(1, 20, False).items

Flask-SQLAlchemy的所有查詢物件都支援paginate方法,需要輸入三個引數來呼叫它:

  • 從1開始的頁碼
  • 每頁的資料量
  • 錯誤處理布林標記,如果是True,當請求範圍超出已知範圍時自動引發404錯誤。如果是False,則會返回一個空列表。

paginate方法返回一個Pagination的例項。其items屬性是請求內容的資料列表。Pagination例項還有一些其他用途,我會在之後討論。

現在想想如何在index()檢視函式展現分頁呢。我先來給應用新增一個配置項,以表示每頁展示的資料列表長度吧。

class Config(object):
    # ...
    POSTS_PER_PAGE = 3

儲存這些應用範圍的“可控機關”到配置檔案是一個好主意,因為這樣我調整時只需去一個地方。 在最終的應用中,每頁顯示的資料將會大於三,但是對於測試而言,使用小數字很方便。

接下來,我需要決定如何將頁碼併入到應用URL中。 一個相當常見的方法是使用查詢字串引數來指定一個可選的頁碼,如果沒有給出則預設為頁面1。 以下是一些示例網址,顯示了我將如何實現這一點:

要訪問查詢字串中給出的引數,我可以使用Flask的request.args物件。 你已經在第五章中看到了這種方法,我用Flask-Login實現了使用者登入的可以包含一個next查詢字串引數的URL。

給主頁和發現頁的檢視函式新增分頁的程式碼變更如下:

@app.route(`/`, methods=[`GET`, `POST`])
@app.route(`/index`, methods=[`GET`, `POST`])
@login_required
def index():
    # ...
    page = request.args.get(`page`, 1, type=int)
    posts = current_user.followed_posts().paginate(
        page, app.config[`POSTS_PER_PAGE`], False)
    return render_template(`index.html`, title=`Home`, form=form,
                           posts=posts.items)

@app.route(`/explore`)
@login_required
def explore():
    page = request.args.get(`page`, 1, type=int)
    posts = Post.query.order_by(Post.timestamp.desc()).paginate(
        page, app.config[`POSTS_PER_PAGE`], False)
    return render_template("index.html", title=`Explore`, posts=posts.items)

通過這些更改,這兩個路由決定了要顯示的頁碼,可以從page查詢字串引數獲得或是預設值1。然後使用paginate()方法來檢索指定範圍的結果。 決定頁面資料列表大小的POSTS_PER_PAGE配置項是通過app.config物件中獲取的。

請注意,這些更改非常簡單,每次更改都只會影響很少的程式碼。 我試圖在編寫應用每個部分的時候,不做任何有關其他部分如何工作的假設,這使我可以編寫更易於擴充套件和測試的且兼具模組化和健壯性的應用,並且不太可能失敗或出現BUG。

來嘗試下分頁功能吧。 首先確保你有三條以上的使用者動態。 在發現頁面中更方便測試,因為該頁面顯示所有使用者的動態。 你現在只會看到最近的三條使用者動態。 如果你想看接下來的三條,請在瀏覽器的位址列中輸入http://localhost:5000/explore?page=2

分頁導航

接下來的改變是在使用者動態列表的底部新增連結,允許使用者導航到下一頁或上一頁。 還記得我曾提到過paginate()的返回是Pagination類的例項嗎? 到目前為止,我已經使用了此物件的items屬性,其中包含為所選頁面檢索的使用者動態列表。 但是這個分頁物件還有一些其他的屬性在構建分頁連結時很有用:

  • has_next: 當前頁之後存在後續頁面時為真
  • has_prev: 當前頁之前存在前置頁面時為真
  • next_num: 下一頁的頁碼
  • prev_num: 上一頁的頁碼

有了這四個元素,我就可以生成上一頁和下一頁的連結並將其傳入模板以渲染:

@app.route(`/`, methods=[`GET`, `POST`])
@app.route(`/index`, methods=[`GET`, `POST`])
@login_required
def index():
    # ...
    page = request.args.get(`page`, 1, type=int)
    posts = current_user.followed_posts().paginate(
        page, app.config[`POSTS_PER_PAGE`], False)
    next_url = url_for(`index`, page=posts.next_num) 
        if posts.has_next else None
    prev_url = url_for(`index`, page=posts.prev_num) 
        if posts.has_prev else None
    return render_template(`index.html`, title=`Home`, form=form,
                           posts=posts.items, next_url=next_url,
                           prev_url=prev_url)

 @app.route(`/explore`)
 @login_required
 def explore():
    page = request.args.get(`page`, 1, type=int)
    posts = Post.query.order_by(Post.timestamp.desc()).paginate(
        page, app.config[`POSTS_PER_PAGE`], False)
    next_url = url_for(`explore`, page=posts.next_num) 
        if posts.has_next else None
    prev_url = url_for(`explore`, page=posts.prev_num) 
        if posts.has_prev else None
    return render_template("index.html", title=`Explore`, posts=posts.items,
                          next_url=next_url, prev_url=prev_url)

這兩個檢視函式中的next_urlprev_url只有在該方向上存在一個頁面時,才會被設定為由url_for()返回的URL。 如果當前頁面位於使用者動態集合的末尾或者開頭,那麼Pagination例項的has_nexthas_prev屬性將為’False’,在這種情況下,將設定該方向的連結為None

url_for()函式的一個有趣的地方是,你可以新增任何關鍵字引數,如果這些引數的名字沒有直接在URL中匹配使用,那麼Flask將它們設定為URL的查詢字串引數。

現在讓我們把它們渲染在index.html模板上,就在使用者動態列表的正下方:

    ...
    {% for post in posts %}
        {% include `_post.html` %}
    {% endfor %}
    {% if prev_url %}
    <a href="{{ prev_url }}">Newer posts</a>
    {% endif %}
    {% if next_url %}
    <a href="{{ next_url }}">Older posts</a>
    {% endif %}
    ...

主頁和發現頁都新增了分頁連結。第一個連結標記為“Newer posts”,並指向前一頁(請記住,我顯示的使用者動態按時間的倒序來排序,所以第一頁是最新的內容)。 第二個連結標記為“Older posts”,並指向下一頁的帖子。 如果這兩個連結中的任何一個都是None,則通過條件過濾將其從頁面中省略。

分頁

個人主頁中的分頁

主頁分頁已經完成,但是,個人主頁中也有一個使用者動態列表,其中只顯示個人主頁擁有者的動態。 為了保持一致,個人主頁也應該實現分頁,以匹配主頁的分頁樣式。

我開始更新個人主頁檢視函式,其中仍然有一個模擬使用者動態的列表。

@app.route(`/user/<username>`)
@login_required
def user(username):
    user = User.query.filter_by(username=username).first_or_404()
    page = request.args.get(`page`, 1, type=int)
    posts = user.posts.order_by(Post.timestamp.desc()).paginate(
        page, app.config[`POSTS_PER_PAGE`], False)
    next_url = url_for(`user`, username=user.username, page=posts.next_num) 
        if posts.has_next else None
    prev_url = url_for(`user`, username=user.username, page=posts.prev_num) 
        if posts.has_prev else None
    return render_template(`user.html`, user=user, posts=posts.items,
                           next_url=next_url, prev_url=prev_url)

為了得到使用者的動態列表,我利用了User模型中已經定義好的user.posts一對多關係。 我執行該查詢並新增一個order_by()子句,以便我首先得到最新的使用者動態,然後完全按照我對主頁和發現頁面中的使用者動態所做的那樣進行分頁。 請注意,由url_for()函式生成的分頁連結需要額外的username引數,因為它們指向個人主頁,個人主頁依賴使用者名稱作為URL的動態元件。

最後,對user.html模板的更改與我在主頁上所做的更改相同:

    ...
    {% for post in posts %}
        {% include `_post.html` %}
    {% endfor %}
    {% if prev_url %}
    <a href="{{ prev_url }}">Newer posts</a>
    {% endif %}
    {% if next_url %}
    <a href="{{ next_url }}">Older posts</a>
    {% endif %}

完成對分頁功能的實驗後,可以將POSTS_PER_PAGE配置項設定為更合理的值:

class Config(object):
    # ...
    POSTS_PER_PAGE = 25


相關文章