1. 程式人生 > >Flask Web開發 7.0 大型程式的結構

Flask Web開發 7.0 大型程式的結構

儘管在單一指令碼中編寫小型Web程式很方便,但這種方法並不能廣泛使用。程式變複雜後,使用單個大型原始碼檔案會導致很多問題。
不同於大多數其他Web框架,Flask並不強制要求大型專案使用特定的組織方式,程式結構的組織方式完全由開發者決定。本章將會介紹一種使用包和模組組織大型程式的方式。本書後續示例都會採用這種結構。

7.1專案結構

Flask程式的基本結構:

| -flasky
  | -app/
    | -templates/
    | -static/
    | -main/
      | -__init__.py
      | -errors.py
      | -forms.py
      | -views.py
    | -__init__.py
    | -email.py
    | -models.py
  | -migrations.py
  | -tests/
    | -__init__.py
    | -test.py
  | -venv
  | -requirements.txt
  | -config.py
  | -manage.py
這種結構有四個頂級資料夾:
  • Flask程式一般都儲存在名為app的包中
  • 和之前一樣,migrations資料夾包含資料庫遷移指令碼
  • 單元測試編寫在tests包中
  • 和之前一樣,venv資料夾包含Python虛擬環境
同時還建立了一些新檔案:
  • requirements.txt列出了所有依賴包,便於在其他電腦中重新生成相同的虛擬環境
  • config.py儲存配置
  • manage.py用於啟動程式以及其他的程式任務
下面幾節講解把目前為止我們的略顯規模的hello.py程式轉換為這種結構的過程。

7.2配置選項

程式經常需要設定多個配置。這方面最好的例子就是開發、測試和生產環境要使用不同的資料庫,這樣才不會彼此影響。我們不再使用 hello.py 中簡單的字典狀結構配置,而使用層次結構的配置類。config.py 檔案的內容如示例 7-2 所示。
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的值,這是個敏感資訊,可以在環境中設定,但系統也提供了一個預設值,以防環境中沒有定義。在3個子類中,SQLALCHEMY_DATABASE_URI 變數都被指定了不同的值。這樣程式就可在不同的配置環境中執行,每個環境都使用不同的資料庫。
配置類可以定義 init_app() 類方法,其引數是程式例項。在這個方法中,可以執行對當前環境的配置初始化。現在,基類 Config 中的 init_app() 方法為空。
在這個配置指令碼末尾,config 字典中註冊了不同的配置環境,而且還註冊了一個預設配置(本例的開發環境)。

7.3程式包

程式包用來儲存程式的所有程式碼、模板和靜態檔案。我們可以把這個包直接稱為 app(應用),如果有需求,也可使用一個程式專用名字。templates 和 static 資料夾是程式包的一部分,因此這兩個資料夾被移到了 app 中。資料庫模型和電子郵件支援函式也被移到了這個包中,分別儲存為 app/models.py 和 app/email.py。

7.3.1使用程式工廠函式

在單個檔案中開發程式很方便,但卻有個很大的缺點,因為程式在全域性作用域中建立,所以無法動態修改配置。執行指令碼時,程式例項已經建立,再修改配置為時已晚。這一點對單元測試尤其重要,因為有時為了提高測試覆蓋度,必須在不同的配置環境中執行程式。
這個問題的解決方法是延遲建立程式例項,把建立過程移到可顯式呼叫的工廠函式中。這種方法不僅可以給指令碼留出配置程式的時間,還能夠建立多個程式例項,這些例項有時在測試中非常有用。程式的工廠函式在 app 包的構造檔案中定義,如下所示。
示例 app/__init__.py:程式包的構造檔案
from flask import Flask,render_template
from flask_bootstrap import Bootstrap
from flask_mail import Mail
from flask_sqlalchemy import SQLAlchemy
from flask_moment import Moment
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)

    #附加路由和自定義的錯誤頁面

    return app
構造檔案匯入了大多數正在使用的 Flask 擴充套件。由於尚未初始化所需的程式例項(即Flask(__name__)),所以沒有初始化擴充套件,建立擴充套件類時沒有向建構函式傳入引數。create_app() 函式就是程式的工廠函式,接受一個引數,是程式使用的配置名。配置類在 config.py 檔案中定義,其中儲存的配置可以使用 Flask app.config 配置物件提供的 from_object() 方法直接匯入程式。至於配置物件,則可以通過名字從 config 字典中選擇。程式建立並配置好後,就能初始化擴充套件了。在之前建立的擴充套件物件上呼叫 init_app() 可以完成初始化過程。
工廠函式返回建立的程式示例,不過要注意,現在工廠函式建立的程式還不完整,因為沒有路由和自定義的錯誤頁面處理程式。

7.3.2在藍本中實現程式功能

轉換成程式工廠函式的操作讓定義路由變複雜了。在單指令碼程式中,程式例項存在於全域性作用域中,路由可以直接使用 app.route 修飾器定義。但現在程式在執行時建立,只有呼叫 create_app() 之後才能使用 app.route 修飾器,這時定義路由就太晚了。和路由一樣,自定義的錯誤頁面處理程式也面臨相同的困難,因為錯誤頁面處理程式使用 app.errorhandler 修飾器定義。
不過,Flask使用藍本提供了更好的解決方法。藍本和程式類似,也可以定義路由。不同的是,在藍本中定義的路由處於休眠狀態,直到藍本註冊到程式上後,路由才真正成為程式的一部分。使用位於全域性作用域中的藍本時,定義路由的方法幾乎和單指令碼程式一樣。和程式一樣,藍本可以在單個檔案中定義,也可使用更結構化的方式在包中的多個模組中建立。為了獲得最大的靈活性,程式包中建立了一個子包(main),用於儲存藍本。下面的程式是這個子包的構造檔案,藍本就創建於此。示例 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/main/__init__.py 指令碼的末尾匯入,這是為了避免迴圈匯入依賴,因為在views.py 和 errors.py 中還要匯入藍本 main
藍本在工廠函式create_app()中註冊到程式上,如下所示示例 app/__init__.py:註冊藍本
    #...
    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')

@main.app_errorhandler(500):
def internal_server_error(e):
    return render_template('500.html')
在藍本中編寫錯誤處理程式稍有不同,如果使用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=datatime.utcnow())
在藍本中編寫檢視函式主要有兩點不同:第一,和前面的錯誤處理程式一樣,路由修飾器由藍本提供;第二,url_for()函式的用法不同。我們還記得,url_for()函式的第一個引數是路由的端點名,在程式的路由中,預設為檢視函式的名字。例如,在但指令碼程式中index檢視函式的URL可使用url_for('index')獲取。在藍本中就不一樣了,Flask會為藍本中的全部端點加上一個名稱空間,這樣就可以在不同的藍本中使用相同的端點名定義檢視函式,而不會產生衝突。名稱空間就是藍本的名字(Blueprint建構函式的第一個引數),所以檢視函式index()註冊的端點名是main.index,其URL使用"main.index"獲取。在這裡我們使用了url_for()支援的一種簡寫的端點形式,即省略藍本名,例如url_for('.index')。在這種寫法中,名稱空間是當前請求所在的藍本。這意味著同一藍本中的重定向可以使用簡寫模式,但跨藍本的重定向必須使用帶有名稱空間的端點名。為了完全修改程式的頁面,表單物件也要移到藍本中,保存於app/main/forms.py模組。示例 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')

7.4啟動指令碼-----

注意:新版本的flask已經開始採用flask-cli庫而不是下面的示例使用的flask-script作為其命令列處理工具,因此我們可以在作者的GitHub上看到flasky-7a中已經沒有了manage.py,而是有一個flasky.py。而兩個py檔案的內容也略有不同,但是實際起的作用是相同的,如果想要使用flask-cli庫作為你的工具,可以在網上檢視flask-cli的使用方法。本節我們仍使用flask-script作為討論。頂級資料夾中的manage.py檔案用於啟動程式。示例 manage.py:啟動指令碼
#!/usr/bin/env python 
import os 
from app import create_app, db 
from app.models import User, Role 
from flask_script import Manager, Shell 
from flask_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 和為 Python shell 定義上下文。
出於便利,指令碼中加入了 shebang 宣告,所以在基於 Unix 的作業系統中可以通過 ./manage.py 執行指令碼,而不用使用複雜的 python manage.py。
如果你沒看明白具體是怎麼操作的,文章末尾有操作的細節。

7.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 檔案中的版本號可能已經過期了。如果願意,你可以試著使用這些包的最新版。如果遇到問題,你可以隨時換回這個需求檔案中的版本,因為這些版本和程式相容。

7.6 單元測試

這個程式很小,所以沒什麼可測試的,為了演示,我們可以編寫兩個簡單的測試,如下所示。示例 tests/test_basic.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_開頭的函式都作為測試執行。setUp() 方法嘗試建立一個測試環境,類似於執行中的程式。首先,使用測試配置建立程式,然後啟用上下文。這一步的作用是確保能在測試中使用 current_app,像普通請求一樣。然後建立一個全新的資料庫,以備不時之需。資料庫和程式上下文在 tearDown() 方法中刪除。
第一個測試確保程式例項存在。第二個測試確保程式在測試配置中執行。若想把 tests 資料夾作為包使用,需要新增 tests/__init__.py 檔案,不過這個檔案可以為空,因為 unittest包會掃描所有模組並查詢測試。為了執行單元測試,你可以在 manage.py 指令碼中新增一個自定義命令。
示例 manage.py:啟動單元測試的命令
@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 

7.7建立資料庫

重組後的程式和單指令碼版本使用不同的資料庫。

首選從環境變數中讀取URL,同時還提供了一個預設的SQLite資料庫做備用。3種配置環境中的環境變數名和SQLite資料庫檔名都不一樣。例如,在開發環境中,資料庫URL從環境變數DEV_DATABASE_URL讀取,如果沒有定義這個環境變數,則使用名為data-dev.sqlite的SQLite資料庫。

不管從哪裡獲取資料庫URL,都要在新資料庫中建立資料表。如果使用Flask-Migrate跟蹤遷移,可使用如下命令建立新資料表或者升級到最新修訂版本:
(venv) $ python manage.py db upgrade

至此,第一部分已經結束了。現在我們已經學習了使用Flask開發Web程式的必備基礎知識,不過可能還不確定如何把這些知識融貫起來開發一個真正的程式。第二部分將會帶著我們一步一步地開發出一個完整的程式。

如果我們的操作每一步都沒出問題,那我們應該得到這樣的資料夾

而我們從作者的github點選開啟連結上下載的是這樣的:


如果你擔心你的步驟有問題,可以直接在他的GitHub上下載點選開啟連結 然後把你的manage.py檔案移動到和flasky.py相同的目錄即可。


現在我們可以在這個目錄下通過manage.py來執行flask程式。

注意:當使用runserver開始啟動伺服器程式時,如果我們的資料庫中沒有賦值相關表的資料,在開啟網頁時會報錯,因此我們要先進入程式的shell模式為資料庫初始化一些值。


在shell中可以輸入我們第五章學的那些新增的指令,將role_admin,role_user,user_john等新增到資料庫會話並提交。

這時我們就可以啟動我們的程式了,命令列python manage.py runserver

ok,這時我們的程式就正常執行起來了。嘗試在name欄輸入john、david、susan和123等名字試試,就可以看到執行效果。