[13] [烧瓶里的部落格] 03. Blueprints 和 View function

面对庞大架构,官方建议使用 Blueprints 将程序码拆分成不同的模组(modules)

Blueprint 是一种把关联程序和 view 组织起来的方式
和直过把 view 和其他程序直接注册到应用的方式不同,直接是把它们注册到Blueprints
然後在工厂函数中把 Blueprint 注册到应用中

在我们的练习 Flaskr 中有两个 Blueprint,一个用於认证功能,另一个用於部落格文章管理
每个 Blueprint 的程序都在一个独立的的 module 中

使用部落格首先需要认证,因此我们先写认证的 Blueprint

建立第一个 Blueprint

flaskr/auth.py

import functools

from flask import (
    Blueprint, flash, g, redirect, render_template, request, session, url_for
)
from werkzeug.security import check_password_hash, generate_password_hash

from flaskr.db import get_db

bp = Blueprint('auth', __name__, url_prefix='/auth')

这里创建了一个名称为auth的 Blueprint
和应用物件一样,Blueprint 需要知道是在哪里定义的,因此把 __name__ 作为函数的第二个参数
url_prefix 会添加到所有与该 Blueprint 关联的 URL 前面

flaskr/init.py
修改档案,使用 app.register_blueprint() 导入并注册刚刚建立的 auth.bp Blueprint
把新的程序放在工厂函数的尾部回传应用之前!

def create_app():
    app = ...
    # existing code omitted

    from . import auth
    app.register_blueprint(auth.bp)

    return app 

认证的 Blueprint 将包括新用户注册、登入和登出的 view

注册

当访问/auth/register URL 时,register view 会回传注册表单的 HTML 页面
当用户提交表单时,view 会验证表单内容,接着根据注册结果
注册成功则建立新用户并显示登录页面,否则显示表单并显示一个错误讯息

底下是 view function 的程序内容

flaskr/auth.py

@bp.route('/register', methods=('GET', 'POST'))
def register():
    if request.method == 'POST':
        username = request.form['username']
        password = request.form['password']
        db = get_db()
        error = None

        if not username:
            error = 'Username is required.'
        elif not password:
            error = 'Password is required.'

        if error is None:
            try:
                db.execute(
                    "INSERT INTO user (username, password) VALUES (?, ?)",
                    (username, generate_password_hash(password)),
                )
                db.commit()
            except db.IntegrityError:
                error = f"User {username} is already registered."
            else:
                return redirect(url_for("auth.login"))

        flash(error)

    return render_template('auth/register.html')

这个register view function做了以下的事情

路由注册

@bp.route 关联了 URL /register 和 register 的 view function
当 Flask 收到一个指向 /auth/register 的 request 时会呼叫 register view function
并把其返回值作为 response

提交表单

如果使用者送出表单,那麽 request.method 将会是POST
这个情况下会进行输入内容的验证

验证表单

request.form 是一个特殊类型的 dict,其对应了提交表单的键和值
表单中会求使用者会输入usernamepassword,所以要验证usernamepassword不为空

写入资料

如果表单验证通过,则写入新的使用者资料

db.execute 使用了带有「?」占位符的 SQL 查询语句
占位符可以代替後面的元组参数中相应的值
使用占位符的好处是会自动帮你转译输入值,以防止 SQL 注入攻击

为了安全原因,不能把密码明文储存在资料库中
使用 generate_password_hash() 将输入的密码进行 hash
而 db.execute 只会执行 SQL 指令
要将指令提交至 SQL,要使用 db.commit() 才会真的执行前面的 SQL query

如果username已经存在,造成无法写入的情况下
会发生sqlite3.IntegrityError,这时候会建立一条错误提示讯息

页面跳转

使用者资料建立後会跳转到登入页面,透过 url_for() 产生对应路由方法函式的 URL
比起直接写死 URL,这麽做的好处是如果之後需要修改对应的 URL,则不需要一个一个找出来改
使用 redirect() 直接跳转到指定的 URL,也就是登入页

如果验证失败,透过flash() 可以在渲染的模块时候向使用者显示一个错误讯息

使用者一开始打开auth/register时,或是注册出错时应该显示一个注册的表单
呼叫 render_template() 会渲染一个包含 HTML 的模板,在下一节会学习如何写这个模板

登入

这个 view 和上面注册的模式一样

flaskr/auth.py

@bp.route('/login', methods=('GET', 'POST'))
def login():
    if request.method == 'POST':
        username = request.form['username']
        password = request.form['password']
        db = get_db()
        error = None
        user = db.execute(
            'SELECT * FROM user WHERE username = ?', (username,)
        ).fetchone()

        if user is None:
            error = 'Incorrect username.'
        elif not check_password_hash(user['password'], password):
            error = 'Incorrect password.'

        if error is None:
            session.clear()
            session['user_id'] = user['id']
            return redirect(url_for('index'))

        flash(error)

    return render_template('auth/login.html')

register有一点点不同之处

查询使用者资料

fetchone() 会据查询回传一笔纪录,如果查询没有结果,则回传None
後面会用到 fetchall() 则会回传所有查询结果的列表

检查密码

check_password_hash() 会以相同的方式对密码进行 hash 密码并比较查询结果是否正确

储存登入状态

session 是一个 dict,用於储存横跨请求的值
当登入验证成功後,使用者的id被储存於一个新的 session 中
资料被储存到一个向浏览器发送的 cookie 中,而浏览器在後继发送的请求中会带上该 cookie
Flask 会对资料进行签章,以防数据被篡改

现在使用者 id 已被储存在 session 中,可以在後续的 request 中使用
如果使用者已经登入,那麽他的使用者资料应该被载入并且在其他 view 里被使用

flaskr/auth.py

@bp.before_app_request
def load_logged_in_user():
    user_id = session.get('user_id')

    if user_id is None:
        g.user = None
    else:
        g.user = get_db().execute(
            'SELECT * FROM user WHERE id = ?', (user_id,)
        ).fetchone()

bp.before_app_request() 注册一个在 view function 之前运行的函数
不论 URL 是什麽,load_logged_in_user都会检查使用者 id 是否已经储存在 session 中
并从资料库中取得使用者资料,然後储存在 g.user 中,并且会持续存在
如果没有使用者 id ,或者 id 查询结果不存在,那g.user将会是None

登出

登出的时候需要把使用者 id 从 session 中移除
然後 load_logged_in_user 就不会在後续的请求中载入使用者资料了

flaskr/auth.py

@bp.route('/logout')
def logout():
    session.clear()
    return redirect(url_for('index'))

在其他 View 中验证登入状态

因为要登入以後才能建立、编辑和删除文章
在每个 view 中可以使用装饰器来完成这个工作

flaskr/auth.py

def login_required(view):
    @functools.wraps(view)
    def wrapped_view(**kwargs):
        if g.user is None:
            return redirect(url_for('auth.login'))

        return view(**kwargs)

    return wrapped_view

装饰器回传一个新的 view,包含了传递给装饰器的原始 view
新的函数检查使用者是否登入
如果已经登入,那麽就继续正常执行原本的 view
否则就跳转到登入页!之後会在部落格的 view 中使用这个装饰器

Endpoints 和 URL

函数 url_for() 根据 view 的名称产生 URL
和 view 相关联的名称亦称为 endpoint
预设情况下,endpoint 名称与 view 的函数名称相同

For example, the hello() view that was added to the app factory earlier in the tutorial has the name 'hello' and can be linked to with url_for('hello'). If it took an argument, which you’ll see later, it would be linked to using url_for('hello', who='World').

例如,之前被加入应用工厂hello()的 view 为'hello',可以使用url_for('hello')来连接
之後会遇到 view 有参数,那麽可使用url_for('hello', who='World') 连接

When using a blueprint, the name of the blueprint is prepended to the name of the function, so the endpoint for the login function you wrote above is 'auth.login' because you added it to the 'auth' blueprint.

当使用 blueprint 的时候,blueprint 的名称会添加到函数名称的前面
上面写的 login 函数 endpoint 为'auth.login',因为你把他加在 'auth' 的 blueprint 中

今天这篇是真的长,主委没拆成多篇水一天很有诚意的吧


<<:  Day 27 Google Ads 的广告帐户最佳分数

>>:  Day12-Express 的部署

D16 - 用 Swift 和公开资讯,打造投资理财的 Apps { 加权指数 K 线图实作.4 - 在 X 轴标上每一根 K 棒的日期 }

目前我们已经做出台股加权指数的 K 线图,但目前进度的线图的 x 轴没有时间,所以当使用者看到这张图...

[Day19] 团队管理:绩效对谈

绩效的对谈 不要错过任何一次反馈的机会 公司运营里面,绩效永远不会缺席,透过绩效可以清楚的为成员嘉勉...

家齐高中资讯研究社 社课内容1

1.练习打字 在Typing Club练习打字 2.练习上传档案 3.下载及使用pyperclip模...

[Day 5] 排版布局Stack

Stack 组件用於沿垂直或水平轴的布局 也是RWD应用的选项之一 复杂度跟所选参数都可以轻易使用 ...

DAY 17 取得资料库资料并将含LINE emoji的讯息传出

小弟自开学後白天上课晚上上班,每天时间不多,进度比较缓慢,请多见谅 上篇将资料存至资料库,这篇要将资...