Flask重構-使用工廠方法和藍圖

雖然初步開發的時候可以不用管結構,全部程式碼都塞在一起就好,但是遇到要擴展或是測試的時候問題就出現了,很多潛在的問題在程式擴展的路上會出現,增加很多時間成本。而我自己也想練習一下寫測試,有好的結構才能快速切換測試和開發的環境,剛好藉由自己的這個小project來玩玩看

結構

參考《Flask Web開發》

書中範例結構

以下是重構後的資料結構

主資料夾(/)

APP資料夾(/app)

APP資料夾的檔案

code都拆開放在專屬的py檔案裡了

  • app資料夾是放Flask主程式,也可以換成其他名子
  • app/main底下放的是views(update, delete, add之類的url)
  • app/templates存放各個頁面的html
  • app/tests放測試
  • app底下放command(建資料庫,新增帳號密碼之類的)以及models(放資料庫的models)
  • /根目錄放main.py用來呼叫app裡的create_app()來建立app,這個是工廠方法,下面會提到

工廠方法(Factory pattern)

為了讓程式可以建立多個instance,以便Unit test(因為為了提高測試覆蓋度,有時候必須在不同的的設定下執行程式)。單instance建立後沒辦法再修改設定,所以需要多個instance

解決方法是延遲建立instance,建立過程移至工廠方法,這裡放在app/__init__.py(官方文件寫得超簡短…用處有限🤦‍♂️只知道db要放在create_app())

工廠方法要配合藍圖(blueprint)才能建立路由和自訂404頁面

import os
import sys
from flask import Blueprint, Flask
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager
from .config import config

# SQLite URI compatible
WIN = sys.platform.startswith('win')
if WIN:
    prefix = 'sqlite:///'
else:
    prefix = 'sqlite:////'

def create_app(config_name):
    app = Flask(__name__)
    app.config.from_object(config[config_name])

    from .models import db
    db.init_app(app)
    login_manager = LoginManager(app)
    login_manager.init_app(app)

    @login_manager.user_loader
    def load_user(user_id):
        from .models import User
        user = User.query.get(int(user_id))
        return user

    login_manager.login_view = 'main.login'

    @app.context_processor
    def inject_user():
        from .models import User
        user = User.query.first()
        return dict(user=user)
    
    # 註冊藍圖
    from .main import main as main_blueprint
    app.register_blueprint(main_blueprint)

    return app

藍圖

  • 藍圖: 放在app/main/__init__.py
  • 由於換成工廠模式來建立instance,建立後才能使用app.route,但已經無法定義路由了,也就是在create_app()裡定義藍圖好定義路由
  • 藍圖中定義路由和錯誤處理程式處於休眠狀態,只有註冊(register)到應用程式上才成為他的一部分
  • 可以在單個檔案中定義,也可以用結構化方式在多個module裡建立,為了方便直接在app/main裡建立一個blueprint,包含viewserrors
  • 定義路由的時候要寫上藍圖的名稱,因為是由藍圖提供,而非app.route,這裡是@main.route()
  • 使用藍圖時,url_for()裡的參數不再是直接寫上路由路徑的名子,例如url_for('index')。而是要加上藍圖的名稱變為url_for('main.index')這是為了在不同藍圖中使用同樣的路徑名
  • 當同一個藍圖重定向(redirect)的時候可以簡化寫法變為url_for('.index'),跨藍圖的時候就得加上藍圖名
from flask import Blueprint

# Main blueprint
# 兩個必要的參數: 藍圖名稱和所在的包或模塊
main = Blueprint('main', __name__)

# 避免循環依賴
# 因為兩個script都會導入main
from . import views, errors

設定檔

由於不同的階段會有不同的設定以及參數,所以需要設定檔來定義每個階段需要的設定,以便快速切換。例如開發、測試、和部署,都需要不同的設定以及參數,這時有設定檔就可以隨意切換,不須手動調整(自動就是爽)

這裡我是放在app/config.py,簡單設定一下不同環境的參數

import os
import sys

# SQLite URI compatible
WIN = sys.platform.startswith('win')
if WIN:
    prefix = 'sqlite:///'
else:
    prefix = 'sqlite:////'

def create_sqlite_uri(db_name):
    return prefix + os.path.join(os.path.dirname(__file__), db_name)

class BaseConfig(object):
    SECRET_KEY = '你的密碼'

class DevelopmentConfig(BaseConfig):
    DEBUG = False
    SQLALCHEMY_DATABASE_URI = create_sqlite_uri('watching-history.db')
    SQLALCHEMY_TRACK_MODIFICATIONS = False

class TestingConfig(BaseConfig):
    TESTING = True
    SQLALCHEMY_DATABASE_URI = create_sqlite_uri('test.db')
    SQLALCHEMY_TRACK_MODIFICATIONS = False

config = {
    'development': DevelopmentConfig,
    'testing': TestingConfig
}

SECRET_KEY等一些機密資訊可以用環境變數導入,更安全,為了方便,而且沒有機密,所以就直接寫入

切記不要把密碼以及機密資訊寫在版本控制的設定檔中

上面程式碼中也可以看到SQLALCHEMY_DATABASE_URIdevelopmenttesting中是不同的值,這樣可以在不同環境中使用不同的資料庫,以免原本的資料庫被覆寫

啟動

  • 重構後在根目錄建立.flaskenvflask知道哪個是啟動的py檔案

FLASK_APP=main.py

# main.py
from app import create_app
 
app = create_app(config_name='development')

Flask重構-使用工廠方法和藍圖
https://f88083.github.io/2024/01/09/Flask重構-使用工廠方法和藍圖/
作者
Simon Lai
發布於
2024年1月9日
許可協議