到現在爲止,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