《Flask Web Development》第7章-大型應用程序架構

把一個小應用程序的代碼都放在一起會很方便,但是不利于擴展,尤其當項目開始變大時在一個文件中工作就會帶來一些問題。不像其他框架,Flask應用程序沒有特定的組織方式,選擇權完全交給了使用者。本章會介紹一種按照包和模塊來組織大型應用程序的方法,并會在本書剩余的章節都采用這種結構。

項目結構

Example 7-1展示了一個Flask應用程序的布局:

Example 7-1. Basic multiple-file Flask application structure

Example 7-1.png

頂級有四個文件夾,分別是:

  • Flask應用程序所在的包通常被命名為app
  • 數據庫遷移相關的腳本被放置在migration
  • 單元測試寫在在tests
  • venv包含了Python的虛擬環境

同樣,增加了一些新的文件:

  • requirements.txt 列舉了依賴的包方便在新的電腦中對虛擬環境快速進行配置
  • config.py 存儲了應用程序的配置參數
  • manage.py 用于啟動應用程序以及做一些其他任務

為了更好地理解這樣的布局方式,后面的部分會介紹如何從一個只有hello.py的程序擴展到上圖所示的結構。

配置選項

應用程序需要一些配置,比如對于開發、測試、產品會需要不同的數據庫那樣才不會相互影響。和單文件版本中在hello.py中寫所有的配置不同,我們能夠用類層級的方式來組織配置:

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基類包含了對所有配置通用的設置,不同的配置子類則定義了特有的設置。隨需求變更還能增加其他配置子類。

為了讓配置更靈活、安全,一些配置參數可以從環境變量中導入,比如SECRET_KEY考慮到安全性,可以存儲在環境變量中,并且在配置腳本中提供了一個默認值以防環境變量沒有設置它。

在三套不同的配置中,SQLALCHEMY_DATABASE_URI被賦予了不同的值,這樣運行在三套不同配置下的應用程序都使用了不同的數據庫。

配置類定義了類方法init_app(),它接受一個應用程序實例作為參數。這樣特殊的配置就能夠執行了(:原文是 Here configuration-specific initialization can performed 沒明白init_app()這個方法跟特殊配置起不起作用有什么關系,至少在本章中的例子中沒有體現出來)。當前,僅Config類實現了一個空的init_app()方法。

在配置文件的底部不同的配置被添加到了字典中,并且開發環境的配置被設置成了默認的。

應用程序包App

應用程序包app是所有應用程序代碼、模板、靜態資源文件存放的地方,當然你也可以根據項目需求取別的名字。模板和資源文件的文件夾都被放入了app中,數據庫對應的models和郵件支持功能模塊則分別對應 app/models.py 和 app/email.py。

使用工廠方法來構建應用示例

在單文件版本中創建應用程序實例很方便,但是通常會有缺陷。因為應用程序實例在全局作用于下被創建,而實例被創建后是沒辦法動態修改配置的。 尤其在做單元測試時,因為要跑不同的數據庫,所以我們要應用不同的配置。

解決辦法就是通過使用工廠方法延遲應用程序實例的創建,這樣不僅僅是延遲了創建時間還讓腳本有創建多個應用程序實例的能力,這對于測試尤其有用。Example 7-3中在app包中定義了了這樣一個工廠方法。

app包導入了Flask目前會用到的擴展,但因為應用程序實例還沒有被構建出來,它們都還沒有被正確初始化。create_app()這個工廠方法接受一個配置名稱作為參數,通過使用Flask提供的app.config的from_object()方法,我們就能從config.py中導入所需要的配置。一旦應用程序實例被創建出來,擴展就能夠通過調用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

工廠方法返回的應用程序實例還不完整,因它們沒有包含路由和錯誤處理功能,下一節會介紹如何解決這個問題。

使用Blueprint來實現應用程實例的功能

用工廠方法構建應用程序實例會給路由設置帶來一些麻煩。單腳本應用中,應用程序實例是全局的,路由能簡單地用app.route decorator來定義。但是現在應用程序實例是運行時創建的,app.route decorator只在在create_app()以后才存在,除此之外app.errorhandler decorator也有同樣的問題。

Flask提供的解決方案是使用blueprints來解決這個問題。blueprints跟application類似,也能定義路由。不同之處是它的路由都處于休眠狀態,直到它被注冊到應用程序實例后路由才是它的一部分。

blueprint在全局作用域下使用,因此我們完全可以像在單文件中那樣使用路由。當然你既能通過單文件也能通過更加組織良好的方式。為了達到最大程度的便利性,一個子包結構被創建用于管理blueprint。Example 7-4展示了在這個main包中如何創建blueprint:

Example 7-4. app/main/init.py: Blueprint creation

from flask import Blueprint
main = Blueprint('main', __name__) 
from . import views, errors

blueprints被創建為Blueprint的實例對象,構造函數有兩個參數:blueprint的名字和它所在的模塊或者包,在這個應用程序中,Python的 __name__ 變量就是第二個參數所需要的值。

應用程序的路由被存儲在app/main/views.py模塊中, 錯誤處理則在app/main/errors.py。導入這些模塊以后,路由和錯誤處理就和blueprint關聯起來了。

有一點要注意路由和錯誤處理模塊是在app/__init__.py的底部被導入的,因為views.py 和 errors.py要導入main blueprint,所以為了避免循環依賴我們要等到main被創建出來才能夠導入路由和錯誤處理。

如Example 7-5所示,blueprint在create_app()方法內被注冊到應用程序實例中:

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展現了錯誤處理:

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使用錯誤處理,如果使用@app.errorhandler,只有由blueprint定義的路由中導致的錯誤才會觸發對應的handler,如果想要錯誤處理對整個應用程序可用,我們需要使用@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中使用視圖方法跟之前有兩個不同的地方。第一個是route是來自blueprint,即-使用@main.route,第二個是url_for()方法的使用。在前面介紹過url_for()的參數默認是視圖方法的名稱,比如在單腳本應用中index()這個視圖方法的URL能夠通過url_for('index')獲取到。

在blueprints中區別在于所有的作用域都來自于blueprint(作用域就是blueprint的名稱,即Blueprint構造函數的第一個參數),因此index()視圖方法需要通過main.index來獲取到URL,即url_for('main.index')。url_for()方法同樣支持參數的更短形式,通過將blueprint名字省略,我們可以簡寫為url_for('.index')。當然如果跨越不同的blueprints,blueprint的名字還是要加上的。

為了完成應用程序,我們還需要在app/main/forms.py模塊導入form相關的一些對象。

啟動腳本

在頂層文件夾下的manage.py是用來啟動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()

該腳本首先創建應用程序實例,然后從系統環境中讀取FLASK_CONFIG變量,如果該變量沒有定義則使用默認值。然后Flask-Script, Flask-Migrate等擴展的實例都被初始化。為了方便在Unix-based系統下運行我們增加了第一行。

Requirements文件

Applications應該包含一個requirements.txt,它記錄了有著準確版本號的所有包依賴,這對以在其他電腦上初始化項目環境很重要。通過如下命令能夠自動生成一個項目用到的包的requirement.txt文件:

(venv) $ pip freeze >requirements.txt

在一個新的環境中,你如果要復制虛擬環境中的安裝包,只需要執行如下命令即可:

(venv) $ pip install -r requirements.txt

該書示例中的requirement.txt中的包可能有一些已經過時了,你可以選擇更加新版的包。如果因此遇到了什么問題,只要回退到老版本即可,因為老版本的都是通過了測試和應用程序兼容的。

單元測試

到目前應用程序還很小,幾乎還沒有什么要測試的,但如Example 7-9所示我們先來寫一個小的測試例子:

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'])

測試是按照Python包中的典型的單元測試的寫法來構建的,setUp() 和 tearDown() 方法在每個測試方法執行前后都會運行,任何以test_ 開頭的方法都會被當做測試方法來執行。關于使用Python包來做單元測試的更多信息可以查看official documentation

setUp()方法創建了測試所需的環境, 他首先創建了應用程序實例用作測試的山下文環境,這樣就能確保測試拿到current_app, 然后新建了一個全新的數據庫。數據庫和應用程序實例最后都會在tearDown() 方法被銷毀。

第一個測試確保了應用程序實例是存在的,第二個測試應用程序實例在測試配置下運行。為了確保測試文件夾有正確的包結構,我們需要添加一個tests/__init__.py文件(:涉及Python包相關知識),這樣單元測試包就能掃描所有在測試文件夾中的模塊了。

你可以把代碼checkout到7a的歷史節點,并且執行 pip install -r requirements.txt 來確保你安裝了所需要的包。為了運行測試用例,還需要添加命令到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所對應的方法名字就是命令的名字,并且方法的文檔信息會被顯示在help中,test() 的實現調用了unittest package包的test runner。如下是運行過程:

(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

數據庫設置

重構后的應用程序使用了跟單文件本版本中完全不同的數據庫。數據庫URL會首先從環境變量中獲取,然后把默認的SQLite數據庫作為備選,在三個配置環境下數據庫的名字是不同的。

不論數據庫的URL是什么,只要是轉換到一個新的數據庫數,據庫表一定要被重新創建(:原文Regardless of the source of the database URL, the database tables must be created for the new database 不完全理解)。使用Flask-Migrate進行遷移管理的過程中,數據庫表能夠通過如下命令被新建或者upgrade:

(venv) $ python manage.py db upgrade

第一部分的內容到此算是結束了,我們已經基本介紹了使用Flask來創建應用程序的所有知識,但是你也許仍舊不確定如何將他們捏合在一起。第二部分的目標就是幫助你完成一個應用程序的開發。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,316評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,481評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,241評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,939評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,697評論 6 409
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,182評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,247評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,406評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,933評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,772評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,973評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,516評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,209評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,638評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,866評論 1 285
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,644評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,953評論 2 373

推薦閱讀更多精彩內容

  • 22年12月更新:個人網站關停,如果仍舊對舊教程有興趣參考 Github 的markdown內容[https://...
    tangyefei閱讀 35,200評論 22 257
  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,781評論 18 139
  • flask是python的一個web應用框架,django很多人聽過,flask比較少見,連創始人一開始寫出來只是...
    思而憂閱讀 2,951評論 0 5
  • 第七章 大型程序架構 雖然在一個腳本里完成一個web應用很便利,但是這也意味著它很難擴展。當程序不斷增長,越來越復...
    易木成華閱讀 925評論 0 1
  • 這幾天想學新東西,就看了flask框架,本身對python不太了解,網上的很多教程看了,總是在某些地方卡住。翻到一...
    易木成華閱讀 2,262評論 0 11