雖然小型web應用程序用單個腳本可以很方便,但這種方法卻不能很好地擴展。隨著應用變得復雜,在單個大的源文件中處理會變得問題重重。
與大多數其他web框架不同,Flask對大型項目沒有特定的組織方式;應用程序的結構完全交給開發人員自己決定。在這一章,提出一個可能的方式來組織管理一個大型應用程序的包和模塊。這種結構將用于書中其余的示例中。
1、項目結構
示例 基本多文件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 |-manage.py
這個結構有四個頂層目錄:
Flask應用一般放置在名為app的目錄下。
migrations目錄包含數據庫遷移腳本,這和之前說的一樣。
單元測試放置在test目錄下
venv目錄包含Python虛擬環境,這和之前說的也是一樣的。
還有一些新的文件:
requirements.txt列出一些依賴包,這樣就可以很容易的在不同的計算機上部署一個相同的虛擬環境。
config.py存儲了一些配置設置。
manage.py用于啟動應用程序和其他應用程序任務。
為了幫助你完全理解這個結構,下面會描述將hello.py應用改為符合這一結構的整個流程。
2、配置選項
應用程序通常需要幾個配置設置。最好的例子就是在開發過程中需要使用不同的數據庫,測試,生產環境,這樣他們可以做到互不干擾。
我們可以使用配置類的層次結構來代替hello.py中的簡單類字典結構配置。下面展示了config.py文件。
config.py:應用程序配置
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()靜態方法。這里特定于配置的初始化是可以執行的。這里Config基類實現一個空init_app()方法。
在配置腳本的底部,這些不同的配置是注冊在配置字典中。將其中一個配置(開發配置)注冊為默認配置。
3、應用程序包
應用程序包放置了所有應用程序代碼、模板和靜態文件。它被簡單的稱為app,也可以給定一個特定于應用的名稱(如果需要的話)。templates和static目錄是應用的一部分,因此這兩個目錄應該放置在app中。數據庫模型和電子郵件支持功能也要置入到這個包中,每個都以app/models.py和app/email.py形式存入自己的模塊當中。
3.1、使用一個應用程序工廠
在單個文件中創建應用程序的方式非常方便,但是它有一個大缺點。因為應用程序創建在全局范圍,沒有辦法動態的適應應用配置的更改:腳本運行時,應用程序實例已經創建,所以它已經來不及更改配置。對于單元測試這是特別重要的,因為有時需要在不同的配置下運行應用程序來獲得更好的測試覆蓋率。
解決這一問題的方法就是將應用程序放入一個工廠函數中來延遲創建,這樣就可以從腳本中顯式的調用。
這不僅給腳本充足的時間來設置配置,也能用于創建多個應用程序實例——一些在測試過程中非常有用的東西。被定義在app包的構造函數中的應用程序工廠函數會在示例7-3中展示。
這個構造函數導入大部分當前需要使用的擴展,但因為沒有應用程序實例初始化它們,它可以被創建但不初始化通過不傳遞參數給它們的構造函數。create_app()即應用程序工廠函數,需要傳入用于應用程序的配置名。配置中的設置被保存在config.py中的一個類中,可以使用Flask的app.config配置對象的from_object()方法來直接導入。配置對象可以通過對象名從config字典中選出。一旦應用程序被創建且配置好,擴展就可以被初始化。調用擴展里的init_app()之前先創建并完成初始化工作。
app/ _init__.py:應用程序包構造函數_
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
工廠函數返回創建的應用程序實例,但是請注意,在當前狀態下使用工廠函數創建的應用程序是不完整的,因為它們沒有路由和自定義錯誤頁面處理程序。這是下一節的主題。
3.2、在藍圖中實現應用程序的功能
應用程序工廠的轉化工作引出了路由的復雜化。在單腳本應用中,應用程序實例是全局的,所以可以很容易地使用app.route裝飾器定義路由。但是現在應用程序在運行時創建,app.route裝飾器只有在create_app()調用后才開始存在,這就太遲了。就像路由那樣,這些通過app.errorhandler裝飾器定義的自定義錯誤頁面處理程序也存在同樣的問題。
幸運的是Flask使用藍圖來提供一個更好的解決方案。一個藍圖就類似于一個可以定義路由的應用程序。不同的是,和路由相關聯的藍圖都在休眠狀態,只有當藍圖在應用中被注冊后,此時的路由才會成為它的一部分。使用定義在全局作用域下的藍圖,定義應用程序的路由就幾乎可以和單腳本應用程序一樣簡單了。
和應用程序一樣,藍圖可以定義在一個文件或一個包中與多個模塊一起創建更結構化的方式。為了追求最大的靈活性,可以在應用程序包中創建子包來持有藍圖。下面展示了創建藍圖的構造函數。
app/main/ _init__.py:創建藍圖_
from flask import Blueprint main = Blueprint('main', __name__) from . import views, errors
藍圖是通過實例化Blueprint類對象來創建的。這個類的構造函數接收兩個參數:藍圖名和藍圖所在的模塊或包的位置。與應用程序一樣,在大多數情況下,對于第二個參數值使用Python的__name__變量是正確的。
應用程序的路由都保存在app/main/views.py模塊內部,而錯誤處理程序則保存在app/main/errors.py中。導入這些模塊可以使路由、錯誤處理與藍圖相關聯。重要的是要注意,在app/init.py腳本的底部導入模塊要避免循環依賴,因為view.py和errors.py都需要導入main藍圖。
藍圖和應用程序一樣注冊在create_app()工廠函數中,如下所示。
示例 app/ _init__.py:藍圖注冊_
def create_app(config_name): # ... from .main import main as main_blueprint app.register_blueprint(main_blueprint) return app
下面則展示了錯誤處理。
app/main/errors.py:藍圖的錯誤處理
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
在藍圖中寫錯誤處理的不同之處是,如果使用了errorhandler裝飾器,則只會調用在藍圖中引起的錯誤處理。而應用程序范圍內的錯誤處理則必須使用app_errorhandler。
這里展示了被更新在藍圖中的應用程序路由。
app/main/views.py:帶有藍圖的應用程序路由
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())
在藍圖中寫視圖函數有兩大不同點。第一,正如之前的錯誤處理一樣,路由裝飾器來自于藍圖。第二個不同是url_for()函數的使用。你可能會回想,該函數的第一個參數為路由節點名,它給基于應用程序的路由指定默認視圖函數。例如,單腳本應用程序中的index()視圖函數的URL可以通過url_for('index')來獲得。
不同的是Flask名稱空間適用于來自藍圖的所有節點,這樣多個藍圖可以使用相同節點定義視圖函數而不會產生沖突。名稱空間就是藍圖名(Blueprint構造函數中的第一個參數),所以index()視圖函數注冊為main.index且它的URL可以通過url_for('main.index')獲得。
在藍圖中,url_for()函數同樣支持更短格式的節點,省略藍圖名,例如url_for('.index')。有了這個,就可以這樣使用當前請求的藍圖了。這實際意味著相同藍圖內的重定向可以使用更短的形式,如果重定向跨藍圖則必須使用帶名稱空間的節點名。
完成了應用程序頁面更改,表單對象也保存在app/main/forms.py模塊中的藍圖里面。
4、啟動腳本
頂層目錄中的manage.py文件用于啟動應用。
manage.py:啟動腳本
#!/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,若它已經定義了則從中獲取配置;如果沒有,則是用默認配置。然后用于Python shell的Flask-Script、Flask-Migrate以及自定義上下文會被初始化。
為了方便,會增加一行執行環境,這樣在基于Unix的操作系統上可以通過./manage.py來執行腳本來替代冗長的python manage.py。
5、需求文件
應用程序必須包含requirements.txt文件來記錄所有依賴包,包括精確的版本號。這很重要,因為可以在不同的機器上重新生成虛擬環境,例如在生產環境的機器上部署應用程序。這個文件可以通過下面的pip命令自動生成:
(venv) $ pip freeze >requirements.txt
當安裝或更新一個包之后最好再更新一下這個文件。以下展示了一個需求文件示例:
Flask==0.10.1 Flask-Bootstrap==3.0.3.1 Flask-Mail==0.9.0 Flask-Migrate==1.1.0 Flask-Moment==0.2.0 Flask-SQLAlchemy==1.0 Flask-Script==0.6.6 Flask-WTF==0.9.4 Jinja2==2.7.1 Mako==0.9.1 MarkupSafe==0.18 SQLAlchemy==0.8.4 WTForms==1.0.5 Werkzeug==0.9.4 alembic==0.6.2 blinker==1.3 itsdangerous==0.23
當你需要完美復制一個虛擬環境的時候,你可以運行以下命令創建一個新的虛擬環境:
(venv) $ pip install -r requirements.txt
當你讀到這時,示例requirements.txt文件中的版本號可能已經過時了。如果喜歡你可以嘗試用最近發布的包。如果遇到任何問題,你可以隨時回退到需求文件中與應用兼容的指定版本。
6、單元測試
這個應用非常小以至于不需要太多的測試,但是作為示例會在示例中展示兩個簡單的測試定義。
示例:tests/test_basics.py:單元測試
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標準庫中標準的unittest包。setUp()和tearDown()方法在每個測試之前和之后運行,且任何一個方法必須以test_開頭作為測試來執行。
建議:如果你想要學習更多使用Python的unittest包來寫單元測試的內容,請參閱官方文檔。
setUp()方法嘗試創建一個測試環境,類似于運行應用程序。首先它創建應用程序配置用于測試并激活上下文。這一步確保測試可以和常規請求一樣訪問current_app。然后,當需要的時候,可以創建一個供測試使用的全新數據庫。數據庫和應用程序上下文會在tearDown()方法中被移除。
第一個測試確保應用程序實例存在。第二個測試確保應用程序在測試配置下運行。為了確保tests目錄有效,需要在tests目錄下增加__init__.py文件,不過該文件可以為空,這樣unittest包可以掃描所有模塊并定位測試。
建議:如果你有克隆在GitHub上的應用程序,你現在可以運行git checkout 7a來切換到這個版本的應用程序。為了確保你已經安裝了所有依賴集,需要運行pip install -r requirements.txt。
為了運行單元測試,可以在manage.py腳本中增加一個自定義的命令。
下面展示如何添加測試命令。
示例:manage.pyt:單元測試啟動腳本
@manager.command def test(): """Run the unit tests.""" import unittest tests = unittest.TestLoader().discover('tests') unittest.TextTestRunner(verbosity=2).run(tests)
manager.command裝飾器使得它可以很容易的實現自定義命令。被裝飾的函數名可以被當做命令名使用,且函數的文檔字符串會顯示幫助信息。test()函數的執行會調用unittest包中的測試運行器。
單元測試可以像下面這樣執行:
(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
7、數據庫啟動
與單腳本的應用相比,重構后的應用使用不同數據庫。
從環境變量中獲取的數據庫URL作為首選,默認SQLite數據庫作為可選。三個配置中的環境變量和SQLite數據庫文件名是不一樣的。例如,開發配置的URL是從DEV_DATABASE_URL環境變量中獲取,如果沒有定義則會使用名為data-dev.sqlite的SQLite數據庫。
無論數據庫URL源的是哪一個,都必須為新的數據庫創建數據庫表。如果使用了Flask-Migrate來保持遷移跟蹤,數據庫表可以被創建或更新到最近的版本通過下面的命令:
(venv) $ python manage.py db upgrade
相信與否,已經到了第一部分結束的地方。你現在已經學到了Flask必要的基本要素,但是你不確定如何將這些零散的知識組合在一起形成一個真正的應用程序。第二部分的目的是通過開發一個完整的應用程序來帶領你繼續前行。
更多Python的Flask框架構建大型Web應用程序的結構相關文章請關注PHP中文網!
聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。TEL:177 7030 7066 E-MAIL:11247931@qq.com