使用Python的Flask框架構建大型Web應用程式的結構示例

NO IMAGE
1 Star2 Stars3 Stars4 Stars5 Stars 給文章打分!
Loading...

雖然小型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 <[email protected]>' 
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應用程式一個基於flask的web應用誕生(1)

相關文章

程式語言 最新文章