把一個(gè)小應(yīng)用程序的代碼都放在一起會(huì)很方便,但是不利于擴(kuò)展,尤其當(dāng)項(xiàng)目開始變大時(shí)在一個(gè)文件中工作就會(huì)帶來一些問題。不像其他框架,F(xiàn)lask應(yīng)用程序沒有特定的組織方式,選擇權(quán)完全交給了使用者。本章會(huì)介紹一種按照包和模塊來組織大型應(yīng)用程序的方法,并會(huì)在本書剩余的章節(jié)都采用這種結(jié)構(gòu)。
項(xiàng)目結(jié)構(gòu)
Example 7-1展示了一個(gè)Flask應(yīng)用程序的布局:
Example 7-1. Basic multiple-file Flask application structure
頂級(jí)有四個(gè)文件夾,分別是:
- Flask應(yīng)用程序所在的包通常被命名為app
- 數(shù)據(jù)庫遷移相關(guān)的腳本被放置在migration
- 單元測(cè)試寫在在tests
- venv包含了Python的虛擬環(huán)境
同樣,增加了一些新的文件:
- requirements.txt 列舉了依賴的包方便在新的電腦中對(duì)虛擬環(huán)境快速進(jìn)行配置
- config.py 存儲(chǔ)了應(yīng)用程序的配置參數(shù)
- manage.py 用于啟動(dòng)應(yīng)用程序以及做一些其他任務(wù)
為了更好地理解這樣的布局方式,后面的部分會(huì)介紹如何從一個(gè)只有hello.py的程序擴(kuò)展到上圖所示的結(jié)構(gòu)。
配置選項(xiàng)
應(yīng)用程序需要一些配置,比如對(duì)于開發(fā)、測(cè)試、產(chǎn)品會(huì)需要不同的數(shù)據(jù)庫那樣才不會(huì)相互影響。和單文件版本中在hello.py中寫所有的配置不同,我們能夠用類層級(jí)的方式來組織配置:
Example 7-2. config.py: Application configuration
import os
basedir = os.path.abspath(os.path.dirname(__file__))
class Config:
SECRET_KEY = os.environ.get('SECRET_KEY') or 'hard to guess string'
SQLALCHEMY_COMMIT_ON_TEARDOWN = True
FLASKY_MAIL_SUBJECT_PREFIX = '[Flasky]'
FLASKY_MAIL_SENDER = 'Flasky Admin <flasky@example.com>'
FLASKY_ADMIN = os.environ.get('FLASKY_ADMIN')
@staticmethod
def init_app(app):
pass
class DevelopmentConfig(Config): DEBUG = True
MAIL_SERVER = 'smtp.googlemail.com'
MAIL_PORT = 587
MAIL_USE_TLS = True
MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
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:///' + os.path.join(basedir, 'data-test.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基類包含了對(duì)所有配置通用的設(shè)置,不同的配置子類則定義了特有的設(shè)置。隨需求變更還能增加其他配置子類。
為了讓配置更靈活、安全,一些配置參數(shù)可以從環(huán)境變量中導(dǎo)入,比如SECRET_KEY考慮到安全性,可以存儲(chǔ)在環(huán)境變量中,并且在配置腳本中提供了一個(gè)默認(rèn)值以防環(huán)境變量沒有設(shè)置它。
在三套不同的配置中,SQLALCHEMY_DATABASE_URI被賦予了不同的值,這樣運(yùn)行在三套不同配置下的應(yīng)用程序都使用了不同的數(shù)據(jù)庫。
配置類定義了類方法init_app(),它接受一個(gè)應(yīng)用程序?qū)嵗鳛閰?shù)。這樣特殊的配置就能夠執(zhí)行了(注
:原文是 Here configuration-specific initialization can performed 沒明白init_app()這個(gè)方法跟特殊配置起不起作用有什么關(guān)系,至少在本章中的例子中沒有體現(xiàn)出來)。當(dāng)前,僅Config類實(shí)現(xiàn)了一個(gè)空的init_app()方法。
在配置文件的底部不同的配置被添加到了字典中,并且開發(fā)環(huán)境的配置被設(shè)置成了默認(rèn)的。
應(yīng)用程序包App
應(yīng)用程序包app是所有應(yīng)用程序代碼、模板、靜態(tài)資源文件存放的地方,當(dāng)然你也可以根據(jù)項(xiàng)目需求取別的名字。模板和資源文件的文件夾都被放入了app中,數(shù)據(jù)庫對(duì)應(yīng)的models和郵件支持功能模塊則分別對(duì)應(yīng) app/models.py 和 app/email.py。
使用工廠方法來構(gòu)建應(yīng)用示例
在單文件版本中創(chuàng)建應(yīng)用程序?qū)嵗芊奖悖峭ǔ?huì)有缺陷。因?yàn)閼?yīng)用程序?qū)嵗谌肿饔糜谙卤粍?chuàng)建,而實(shí)例被創(chuàng)建后是沒辦法動(dòng)態(tài)修改配置的。 尤其在做單元測(cè)試時(shí),因?yàn)橐懿煌臄?shù)據(jù)庫,所以我們要應(yīng)用不同的配置。
解決辦法就是通過使用工廠方法延遲應(yīng)用程序?qū)嵗膭?chuàng)建,這樣不僅僅是延遲了創(chuàng)建時(shí)間還讓腳本有創(chuàng)建多個(gè)應(yīng)用程序?qū)嵗哪芰Γ@對(duì)于測(cè)試尤其有用。Example 7-3中在app包中定義了了這樣一個(gè)工廠方法。
app包導(dǎo)入了Flask目前會(huì)用到的擴(kuò)展,但因?yàn)閼?yīng)用程序?qū)嵗€沒有被構(gòu)建出來,它們都還沒有被正確初始化。create_app()這個(gè)工廠方法接受一個(gè)配置名稱作為參數(shù),通過使用Flask提供的app.config的from_object()方法,我們就能從config.py中導(dǎo)入所需要的配置。一旦應(yīng)用程序?qū)嵗粍?chuàng)建出來,擴(kuò)展就能夠通過調(diào)用init_app()來完成初始化。
Example 7-3. app/__init__.py: Application package constructor
from flask import Flask, render_template
from flask.ext.bootstrap import Bootstrap
from flask.ext.mail import Mail
from flask.ext.moment import Moment
from flask.ext.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)
# attach routes and custom error pages here
return app
工廠方法返回的應(yīng)用程序?qū)嵗€不完整,因它們沒有包含路由和錯(cuò)誤處理功能,下一節(jié)會(huì)介紹如何解決這個(gè)問題。
使用Blueprint來實(shí)現(xiàn)應(yīng)用程實(shí)例的功能
用工廠方法構(gòu)建應(yīng)用程序?qū)嵗龝?huì)給路由設(shè)置帶來一些麻煩。單腳本應(yīng)用中,應(yīng)用程序?qū)嵗侨值模酚赡芎?jiǎn)單地用app.route decorator來定義。但是現(xiàn)在應(yīng)用程序?qū)嵗沁\(yùn)行時(shí)創(chuàng)建的,app.route decorator只在在create_app()以后才存在,除此之外app.errorhandler decorator也有同樣的問題。
Flask提供的解決方案是使用blueprints來解決這個(gè)問題。blueprints跟application類似,也能定義路由。不同之處是它的路由都處于休眠狀態(tài),直到它被注冊(cè)到應(yīng)用程序?qū)嵗舐酚刹攀撬囊徊糠帧?/p>
blueprint在全局作用域下使用,因此我們完全可以像在單文件中那樣使用路由。當(dāng)然你既能通過單文件也能通過更加組織良好的方式。為了達(dá)到最大程度的便利性,一個(gè)子包結(jié)構(gòu)被創(chuàng)建用于管理blueprint。Example 7-4展示了在這個(gè)main包中如何創(chuàng)建blueprint:
Example 7-4. app/main/init.py: Blueprint creation
from flask import Blueprint
main = Blueprint('main', __name__)
from . import views, errors
blueprints被創(chuàng)建為Blueprint的實(shí)例對(duì)象,構(gòu)造函數(shù)有兩個(gè)參數(shù):blueprint的名字和它所在的模塊或者包,在這個(gè)應(yīng)用程序中,Python的 __name__ 變量就是第二個(gè)參數(shù)所需要的值。
應(yīng)用程序的路由被存儲(chǔ)在app/main/views.py模塊中, 錯(cuò)誤處理則在app/main/errors.py。導(dǎo)入這些模塊以后,路由和錯(cuò)誤處理就和blueprint關(guān)聯(lián)起來了。
有一點(diǎn)要注意路由和錯(cuò)誤處理模塊是在app/__init__.py的底部被導(dǎo)入的,因?yàn)関iews.py 和 errors.py要導(dǎo)入main blueprint,所以為了避免循環(huán)依賴我們要等到main被創(chuàng)建出來才能夠?qū)肼酚珊湾e(cuò)誤處理。
如Example 7-5所示,blueprint在create_app()方法內(nèi)被注冊(cè)到應(yīng)用程序?qū)嵗校?/p>
Example 7-5. app/__init__.py: Blueprint registration
def create_app(config_name):
# ...
from main
import main as main_blueprint
app.register_blueprint(main_blueprint)
return app
Example 7-6展現(xiàn)了錯(cuò)誤處理:
Example 7-6. app/main/errors.py: Blueprint with error handlers
from flask import render_template
from . import main
@main.app_errorhandler(404)
def page_not_found(e):
return render_template('404.html'), 404
@main.app_errorhandler(500)
def internal_server_error(e):
return render_template('500.html'), 500
在blueprint使用錯(cuò)誤處理,如果使用@app.errorhandler,只有由blueprint定義的路由中導(dǎo)致的錯(cuò)誤才會(huì)觸發(fā)對(duì)應(yīng)的handler,如果想要錯(cuò)誤處理對(duì)整個(gè)應(yīng)用程序可用,我們需要使用@main.app_errorhandler。
Example 7-7展示了使用blueprint方式的路由:
Example 7-7. app/main/views.py: Blueprint with application routes
from datetime import datetime
from flask import render_template, session, redirect, url_for
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():
# ...
return redirect(url_for('.index'))
return render_template('index.html',
form=form, name=session.get('name'),
known=session.get('known', False),
current_time=datetime.utcnow())
在blueprint中使用視圖方法跟之前有兩個(gè)不同的地方。第一個(gè)是route是來自blueprint,即-使用@main.route,第二個(gè)是url_for()方法的使用。在前面介紹過url_for()的參數(shù)默認(rèn)是視圖方法的名稱,比如在單腳本應(yīng)用中index()這個(gè)視圖方法的URL能夠通過url_for('index')獲取到。
在blueprints中區(qū)別在于所有的作用域都來自于blueprint(作用域就是blueprint的名稱,即Blueprint構(gòu)造函數(shù)的第一個(gè)參數(shù)),因此index()視圖方法需要通過main.index來獲取到URL,即url_for('main.index')。url_for()方法同樣支持參數(shù)的更短形式,通過將blueprint名字省略,我們可以簡(jiǎn)寫為url_for('.index')。當(dāng)然如果跨越不同的blueprints,blueprint的名字還是要加上的。
為了完成應(yīng)用程序,我們還需要在app/main/forms.py模塊導(dǎo)入form相關(guān)的一些對(duì)象。
啟動(dòng)腳本
在頂層文件夾下的manage.py是用來啟動(dòng)application的:
Example 7-8. manage.py: Launch script
#!/usr/bin/env python
import os
from app import create_app, db
from app.models import User, Role
from flask.ext.script import Manager, Shell
from flask.ext.migrate import Migrate, MigrateCommand
app = create_app(os.getenv('FLASK_CONFIG') or 'default')
manager = Manager(app)
migrate = Migrate(app, db)
def make_shell_context():
return dict(app=app, db=db, User=User, Role=Role)
manager.add_command("shell", Shell(make_context=make_shell_context))
manager.add_command('db', MigrateCommand)
if __name__ == '__main__':
manager.run()
該腳本首先創(chuàng)建應(yīng)用程序?qū)嵗缓髲南到y(tǒng)環(huán)境中讀取FLASK_CONFIG變量,如果該變量沒有定義則使用默認(rèn)值。然后Flask-Script, Flask-Migrate等擴(kuò)展的實(shí)例都被初始化。為了方便在Unix-based系統(tǒng)下運(yùn)行我們?cè)黾恿说谝恍小?/p>
Requirements文件
Applications應(yīng)該包含一個(gè)requirements.txt,它記錄了有著準(zhǔn)確版本號(hào)的所有包依賴,這對(duì)以在其他電腦上初始化項(xiàng)目環(huán)境很重要。通過如下命令能夠自動(dòng)生成一個(gè)項(xiàng)目用到的包的requirement.txt文件:
(venv) $ pip freeze >requirements.txt
在一個(gè)新的環(huán)境中,你如果要復(fù)制虛擬環(huán)境中的安裝包,只需要執(zhí)行如下命令即可:
(venv) $ pip install -r requirements.txt
該書示例中的requirement.txt中的包可能有一些已經(jīng)過時(shí)了,你可以選擇更加新版的包。如果因此遇到了什么問題,只要回退到老版本即可,因?yàn)槔习姹镜亩际峭ㄟ^了測(cè)試和應(yīng)用程序兼容的。
單元測(cè)試
到目前應(yīng)用程序還很小,幾乎還沒有什么要測(cè)試的,但如Example 7-9所示我們先來寫一個(gè)小的測(cè)試?yán)樱?/p>
Example 7-9. tests/test_basics.py: Unit tests
import unittest
from flask import current_app
from app import create_app, db
class BasicsTestCase(unittest.TestCase):
def setUp(self):
self.app = create_app('testing')
self.app_context = self.app.app_context()
self.app_context.push()
db.create_all()
def tearDown(self):
db.session.remove()
db.drop_all()
self.app_context.pop()
def test_app_exists(self):
self.assertFalse(current_app is None)
def test_app_is_testing(self):
self.assertTrue(current_app.config['TESTING'])
測(cè)試是按照Python包中的典型的單元測(cè)試的寫法來構(gòu)建的,setUp() 和 tearDown() 方法在每個(gè)測(cè)試方法執(zhí)行前后都會(huì)運(yùn)行,任何以test_ 開頭的方法都會(huì)被當(dāng)做測(cè)試方法來執(zhí)行。關(guān)于使用Python包來做單元測(cè)試的更多信息可以查看official documentation。
setUp()方法創(chuàng)建了測(cè)試所需的環(huán)境, 他首先創(chuàng)建了應(yīng)用程序?qū)嵗米鳒y(cè)試的山下文環(huán)境,這樣就能確保測(cè)試拿到current_app, 然后新建了一個(gè)全新的數(shù)據(jù)庫。數(shù)據(jù)庫和應(yīng)用程序?qū)嵗詈蠖紩?huì)在tearDown() 方法被銷毀。
第一個(gè)測(cè)試確保了應(yīng)用程序?qū)嵗谴嬖诘模诙€(gè)測(cè)試應(yīng)用程序?qū)嵗跍y(cè)試配置下運(yùn)行。為了確保測(cè)試文件夾有正確的包結(jié)構(gòu),我們需要添加一個(gè)tests/__init__.py文件(注
:涉及Python包相關(guān)知識(shí)),這樣單元測(cè)試包就能掃描所有在測(cè)試文件夾中的模塊了。
你可以把代碼checkout到7a的歷史節(jié)點(diǎn),并且執(zhí)行 pip install -r requirements.txt
來確保你安裝了所需要的包。為了運(yùn)行測(cè)試用例,還需要添加命令到manage.py中:
Example 7-10. manage.py: Unit test launcher command
@manager.command
def test():
"""Run the unit tests."""
import unittest
tests = unittest.TestLoader().discover('tests')
unittest.TextTestRunner(verbosity=2).run(tests)
manager.command decorator所對(duì)應(yīng)的方法名字就是命令的名字,并且方法的文檔信息會(huì)被顯示在help中,test() 的實(shí)現(xiàn)調(diào)用了unittest package包的test runner。如下是運(yùn)行過程:
(venv) $ python manage.py test
test_app_exists (test_basics.BasicsTestCase) ... ok
test_app_is_testing (test_basics.BasicsTestCase) ... ok
.----------------------------------------------------------------------
Ran 2 tests in 0.001s
OK
數(shù)據(jù)庫設(shè)置
重構(gòu)后的應(yīng)用程序使用了跟單文件本版本中完全不同的數(shù)據(jù)庫。數(shù)據(jù)庫URL會(huì)首先從環(huán)境變量中獲取,然后把默認(rèn)的SQLite數(shù)據(jù)庫作為備選,在三個(gè)配置環(huán)境下數(shù)據(jù)庫的名字是不同的。
不論數(shù)據(jù)庫的URL是什么,只要是轉(zhuǎn)換到一個(gè)新的數(shù)據(jù)庫數(shù),據(jù)庫表一定要被重新創(chuàng)建(注
:原文Regardless of the source of the database URL, the database tables must be created for the new database 不完全理解)。使用Flask-Migrate進(jìn)行遷移管理的過程中,數(shù)據(jù)庫表能夠通過如下命令被新建或者upgrade:
(venv) $ python manage.py db upgrade
第一部分的內(nèi)容到此算是結(jié)束了,我們已經(jīng)基本介紹了使用Flask來創(chuàng)建應(yīng)用程序的所有知識(shí),但是你也許仍舊不確定如何將他們捏合在一起。第二部分的目標(biāo)就是幫助你完成一個(gè)應(yīng)用程序的開發(fā)。