06.個(gè)人主頁(yè)和頭像

本章將為應(yīng)用添加個(gè)人主頁(yè)肝箱。個(gè)人主頁(yè)用來(lái)展示用戶的相關(guān)信息彰导,其個(gè)人信息可由本用戶編輯矢腻。我將為你展示如何動(dòng)態(tài)地生成每個(gè)用戶的主頁(yè),并提供一個(gè)編輯頁(yè)面給他們來(lái)更新個(gè)人信息学搜。






個(gè)人主頁(yè)

作為創(chuàng)建個(gè)人主頁(yè)的第一步娃善,先要編寫(xiě)一個(gè)與個(gè)人主頁(yè)對(duì)應(yīng)的視圖函數(shù)。

# app\routes.py

from flask_login import login_required

# ...

@main_routes.route('/user/<username>')
@login_required
def user(username):
    user = User.query.filter_by(username=username).first_or_404()
    posts = [
        {'author': user, 'body': 'Test post #1'},
        {'author': user, 'body': 'Test post #2'}
    ]
    return render_template('user.html', user=user, posts=posts)

我們注意到視圖函數(shù)的 @app.route 裝飾器多了被尖括號(hào) <> 包裹的部分瑞佩。 現(xiàn)在 <username> 是動(dòng)態(tài)的聚磺,F(xiàn)lask 將接受該部分 URL 中的任何文本,并以 username 作為參數(shù)名傳遞給視圖函數(shù)钉凌。

例如咧最,如果瀏覽器請(qǐng)求 URL /user/susan,則 user(username) 視圖函數(shù)將被調(diào)用御雕,其參數(shù) username 被設(shè)置為 'susan'

另外這個(gè)視圖函數(shù)只能被已登錄的用戶訪問(wèn)滥搭,所以添加了 @login_required 裝飾器酸纲。

個(gè)人主頁(yè)的視圖函數(shù)當(dāng)然需要查先找用戶實(shí)例再返回其個(gè)人資料,這里我們使用 username 作為唯一標(biāo)識(shí)符來(lái)獲得相應(yīng)的用戶對(duì)象瑟匆。這里我們使用 first_or_404() 方法闽坡,在沒(méi)有與 URL 中傳來(lái)的 username 相匹配的用戶實(shí)例時(shí),它將自動(dòng)發(fā)送 404 error 給客戶端愁溜。

以這種方式執(zhí)行查詢疾嗅,省去了檢查用戶是否處在的邏輯,因?yàn)楫?dāng)用戶名不存在于數(shù)據(jù)庫(kù)中時(shí)冕象,函數(shù)將不會(huì)返回代承,而是會(huì)引發(fā) 404 異常。

如果執(zhí)行數(shù)據(jù)庫(kù)查詢沒(méi)有觸發(fā) 404 錯(cuò)誤渐扮,那么這意味著找到了與給定用戶名匹配的用戶论悴。接下來(lái)掖棉,我們編寫(xiě)一個(gè)新的 user.html 模板,傳入用戶對(duì)象并渲染出來(lái)膀估。

# app\templates\user.html

{% extends "base.html" %}

{% block content %}
  <h1>User: {{ user.username }}</h1>
  <hr>
  {% for post in posts %}
  <p>
    {{ post.author.username }} says: <b>{{ post.body }}</b>
  </p>
  {% endfor %}
{% endblock %}

個(gè)人主頁(yè)完成了幔亥,在頂部的導(dǎo)航欄中添加個(gè)人主頁(yè)的入口鏈接,以便用戶可以查看自己的個(gè)人資料:

# app\templates\base.html

<div>
  Microblog: 
  <a href="{{ url_for('main.index') }}">Home</a>
  {% if current_user.is_anonymous %}
    <a href="{{ url_for('main.login') }}">Login</a>
  {% else %}
    <a href="{{ url_for('main.user', username=current_user.username) }}">
      Profile
    </a>
    <a href="{{ url_for('main.logout') }}">Logout</a>
  {% endif %}
</div>

注意這里生成個(gè)人主頁(yè)的 url_for() 函數(shù)察纯,它接受一個(gè)動(dòng)態(tài)參數(shù)帕棉,所以它可以根據(jù)當(dāng)前登錄用戶 current_user 對(duì)象的 username 動(dòng)態(tài)生成個(gè)人主頁(yè)鏈接。

現(xiàn)在我們的個(gè)人主頁(yè)頁(yè)面如下:






靜態(tài)文件

我們的網(wǎng)站并不僅僅有來(lái)自數(shù)據(jù)庫(kù)中存儲(chǔ)的數(shù)據(jù)記錄饼记,它也包含各種靜態(tài)文件笤昨,可以是圖片、視頻握恳,也可以是日后要用到的 Javascript瞒窒、CSS 文件等。在這里我們先使用靜態(tài)文件的方法管理用戶頭像的圖片乡洼。

Flask 會(huì)在 static 文件夾里尋找靜態(tài)文件崇裁,現(xiàn)在我們?cè)?app 文件夾內(nèi)創(chuàng)建名為 static 的文件夾,靜態(tài)文件相關(guān)的內(nèi)容放置在這里束昵。為了更進(jìn)一步細(xì)化靜態(tài)文件的分類(lèi)管理拔稳,我們繼續(xù)創(chuàng)建 upload/avatar 路徑。

文件組織結(jié)構(gòu)如下:

microblog/
  app/
    static/
        upload/
            avatar/
    templates/
    routes.py
    # ...
  microblog.py
  # ...

我們先在 app\static\upload\avatar 文件夾內(nèi)放置喜歡的圖片作為頭像使用锹雏,為了方便查找到對(duì)應(yīng)的頭像巴比,我們用用戶名來(lái)為圖片重命名。為了符合審美礁遵,最好使用正方形的圖片且文件后綴名為 .png轻绞。

那么怎么訪問(wèn)靜態(tài)文件呢?假設(shè)現(xiàn)在有 diego.png 的圖片文件佣耐,那么訪問(wèn)它的路徑就是 /static/upload/avatar/diego.png政勃。

當(dāng)然更好的方法是使用 url_for() 方法:

url_for("static", filename="/upload/avatar/diego.png")






用靜態(tài)文件構(gòu)建頭像

為了踐行低耦合原則,我們先在 config.py 中增添一些配置:

# config.py

import os
basedir = os.path.abspath(os.path.dirname(__file__))

class Config(object):
    # ...
    
    AVATAR_ROOT = os.path.join(basedir, 'app', 'static', 'upload', 'avatar')
    AVATAR_URL = '/upload/avatar/'
    AVATAR_EXTENSION = 'png'

這兩條并非 Flask 官方或者任何第三方庫(kù)所必須的配置項(xiàng)兼砖,只是我為了方便個(gè)人添加上去的奸远,可以在其他模塊引入后作為常量使用。

AVATAR_ROOT 記錄頭像文件夾的絕對(duì)路徑讽挟;AVATAR_URL 記錄了頭像文件夾相對(duì)于 static 文件夾的路徑懒叛;如果以后位置變化了,直接修改配置文件的相應(yīng)配置即可耽梅,不需要在代碼里每一處再修改 薛窥;AVATAR_EXTENSION 記錄了頭像圖片文件的后綴名。

現(xiàn)在在數(shù)據(jù)庫(kù)模型的 User 類(lèi)中添加 avatar() 方法用于生產(chǎn)用戶頭像的靜態(tài)文件路徑:

import os
from flask import url_for
from config import Config

class User(db.Model, UserMixin):

# ...

def avatar(self):
        root = Config.AVATAR_ROOT
        url = Config.AVATAR_URL
        extension = Config.AVATAR_EXTENSION

        filename = "{}/{}.{}".format(url, self.username, extension)
        avatar_path = os.path.join(root, '{}.{}'.format(self.username, extension))

        if os.path.exists(avatar_path):
            return url_for("static", filename=filename)
        else:
            filename = "{}{}.{}".format(url, 'default', extension)
            return url_for("static", filename=filenam

現(xiàn)在我們把之前在配置中設(shè)置的 AVATAR_ROOTAVATAR_URL 等配置項(xiàng)引入進(jìn)了作為常量來(lái)使用了褐墅。本函數(shù)我們通過(guò)用戶名來(lái)構(gòu)建用戶對(duì)應(yīng)的頭像文件路徑拆檬,如果對(duì)應(yīng)的文件不存在洪己,則返回一個(gè)默認(rèn)頭像。

下一步我們改寫(xiě)個(gè)人主頁(yè)的 HTML 模板文件竟贯,來(lái)顯示用戶的頭像:

# app\templates\user.html

{% extends "base.html" %}

{% block content %}
  <h1>User: {{ user.username }}</h1>
  <img src="{{ user.avatar() }}" weight="80" height="80">
  <hr>
  {% for post in posts %}
  <p>
    {{ post.author.username }} says: <b>{{ post.body }}</b>
  </p>
  {% endfor %}
{% endblock %}

Jinja2 模板語(yǔ)法里除了可以調(diào)用對(duì)象的屬性外答捕,還能直接調(diào)用對(duì)象的方法,這里我們直接使用了 user 對(duì)象的 avatar() 方法屑那。

現(xiàn)在我們的個(gè)人主頁(yè)是這個(gè)樣子:






使用 Gravatar 構(gòu)建頭像

上一節(jié)我們嘗試用本地的靜態(tài)文件來(lái)實(shí)現(xiàn)頭像系統(tǒng)拱镐,這意味著我們需要把服務(wù)器空間里相當(dāng)一部分空間用于存放圖片,在本節(jié)持际,我們使用一個(gè)第三方服務(wù) Gravatar 來(lái)實(shí)現(xiàn)頭像模塊沃琅,上一節(jié)的代碼我們暫且清空。

Gravatar(https://en.gravatar.com/)是一項(xiàng)全球通用頭像服務(wù)蜘欲,它允許我們把頭像文件存儲(chǔ)到 Gravatar 服務(wù)器中并在其他網(wǎng)站或應(yīng)用里使用益眉,只要提供你與這個(gè)頭像關(guān)聯(lián)的 email 地址,就能夠顯示出你的 Gravatar 頭像來(lái)姥份。

Gravatar 頭像的基礎(chǔ)用法是 https://www.gravatar.com/avatar/ + 電子郵箱的 MD5 哈希值郭脂。URL 查詢字符串 s 定義圖片大小,查詢字符串 d 定義默認(rèn)頭像澈歉。

以我的頭像為例:https://www.gravatar.com/avatar/222db16df13a040d59400787573725bb?s=128&d=robohash

更多用法展鸡,我們參見(jiàn):Gravatar 文檔

現(xiàn)在在數(shù)據(jù)庫(kù)模型的 User 類(lèi)中編寫(xiě) avatar() 方法埃难,用來(lái)生成 Gravatar 頭像 URL莹弊。

# app\models.py

from hashlib import md5

# ...

class User(db.Model, UserMixin):
    # ...
    def avatar(self, size):
        digest = md5(self.email.lower().encode('utf-8')).hexdigest()
        return 'https://www.gravatar.com/avatar/{}?s={}&d=robohash' \
            .format(digest, size)

我們先把 email 轉(zhuǎn)化為小寫(xiě)字母,再把字符串編碼為字節(jié)后生成 MD5 哈希值涡尘,用拼接字符串的方式生成頭像的 URL忍弛。這里用 size 參數(shù)設(shè)置頭像大小,如果頭像不存在則隨機(jī)生成一個(gè)機(jī)器人圖片來(lái)做頭像悟衩,這是 URL 查詢字符串 &d=robohash 定義的剧罩。

下一步把頭像圖片插入到個(gè)人主頁(yè)的模板中:

# app\templates\user.html

{% extends "base.html" %}

{% block content %}
  <table>
    <tr valign="top">
      <td><img src="{{ user.avatar(128) }}"></td>
      <td><h1>User: {{ user.username }}</h1></td>
    </tr>
  </table>
  <hr>
  {% for post in posts %}
    <table>
      <tr valign="top">
        <td><img src="{{ post.author.avatar(36) }}"></td>
        <td>{{ post.author.username }} says:<br>{{ post.body }}</td>
      </tr>
    </table>
  {% endfor %}
{% endblock %}

使用 User 類(lèi)來(lái)返回頭像 URL 的好處是,如果有一天我不想繼續(xù)使用 Gravatar 頭像了座泳,我可以重寫(xiě) avatar() 方法來(lái)返回其他頭像服務(wù)網(wǎng)站的 URL。

現(xiàn)在用戶個(gè)人主頁(yè)構(gòu)建完成:






使用 Jinja2 子模板

在個(gè)人主頁(yè)幕与,我們使用頭像和文字組合的方式來(lái)展示用戶動(dòng)態(tài)挑势。如果我想在主頁(yè)也使用一樣的風(fēng)格來(lái)布局,簡(jiǎn)單直接的做法就是把 user.html 的相關(guān)代碼復(fù)制粘貼到 index.html啦鸣,但如果以后需要修改用戶動(dòng)態(tài)的布局潮饱,就必須同時(shí)修改 user.htmlindex.html 兩個(gè)模板文件。

聰明的做法是把可以重用的用戶動(dòng)態(tài)部分作為子模板诫给,然后在 user.htmlindex.html 模板中引用它香拉。

首先啦扬,創(chuàng)建這個(gè)只有一條用戶動(dòng)態(tài) HTML 元素的子模板。 我將其命名為 app/templates/_post.html凫碌, _ 前綴只是一個(gè)命名約定扑毡,可以幫助我標(biāo)記哪些模板文件是子模板。

# app/templates/_post.html

{% extends "base.html" %}

{% block content %}
  <table>
    <tr valign="top">
      <td><img src="{{ user.avatar(128) }}"></td>
      <td><h1>User: {{ user.username }}</h1></td>
    </tr>
  </table>
  <hr>
  {% for post in posts %}
    {% include '_post.html' %}
  {% endfor %}
{% endblock %}

user.html 模板中使用了 Jinja2include 語(yǔ)句來(lái)調(diào)用該子模板:

{% extends "base.html" %}

{% block content %}
  <table>
    <tr valign="top">
      <td><img src="{{ user.avatar(128) }}"></td>
      <td><h1>User: {{ user.username }}</h1></td>
    </tr>
  </table>
  <hr>
  {% for post in posts %}
    {% include '_post.html' %}
  {% endfor %}
{% endblock %}

應(yīng)用的主頁(yè)還沒(méi)有完善盛险,所以現(xiàn)在我不打算在其中添加這個(gè)功能瞄摊。






更多有趣的個(gè)人資料

我們繼續(xù)豐富用戶個(gè)人主頁(yè)的內(nèi)容,我會(huì)新增用戶的自我介紹并在個(gè)人主頁(yè)苦掘。同時(shí)也將跟蹤每個(gè)用戶最后一次訪問(wèn)該網(wǎng)站的時(shí)間换帜,并顯示在他們的個(gè)人主頁(yè)上。

為了支持所有這些額外的信息鹤啡,首先需要做的是用兩個(gè)新的字段擴(kuò)展數(shù)據(jù)庫(kù)中的用戶表:

# app\models.py

class User(UserMixin, db.Model):
    # ...
    about_me = db.Column(db.String(140))
    last_seen = db.Column(db.DateTime, default=datetime.utcnow)

每次數(shù)據(jù)庫(kù)模型被修改時(shí)惯驼,都需要生成數(shù)據(jù)庫(kù)遷移:

(venv) $ flask db migrate -m "new fields in user model"
INFO  [alembic.runtime.migration] Context impl SQLiteImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.autogenerate.compare] Detected added column 'user.about_me'
INFO  [alembic.autogenerate.compare] Detected added column 'user.last_seen'
Generating ...\migrations\versions\cf1611920c49_new_fields_in_user_model.py ...  done

migrate 命令的輸出表示一切正確運(yùn)行,因?yàn)樗@示 User 類(lèi)中的兩個(gè)新字段已被檢測(cè)到递瑰。 現(xiàn)在我可以將此更改應(yīng)用于數(shù)據(jù)庫(kù):

(venv) $ flask db upgrade
INFO  [alembic.runtime.migration] Context impl SQLiteImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.runtime.migration] Running upgrade 03f1db8e73ea -> cf1611920c49,new fields in user model

現(xiàn)在數(shù)據(jù)庫(kù)已經(jīng)修改完成祟牲。

下一步,將會(huì)把新增的兩個(gè)字段增加到個(gè)人主頁(yè)中:

# app\templates\user.html

{% extends "base.html" %}

{% block content %}
  <table>
    <tr valign="top">
      <td><img src="{{ user.avatar(128) }}"></td>
      <td>
        <h1>User: {{ user.username }}</h1>
        {% if user.about_me %}
          <p>{{ user.about_me }}</p>
        {% endif %}
        {% if user.last_seen %}
          <p>Last seen on: {{ user.last_seen }}</p>
        {% endif %}
      </td>
    </tr>
  </table>
  <hr>
  {% for post in posts %}
    {% include '_post.html' %}
  {% endfor %}
{% endblock %}

新增的兩個(gè)字段使用了 Jinja2 的 if 語(yǔ)句泣矛,設(shè)置了字段存在才渲染出來(lái)疲眷,現(xiàn)在字段為空,所以暫時(shí)看不見(jiàn)它您朽。






記錄用戶的最后訪問(wèn)時(shí)間

現(xiàn)在我們實(shí)現(xiàn) last_seen 字段狂丝。容易想到這樣的邏輯:一旦某個(gè)用戶向服務(wù)器發(fā)送請(qǐng)求,就將當(dāng)前時(shí)間寫(xiě)入到這個(gè)字段哗总。

為每個(gè)視圖函數(shù)都添加 last_seen 字段的邏輯几颜,顯然這不是合理的做法。在視圖函數(shù)處理請(qǐng)求之前執(zhí)行一段代碼邏輯在 Web 應(yīng)用中十分常見(jiàn)讯屈, 我們利用 Flask 一個(gè)內(nèi)置功能鉤子函數(shù)來(lái)實(shí)現(xiàn)它蛋哭。

我們?cè)?app\routes.py 添加鉤子函數(shù) before_request

# app\routes.py

from datetime import datetime

@main_routes.before_request
def before_request():
    if current_user.is_authenticated:
        current_user.last_seen = datetime.utcnow()
        db.session.commit()

使用了 before_request 裝飾器之后,每一次請(qǐng)求前 before_request 函數(shù)內(nèi)的代碼都會(huì)執(zhí)行涮母。這里會(huì)先檢查當(dāng)前是否有登錄的用戶谆趾,如果當(dāng)前為已登錄用戶,則更新它的 last_seen 字段為當(dāng)前 UTC 時(shí)間叛本。

現(xiàn)在可以在個(gè)人主頁(yè)看見(jiàn)最后訪問(wèn)時(shí)間了沪蓬,它可能和你所在時(shí)區(qū)的實(shí)際時(shí)間有所不同,我們可以很容易地用 Python 做時(shí)區(qū)轉(zhuǎn)換来候,讓在不同地區(qū)的用戶以當(dāng)?shù)貢r(shí)間格式來(lái)顯示□尾妫現(xiàn)在我們暫且不實(shí)現(xiàn)這個(gè)功能。






個(gè)人資料編輯器

這一節(jié)的操作和前面很類(lèi)似,需要給用戶一個(gè)表單云挟,讓他們通過(guò)表單更改個(gè)人介紹或用戶名梆砸。并存儲(chǔ)在新的 about_me 字段中。 現(xiàn)在先編寫(xiě)一個(gè)表單類(lèi)吧:

# app\forms.py

from wtforms import StringField, TextAreaField, SubmitField
from wtforms.validators import DataRequired, Length

# ...

class EditProfileForm(FlaskForm):
    username = StringField('Username', validators=[DataRequired()])
    about_me = TextAreaField('About me', validators=[Length(min=0, max=140)])
    submit = SubmitField('Submit')

對(duì)于 about_me 字段园欣,使用 TextAreaField 類(lèi)型帖世,這是一個(gè)多行輸入文本框,用戶可以在其中輸入文本俊庇。為了驗(yàn)證這個(gè)字段的長(zhǎng)度狮暑,我使用了 Length 驗(yàn)證器,它將確保輸入的文本在 0 到 140 個(gè)字符之間辉饱。

該表單的渲染模板代碼如下:

# app\templates\edit_profile.html

{% extends "base.html" %}

{% block content %}
  <h1>Edit Profile</h1>
  <form action="" method="post">
    {{ form.hidden_tag() }}
    <p>
      {{ form.username.label }}<br>
      {{ form.username(size=32) }}<br>
      {% for error in form.username.errors %}
      <span style="color: red;">[{{ error }}]</span>
      {% endfor %}
    </p>
    <p>
      {{ form.about_me.label }}<br>
      {{ form.about_me(cols=50, rows=4) }}<br>
      {% for error in form.about_me.errors %}
      <span style="color: red;">[{{ error }}]</span>
      {% endfor %}
    </p>
    <p>{{ form.submit() }}</p>
  </form>
{% endblock %}

最后一步搬男,使用視圖函數(shù)將它們結(jié)合起來(lái):

# app\routes.py

from app.forms import EditProfileForm

@main_routes.route('/edit_profile', methods=['GET', 'POST'])
@login_required
def edit_profile():
    form = EditProfileForm()

    if form.validate_on_submit():
        current_user.username = form.username.data
        current_user.about_me = form.about_me.data
        db.session.commit()
        flash('Your changes have been saved.')
        return redirect(url_for('main.edit_profile'))

    elif request.method == 'GET':
        form.username.data = current_user.username
        form.about_me.data = current_user.about_me

    return render_template(
        'edit_profile.html', 
        title='Edit Profile',
        form=form
    )

這個(gè)視圖函數(shù)處理表單的方式和其他的視圖函數(shù)略有不同。當(dāng)瀏覽器提交的表單通過(guò)了驗(yàn)證 validate_on_submit() 返回 True彭沼,將表單中的數(shù)據(jù)復(fù)制到用戶對(duì)象中缔逛,然后將對(duì)象寫(xiě)入數(shù)據(jù)庫(kù)。

但是當(dāng) validate_on_submit() 返回 False 時(shí)姓惑,可能是由于兩個(gè)不同的原因褐奴。一個(gè)是因?yàn)闉g覽器剛剛發(fā)送了一個(gè) GET 請(qǐng)求,這時(shí)需要用存儲(chǔ)在數(shù)據(jù)庫(kù)中的數(shù)據(jù)預(yù)填充字段于毙,以確保這些表單字段具有用戶的當(dāng)前數(shù)據(jù)敦冬。

第二是瀏覽器發(fā)送含表單數(shù)據(jù)的 POST 請(qǐng)求,但該數(shù)據(jù)中的某些內(nèi)容無(wú)效唯沮。這個(gè)時(shí)候我們只要留在原地不動(dòng)脖旱,讓驗(yàn)證器的提示信息出現(xiàn)在網(wǎng)頁(yè)上即可。

最后將個(gè)人資料編輯頁(yè)面的鏈接添加到個(gè)人主頁(yè)介蛉,以便用戶使用:

{% if user == current_user %}
    <p>
        <a href="{{ url_for('main.edit_profile') }}">Edit your profile</a>
    </p>
{% endif %}

請(qǐng)注意這里使用的 if 語(yǔ)句萌庆,它確保編輯個(gè)人資料的鏈接只在瀏覽自己主頁(yè)時(shí)候才出現(xiàn)。






優(yōu)化應(yīng)用結(jié)構(gòu)

回顧我們現(xiàn)在編寫(xiě)的網(wǎng)站應(yīng)用币旧,現(xiàn)在已經(jīng)實(shí)現(xiàn)了比較多的功能践险,隨著開(kāi)發(fā)的深入,其結(jié)構(gòu)也必將變得臃腫吹菱,現(xiàn)在有必要對(duì)其進(jìn)行優(yōu)化巍虫。

我們現(xiàn)在可以按照功能把應(yīng)用拆成兩個(gè)子模塊:一部分稱之為 main,也就是應(yīng)用的主要功能模塊鳍刷,我會(huì)把跟用戶動(dòng)態(tài)相關(guān)的功能邏輯放在這個(gè)模塊里垫言,如當(dāng)前的 index 主頁(yè)和以后實(shí)現(xiàn)的用戶發(fā)布動(dòng)態(tài)的功能。

另一個(gè)部分叫 auth倾剿,我會(huì)把跟用戶相關(guān)的功能,比如用戶注冊(cè)、用戶登錄前痘、用戶個(gè)人主頁(yè)等功能邏輯放到該模塊凛捏。

兩個(gè)不同子模塊都會(huì)放在 app 路徑下,包裝成 Python 的包芹缔,我會(huì)分別用不同的藍(lán)圖來(lái)對(duì)兩個(gè)子模塊進(jìn)行組織坯癣。

拆分后文檔結(jié)構(gòu)如下:

app/
  auth/ # 與用戶相關(guān)的邏輯
    __init__.py
    forms.py
    routes.py
  main/ # 與動(dòng)態(tài)相關(guān)的邏輯
    __init__.py
    routes.py
  templates/
    auth\ # 與用戶相關(guān)的模板文件
    base.html
    # ...
  __init__.py
  models.py
migrations/
venv/
app.db
config.py
microblog.py  






Blueprints

在 Flask 中,blueprint 是代表應(yīng)用子集的邏輯結(jié)構(gòu)最欠。blueprint 可以包括路由示罗,視圖函數(shù),表單芝硬,模板和靜態(tài)文件等元素蚜点。如果在單獨(dú)的 Python 包中編寫(xiě) blueprint,那么你將擁有一個(gè)封裝了應(yīng)用特定功能的組件拌阴。Blueprint 的內(nèi)容最初處于休眠狀態(tài)绍绘。為了關(guān)聯(lián)這些元素,blueprint 需要在應(yīng)用中注冊(cè)迟赃。

我們會(huì)現(xiàn)在子模塊的 __init__.py 中定義該模塊的 blueprint陪拘;__init__.py 能讓一個(gè)文件夾變成一個(gè) Python 的包,從而用 import 語(yǔ)句調(diào)用纤壁。比如在 app\main\__init__.py 內(nèi)定義的代碼左刽,我們用 from app.main import xxx 就可引入。

現(xiàn)在按照這個(gè)原則來(lái)拆分改寫(xiě)我們的應(yīng)用酌媒。

編寫(xiě) app\main\__init__.py

# app\main\__init__.py

from flask import Blueprint
main_routes = Blueprint('main', __name__)

這里我們?cè)诙x一個(gè) blueprint 名為 main_routes欠痴,main 子模塊下的路由都注冊(cè)在該藍(lán)圖下。

編寫(xiě) app\main\routes.py

# app\main\routes.py

from datetime import datetime
from flask import (
    render_template, 
)
from flask_login import current_user
from app import db
from app.models import User, Post
from app.main import main_routes


@main_routes.before_request
def before_request():
    if current_user.is_authenticated:
        current_user.last_seen = datetime.utcnow()
        db.session.commit()

@main_routes.route('/')
@main_routes.route('/index')
def index():
    posts = Post.query.all()
    return render_template(
        'index.html', 
        title='Home Page', 
        posts=posts
    )

我們只是把跟用戶動(dòng)態(tài)相關(guān)的視圖函數(shù)移動(dòng)到這個(gè)文件下馍佑,然后把剛才創(chuàng)建的 main_routes 引入斋否,再把視圖函數(shù)注冊(cè)到該藍(lán)圖之下。

按照同樣的邏輯拭荤,我們完成 auth 子模塊的拆分茵臭。

# app\auth\__init__.py

from flask import Blueprint
auth_routes = Blueprint('auth', __name__)

app\auth\routes.py 里的邏輯參照 main 的部分,把視圖函數(shù)注冊(cè)到 auth_routes 中舅世,由于代碼過(guò)于冗長(zhǎng)旦委,就不一一列出,參見(jiàn)源碼即可雏亚。forms 里的表單類(lèi)由于全部都是與用戶相關(guān)的缨硝,不做拆分直接移動(dòng)到 app\auth\forms.py

接下來(lái)我們也按照功能分類(lèi)的原則罢低,對(duì) templates 中的模板文件進(jìn)行分類(lèi)查辩。新建一個(gè) app\templates\auth 文件夾胖笛,edit_profile.htmllogin.html宜岛、register.html长踊、user.html 這些和用戶功能相關(guān)的模板文件放置其中。其它的模板文件仍然保留在 templates 目錄萍倡。

修改為成后使用 render_template 調(diào)用模板文件時(shí)候就要加上其相對(duì)路徑身弊,如:'auth/login.html'

下一個(gè)任務(wù)就是改寫(xiě) HTML 模板文件的 url_for()列敲,由于我們把用戶相關(guān)的視圖函數(shù)注冊(cè)到 auth_routes 并命名為 'auth'阱佛;我們需要把原來(lái) url_for('main.login') 改為 url_for('auth.login');同樣地 logout戴而、register 等路由也同樣處理凑术。

最后一步,在工廠函數(shù)引入新的藍(lán)圖并注冊(cè)到 app 中:

# app\__init__.py

from flask import Flask
from config import Config

from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from flask_login import LoginManager

db = SQLAlchemy()
migrate = Migrate()
login = LoginManager()

def create_app():
    app = Flask(__name__)

    # 加載配置
    app.config.from_object(Config)
    
    # 初始化各種擴(kuò)展庫(kù)
    db.init_app(app)
    migrate.init_app(app, db)
    login.init_app(app)

    # 引入藍(lán)圖并注冊(cè)
    from app.main.routes import main_routes
    app.register_blueprint(main_routes)

    from app.auth.routes import auth_routes
    app.register_blueprint(auth_routes)

    return app

from app import models

現(xiàn)在應(yīng)用結(jié)構(gòu)的修改已經(jīng)完成填硕,接下來(lái)我會(huì)按照這個(gè)框架繼續(xù)為網(wǎng)站增添新功能麦萤,如果按照功能對(duì)其拆分,如果有比較獨(dú)立的新功能需要開(kāi)發(fā)扁眯,還將增添新的子模塊壮莹。






本章源碼:https://github.com/SingleDiego/Flask-Tutorial-Source-Code/tree/SingleDiego-patch-06

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市姻檀,隨后出現(xiàn)的幾起案子命满,更是在濱河造成了極大的恐慌,老刑警劉巖绣版,帶你破解...
    沈念sama閱讀 217,542評(píng)論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件胶台,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡杂抽,警方通過(guò)查閱死者的電腦和手機(jī)诈唬,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)缩麸,“玉大人铸磅,你說(shuō)我怎么就攤上這事『贾欤” “怎么了阅仔?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,912評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)弧械。 經(jīng)常有香客問(wèn)我八酒,道長(zhǎng),這世上最難降的妖魔是什么刃唐? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,449評(píng)論 1 293
  • 正文 為了忘掉前任羞迷,我火速辦了婚禮界轩,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘闭树。我一直安慰自己耸棒,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,500評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布报辱。 她就那樣靜靜地躺著,像睡著了一般单山。 火紅的嫁衣襯著肌膚如雪碍现。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,370評(píng)論 1 302
  • 那天米奸,我揣著相機(jī)與錄音昼接,去河邊找鬼。 笑死悴晰,一個(gè)胖子當(dāng)著我的面吹牛慢睡,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播铡溪,決...
    沈念sama閱讀 40,193評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼漂辐,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了棕硫?” 一聲冷哼從身側(cè)響起髓涯,我...
    開(kāi)封第一講書(shū)人閱讀 39,074評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎哈扮,沒(méi)想到半個(gè)月后纬纪,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,505評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡滑肉,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,722評(píng)論 3 335
  • 正文 我和宋清朗相戀三年包各,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片靶庙。...
    茶點(diǎn)故事閱讀 39,841評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡问畅,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出惶洲,到底是詐尸還是另有隱情按声,我是刑警寧澤,帶...
    沈念sama閱讀 35,569評(píng)論 5 345
  • 正文 年R本政府宣布恬吕,位于F島的核電站签则,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏铐料。R本人自食惡果不足惜渐裂,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,168評(píng)論 3 328
  • 文/蒙蒙 一豺旬、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧柒凉,春花似錦族阅、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,783評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至蔬咬,卻和暖如春鲤遥,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背林艘。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,918評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工盖奈, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人狐援。 一個(gè)月前我還...
    沈念sama閱讀 47,962評(píng)論 2 370
  • 正文 我出身青樓钢坦,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親啥酱。 傳聞我的和親對(duì)象是個(gè)殘疾皇子爹凹,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,781評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容