Flask學習(6)——重構應用結構

到現在爲止,hello.py的完整代碼如下:

from flask import Flask, render_template, session, redirect, url_for, flash
from flask_bootstrap import Bootstrap
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField
from wtforms.validators import DataRequired
from flask_moment import Moment
import os
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from flask_mail import Mail
from flask_mail import Message
from threading import Thread

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

app = Flask(__name__)

app.config['SECRET_KEY'] = 'I am Lethe'

app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + \
    os.path.join(basedir, 'data.sqlite')
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

# 電子郵件
app.config['MAIL_SERVER'] = 'smtp.qq.com'
app.config['MAIL_PORT'] = 465
app.config['MAIL_USE_SSL'] = True
app.config['MAIL_USERNAME'] = os.environ.get('MAIL_USERNAME')
app.config['MAIL_PASSWORD'] = os.environ.get('MAIL_PASSWORD')

app.config['FLASKY_MAIL_SUBJECT_PREFIX'] = '[Flasky]'
app.config['FLASKY_MAIL_SENDER'] = 'Flasky Admin <[email protected]>'
app.config['FLASKY_ADMIN'] = os.environ.get('FLASKY_ADMIN')

bootstrap = Bootstrap(app)
moment = Moment(app)
db = SQLAlchemy(app)
migrate = Migrate(app, db)
mail = Mail(app)


class NameForm(FlaskForm):
    name = StringField('What is your name?', validators=[DataRequired()])
    submit = SubmitField('Submit')


class Role(db.Model):
    __tablename__ = 'roles'
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(64), unique=True)
    users = db.relationship('User', backref='role')

    def __repr__(self):
        return '<Role %r>' % self.name


class User(db.Model):
    __tablename__ = 'users'
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(64), unique=True, index=True)
    role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))
    message = db.Column(db.Text)

    def __repr__(self):
        return '<User %r>' % self.username


@app.shell_context_processor
def make_shell_context():
    return dict(db=db, User=User, Role=Role)


def send_async_email(app, msg):
    with app.app_context():
        mail.send(msg)


def sned_email(to, subject, template, **kwargs):
    msg = Message(app.config['FLASKY_MAIL_SUBJECT_PREFIX'] + subject,
                  sender=app.config['FLASKY_MAIL_SENDER'], recipients=[to])
    msg.body = render_template(template + '.txt', **kwargs)
    msg.html = render_template(template + '.html', **kwargs)
    thr = Thread(target=send_async_email, args=[app, msg])
    thr.start()
    return thr


@app.route('/', methods=['GET', 'POST'])
def index():
    form = NameForm()
    if form.validate_on_submit():
        user = User.query.filter_by(username=form.name.data).first()
        if user is None:
            user = User(username=form.name.data)
            db.session.add(user)
            db.session.commit()
            session['known'] = False
            # 發送電子郵件
            if app.config['FLASKY_ADMIN']:
                sned_email(app.config['FLASKY_ADMIN'], 'New User',
                           'mail/new_user', user=user)
        else:
            session['known'] = True
        session['name'] = form.name.data
        session['message'] = user.message
        form.name.data = ''
        return redirect(url_for('index'))
    return render_template('index.html',
                           form=form, name=session.get('name'),
                           known=session.get('known', False),
                           message=session.get('message'))


@app.route('/user/<name>')
def user(name):
    return render_template('user.html', name=name)


@app.errorhandler(404)
def pate_not_found(e):
    return render_template('404.html'), 404


@app.errorhandler(500)
def internal_server_error(e):
    return render_template('500.html'), 500

可以看到隨着應用複雜程度增加,將所有部分寫在一個腳本里會導致許多問題,而不同於多數其他的 Web 框架,Flask 並不強制要求大型項目使用特定的組織方式,應用結構的組織方式完全由開發者決定。

一、項目結構

多文件 Flask 應用的基本結構如下:

|-flasky
  |-app/
    |-templates/
    |-static/
    |-main/
      |-__init__.py
      |-errors.py
      |-forms.py
      |-views.py
    |-__init__.py
    |-email.py
    |-models.py
  |-migrations/
  |-tests/
    |-__init__.py
    |-test*.py
  |-venv/
  |-requirements.txt
  |-config.py
  |-flasky.py

這種結構有4個頂級文件夾:

  • Flask 應用一般保存在名爲 app 的包中;
  • 數據庫遷移腳本在 migrations 文件夾中;
  • 單元測試在 tests 包中編寫;
  • Python虛擬環境在 venv 文件夾中。

此外,還多了一些新文件:

  • requirements.txt 列出了所有依賴包,便於在其他計算機中重新生成相同的虛擬環境;
  • config.py 存儲配置;
  • flasky.py 定義 Flask 應用實例,同時還有一些輔助管理應用的任務。

下面我們嘗試把之前的 hello.py 應用轉換成此種結構。


二、配置選項

應用經常需要設定多個配置,如開發、測試和生產環境要使用不同的數據庫,這樣纔不會彼此影響。

除了 hello.py 中類似字典的 app.config 對象之外,還可以使用具有層次結構的配置類。將 hello.py 中的配置項獨立在 config.py 中如下:

import os

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


class Config:
    SECRET_KEY = os.environ.get('SECRET_KEY') or 'I am Lethe'
    MAIL_SERVER = os.environ.get('MAIL_SERVER', 'smtp.qq.com')
    MAIL_PORT = int(os.environ.get('MAIL_PORT', '465'))
    MAIL_USE_TLS = os.environ.get('MAIL_USE_SSL', 'true').lower() in \
        ['true', 'on', '1']
    MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
    MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
    FLASKY_MAIL_SUBJECT_PREFIX = '[Flasky]'
    FLASKY_MAIL_SENDER = 'Flasky Admin <[email protected]>'
    FLASKY_ADMIN = os.environ.get('FLASKY_ADMIN')
    sQLALCHEMY_TRACK_MODIFICATIONS = False

    @staticmethod
    def init_app(app):
        pass


# 開發環境數據庫
class DevelopmentConfig(Config):
    DEBUG = True
    SQLALCHEMY_DATABASE_URI = os.environ.get('DEV_DATABASE_URL') or \
        'sqlite:///' + os.path.join(basedir, 'data-dev.sqlite')


# 測試環境數據庫
class TestingConfig(Config):
    TESTING = True
    SQLALCHEMY_DATABASE_URI = os.environ.get('TEST_DATABASE_URL') or \
        'sqlite://'


# 生成環境數據庫
class ProductionConfig(Config):
    SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
        'sqlite:///' + os.path.join(basedir, 'data.sqlite')


config = {
    'development': DevelopmentConfig,
    'testing': TestingConfig,
    'production': ProductionConfig,
    'default': DevelopmentConfig
}
  • 基本Config包含通用配置,各個子類分別定義專用的配置。如果需要,也可以添加其他配置類。
  • 爲了更安全和靈活,多數配置都可以從環境變量中導入。
  • 在 3 個子類中,SQLALCHEMY_DATABASE_URI 變量都被指定了不同的值。這樣應用就可以在不同的環境中使用不同的數據庫。
  • 開發環境和生產環境都配置了郵件服務器。爲了再給應用提供一種定製配置的方式,Config 類及其子類可以定義 init_app() 類方法,其參數爲應用實例。現在,基類 Config 中的 init_app() 方法爲空。
  • 在這個配置腳本末尾,config 字典中註冊了不同的配置環境,而且還註冊了一個默認配置(這裏註冊爲開發環境)。

三、應用包

應用包用來存放應用的所有代碼、模板和靜態文件,通常稱爲爲 app(應用)。templates 和 static 目錄需要移動到應用包中,數據庫模型和電子郵件支持函數也要移到這個包中,分別保存爲 app/models.py 和 app/email.py。

3.1 使用應用工廠函數

單個文件中開發應用是很方便,但卻有個很大的缺點:應用在全局作用域中創建,無法動態修改配置。運行腳本時,應用實例已經創建,再修改配置爲時已晚。這一點對單元測試尤其重要,因爲有時爲了提高測試覆蓋度,必須在不同的配置下運行應用。

這個問題的解決方法是延遲創建應用實例,把創建過程移到可顯式調用的工廠函數中。這種方法不僅可以給腳本留出配置應用的時間,還能夠創建多個應用實例,爲測試提供便利。

應用的工廠函數在 app 包的構造文件 app/__init__.py 中定義如下:

from flask import Flask, render_template
from flask_bootstrap import Bootstrap
from flask_mail import Mail
from flask_moment import Moment
from flask_sqlalchemy import SQLAlchemy
from config import config

bootstrap = Bootstrap()
mail = Mail()
moment = Moment()
db = SQLAlchemy()

def create_app(config_name):
    app = Flask(__name__)
    app.config.from_object(config[config_name])
    config[config_name].init_app(app)

    bootstrap.init_app(app)
    mail.init_app(app)
    moment.init_app(app)
    db.init_app(app)

    # 添加路由和自定義錯誤頁面
    from .main import main as main_blueprint
    app.register_blueprint(main_blueprint)

    return app
  • 構造文件導入了大多數使用的 Flask 擴展,由於此時尚未初始化應用實例,所以這些擴展的實例化並未傳參,也就沒有真正初始化。
  • create_app() 函數是應用的工廠函數,接收一個參數,即應用使用的配置名(前面在config.py中定義的)。配置可以通過 app.config 配置對象提供的 from_object() 方法直接導入應用,參數 config[config_name] 即從 config 字典中選擇一個配置類進行配置。
  • 在之前創建的的擴展對象上調用 init_app() 方法可以將 Flask 擴展完成初始化。

3.2 在藍本中實現應用功能

(1)藍本(blueprint)和應用類似,也可以定義路由和錯誤處理程序。但是在藍本中定義的路由和錯誤處理程序處於休眠狀態,直到藍本註冊到應用上之後,才相當於真正定義在了應用中。

藍本可以在單個文件中定義,也可使用更結構化的方式在
包中的多個模塊中創建。我們將在應用包中創建一個子包 main,用於保存應用的第一個藍本。

此子包的構造文件 app/main/__init__.py 如下,創建主藍本:

from flask import Blueprint

main = Blueprint('main', __name__)

from . import views, errors
  • 藍本通過實例化一個 Blueprint 類對象創建。這個構造函數有兩個必須指定的參數:藍本的名稱和藍本所在的包或模塊。
  • 應用的路由保存在包裏的 app/main/views.py 模塊中,而錯誤處理程序保存在 app/main/errors.py 模塊中,導入這兩個模塊就能把路由和錯誤處理程序與藍本關聯起來。
  • 這些模塊在 app/main/init.py 腳本的末尾導入,這是爲了避免循環導入依賴,因爲在 app/main/views.py 和app/main/errors.py 中還要導入 main 藍本,所以除非循環引用出現在定義 main 之後,否則會致使導入出錯。

(2)藍本在工廠函數 create_app() 中註冊到應用上,如下注冊主藍本:

# app/__init__.py

def create_app(config_name):
# ...
from .main import main as main_blueprint
app.register_blueprint(main_blueprint)
return app

(3)主藍本中的錯誤處理程序 app/main/errors.py:

from flask import render_template
from . import main

@main.app_errorhandler(404)
def pate_not_found(e):
    return render_template('404.html'), 404


@main.app_errorhandler(500)
def internal_server_error(e):
    return render_template('500.html'), 500
  • 之前我們使用的是 errorhandler 裝飾器,但是在藍本中如果使用他,就只有藍本中的錯誤才能觸發處理程序。
  • 因此我們需要使用 app_errorhandler 裝飾器來註冊全局的錯誤處理程序。

(4)主藍本中定義的應用路由 app/main/views.py:

from datetime import datetime
from flask import render_template, session, redirect, url_for, flash
from . import main
from .forms import NameForm
from .. import db
from ..models import User


@main.route('/', methods=['GET', 'POST'])
def index():
    form = NameForm()
    if form.validate_on_submit():
        user = User.query.filter_by(username=form.name.data).first()
        if user is None:
            user = User(username=form.name.data)
            db.session.add(user)
            db.session.commit()
            session['known'] = False
            # 發送電子郵件
            if app.config['FLASKY_ADMIN']:
                sned_email(app.config['FLASKY_ADMIN'], 'New User',
                           'mail/new_user', user=user)
        else:
            session['known'] = True
        session['name'] = form.name.data
        session['message'] = user.message
        form.name.data = ''
        return redirect(url_for('main.index')) # 在同一藍本中可簡寫爲 .index
    return render_template('index.html',
                           form=form, name=session.get('name'),
                           known=session.get('known', False),
                           message=session.get('message'))


@main.route('/user/<name>')
def user(name):
    return render_template('user.html', name=name)
  • 和錯誤處理程序一樣,這裏的路由裝飾器使用的是 main.route,而不是 app.route。
  • url_for() 函數使用的是 url_for(‘main.index’) ,而不是 url_for(‘index’)。這是因爲 Flask 會爲藍本中的全部端點加上一個命名空間,即爲藍本的名稱(Blueprint 構造函數的第一個參數)。
  • 若請求的端在在藍本內,則也可以縮寫爲 url_for(’.index’)

(5)還需要將表單類移到藍本中,保存在 app/main/forms.py 模塊中:

from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField
from wtforms.validators import DataRequired

class NameForm(FlaskForm):
    name = StringField('What is your name?', validators=[DataRequired()])
    submit = SubmitField('Submit')

四、應用腳本

應用實例在頂級目錄中的 flasky.py 模塊裏定義:

import os
from app import create_app, db
from app.models import User, Role
from flask_migrate import Migrate

app = create_app(os.getenv('FLASK_CONFIG') or 'default')
migrate = Migrate(app, db)

@app.shell_context_processor
def make_shell_context():
    return dict(db=db, User=User, Role=Role)
  • 此主腳本先創建了一個應用實例,配置名可以從環境變量中讀取,也可以使用默認值。
  • 然後初始化數據庫遷移擴展 Flask-Migreate 併爲 Python shell 註冊上下文。

現在我們要想運行應用,就需要把環境變量 FLASK_APP 設置爲 flasky.py ,再執行 flask run 纔可以。此外,還可以將 FLASK_DEBUG設置爲1,來開啓調試模式。


五、需求文件

應用中最好有個 requirements.txt 文件,用於記錄所有依賴包及其精確的版本號,以便在另一個環境上重新生成虛擬環境。

在虛擬環境中執行如下命令:

 pip freeze >requirements.txt

在安裝或升級包後,最好更新一下這個文件。

然後當你想創建這個虛擬環境的副本時,則可以先創建一個新的虛擬環境,然後根據 requirements.txt 安裝需要的包和擴展:

pip install -r requirements.txt

六、創建數據庫

首選從環境變量中讀取數據庫的 URL,同時還提供了一個默認的SQLite 數據庫作爲備用。3 種配置環境中的環境變量名和 SQLite 數據庫文件名都不一樣。

不管從哪裏獲取數據庫 URL,都要在新數據庫中創建數據表,參見“數據庫”章節

如果使用 Flask-Migrate 跟蹤遷移,可使用下述命令創建數據表或者升級到最新修訂版本:

flask db upgrade
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章