Python Flask 概念與實作(三) - 專案檔案分佈

前言

專案檔案的部署這一節,原本想說不就是檔案按種類區分就好?殊不知這其中細節水很深,搞得死去活來。最後也做了好很多功課,甚至有想把現有專案砍掉重來的念頭。但後來發現若只是單純重新建新專案只是照本宣科,何不試著修改當前的專案🙄?
關於這節,若你的網站只是單純blog、少許頁面或API等少量功能可以走馬看花略過,
但如果是有意擴大網站的話,要特別注意檔案分佈的問題👻!


目前 Python Flask 概念與實作 大致規劃為

  1. 佈置環境
  2. Flask model運用(一)
  3. Web server概念(二)
  4. 專案檔案分佈(三)
  5. 連接資料庫(四)
  6. 渲染模板(五)
  7. 登入功能(六)
  8. Blog功能(七)

在開發過程中,Test(測試)其實也很重要,但礙於缺乏經驗,若之後有望的話再補上(專案打包、網站優化等等亦同)


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
.
├── README.md # 專案簡介
├── requirements.txt # 套件版本
├── run.py # 啟動
├── config.py # 設定檔
├── .venv # 虛擬環境
├── app
│ ├── __init__.py # 各套件啟動檔
│ ├── routes.py # URL設定
│ ├── models.py # 模型
│ ├── forms.py # 表格
│ ├── static # 存儲css, js, images
│ └── templates # HTML模板
│ ├── base.html
│ ├── footer.html
│ ├── navbar.html
│ ├── index.html
│ └── about.html
└── instance # 本地端資料庫
├── devp-data.sqlite
└── test-data.sqlite

小型的專案大致上會是這個架構,這樣的架構優點是小而精、簡單且一目瞭然,需要增加網頁時僅需在app/routes.py設定URL,同時在app/templates中新增對應的html即可。而隨著網站的成長,會面臨的幾種問題是:

  1. app/routes.py內的URL太多,有時甚至不小心設定兩個相同的URL,導致較晚設定的URL失效
  2. 同樣也是app/routes.py管理上的問題,設定URL前綴時必須逐一URL更改(e.g. /products)
  3. app/templates中的html過多,不易管理
  4. 測試功能時,需更改config.py設定

Application Factory

為了解決上述的問題,Flask提供Application Factory(工廠模式),將app/__init__.py調整為一個由 create_app() 建立Flask app的方法,而不是直接啟用套件。在run.py依需求輸入參數,即可套用對應的config.py設定

  1. 修改config.py
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    # config.py

    import os
    from dotenv import load_dotenv

    basedir = os.path.abspath(os.path.dirname(__file__)) # 當前檔案絕對路徑, __file__: 當前檔案(config.py)
    load_dotenv() # 引用.env檔

    class Config():
    # Database Configuration
    SQLALCHEMY_TRACK_MODIFICATIONS = False
    SECRET_KEY = os.getenv("SECRET_KEY") or 'A_VERY_LONG_SECRET_KEY' # 給form使用

    # user add image of porduct upload_folder
    PRODUCT_IMG_UPLOAD_FOLDER = "app/static/product/items"
    ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'}

    class DevelopmentConfig(Config):
    DEBUG = True
    SQLALCHEMY_DATABASE_URI = os.getenv("DEV_DATABASE_URL") or \
    'sqlite:///' + os.path.join(basedir, 'instance', 'devp-data.sqlite')

    class TestingConfig(Config):
    TESTING = True
    SQLALCHEMY_DATABASE_URI = os.environ.get('TEST_DATABASE_URL') or \
    'sqlite:///' + os.path.join(basedir, 'instance', 'test-data.sqlite')

    class ProductionConfig(Config):
    DEBUG = False
    FLASK_RUN_HOST = "0.0.0.0"
    SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
    'mysql+pymysql://{}:{}@{}/{}'.format(
    os.getenv("MYSQL_USERNAME"),
    os.getenv("MYSQL_PASSWORD"),
    os.getenv("MYSQL_IP"),
    os.getenv("MYSQL_DATABASE")
    )

    config = {
    'devp': DevelopmentConfig, # 開發設定/DB
    'test': TestingConfig, # 測試設定/DB
    'prod': ProductionConfig, # 正式設定/DB
    'default': DevelopmentConfig # 預設(開發)
    }
  2. 修改/app/models.py
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    # app.models.py

    from flask_sqlalchemy import SQLAlchemy
    from flask_login import LoginManager, UserMixin
    from flask_bcrypt import Bcrypt

    # 將套件移至此
    db = SQLAlchemy()
    login = LoginManager()
    bcrypt = Bcrypt()

    @login.user_loader
    def load_user(user_id):
    return User.query.filter_by(id=user_id).first()

    class User(db.Model, UserMixin):
    # 略...
  3. 修改/app/__init__.py
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    # app.__init__.py

    from flask import Flask, render_template
    from flask_bootstrap import Bootstrap4
    from config import config
    from flask_sqlalchemy import SQLAlchemy
    from flask_login import LoginManager
    from flask_bcrypt import Bcrypt

    bootstrap = Bootstrap4()

    def create_app(configname=None):
    app = Flask(__name__)
    if configname == None:
    configname = 'default'
    app.config.from_object(config[configname])

    # 初始化套件
    bootstrap.init_app(app)
    db.init_app(app)
    login.init_app(app)
    bcrypt.init_app(app)

    # login_required 設定
    login.login_view = "signin"
    login.login_message = "You must signin to access this page."
    login.login_message_category = "info"

    ### routes
    # from app.routes import * # 無法一次性引入
    @app.route('/')
    def index():
    return render_template("index.html")

    return app
  4. 修改run.py
    1
    2
    3
    4
    5
    6
    # run.py
    from app import create_app

    if __name__ == "__main__":
    app = create_app() # 預設為devp, 可依需求輸入
    app.run()

如此一來,使用Application Factory即可快速切換設定檔與DB,但這僅只是解決小型專案的第四個問題而已。

Blueprint

Blueprint(藍圖)可以將專案中的功能區塊化,雖說看起來很抽象,但其實在你的專案還沒成長起來前,也不太知道如何區分功能。但好在Blueprint可以輕易地將URL前綴管理的問題輕易解決。
假設網站中除了首頁以外,也有其他功能。e.g. register/login(或是有order(訂單)、cart(購物車)也行)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
.
├── README.md
├── requirements.txt
├── run.py
├── config.py
├── .venv
├── app
│ ├── __init__.py
│ ├── routes.py
│ ├── models.py # 模型(User)
│ ├── forms.py
│ ├── static
│ └── templates # HTML模板
│ ├── base.html
│ ├── footer.html
│ ├── navbar.html
│ ├── index.html
│ ├── about.html
│ ├── register.html # 註冊頁面
│ └── signin.html # 登入頁面
└── instance
├── devp-data.sqlite
└── test-data.sqlite
  1. 在專案下新增/users(與/app平行)
  2. /users下新增__init__.py
    1
    2
    3
    4
    5
    6
    7
    # users.__init__.py
    from flask import Blueprint

    users_bp = Blueprint('users', __name__, template_folder='templates')
    # 'users': blueprint name
    # template_folder: 此blueprint HTML模板引用位置(可選參數)
    from . import routes
  3. /users下新增routes.py
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    # users.routes.py

    from . import users_bp
    from flask import redirect, flash, render_template, url_for
    from app.forms import *

    ### 註冊 ###
    @users_bp.route("/register", methods=["GET", "POST"])
    def register():
    form = RegisterForm()
    if form.validate_on_submit():
    # 略...
    return redirect(url_for('users.signin')) # 跳轉URL時需加上blueprint name前綴
    return render_template("register.html", form=form)

    ### 登入 ###
    @users_bp.route("/signin", methods=["GET", "POST"])
    def signin():
    form = SigninForm()
    if form.validate_on_submit():
    # 略...
    return render_template("sign-in.html", form=form)

    ### 登出 ###
    @users_bp.route("/logout")
    @login_required
    def logout():
    logout_user()
    return redirect(url_for("users.signin"))
  4. /users下新增templates資料夾,並將原/app/templates中的register.htmlsignin.html移至此
  5. 修改register.htmlsignin.html中的url_for(),有使用到users需加前綴(e.g. url_for('signin')改為url_for('users.signin'))
  6. 修改/app/__init__.py
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    # app.__init__.py

    from flask import Flask
    from flask_bootstrap import Bootstrap4
    from config import config
    from app.models import db, login, bcrypt # 改在app.models中實體化
    from .users import users_bp # 引入users blueprint

    bootstrap = Bootstrap4()

    def create_app(configname='test'):
    app = Flask(__name__)
    if configname == None:
    configname = 'default'
    app.config.from_object(config[configname])

    bootstrap.init_app(app)
    db.init_app(app)
    login.init_app(app)
    bcrypt.init_app(app)

    # login_required 設定
    login.login_view = "signin"
    login.login_message = "You must login to access this page."
    login.login_message_category = "info"

    ### routes, blueprint
    app.register_blueprint(users_bp, url_prefix='/users') # url_prefix: URL前綴

    return app
  7. 修改/app/models.py
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    # /app/models.py

    from flask_sqlalchemy import SQLAlchemy
    from flask_login import LoginManager, UserMixin
    from flask_bcrypt import Bcrypt
    from datetime import datetime

    db = SQLAlchemy()
    login = LoginManager()
    bcrypt = Bcrypt()

    @login.user_loader
    def load_user(user_id):
    return Users.query.filter_by(id=user_id).first()

    class Cart(db.Model):
    # 略...

經過上述修改後,當前專案架構:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
.
├── README.md
├── requirements.txt
├── run.py
├── config.py
├── .venv
├── app
│ ├── __init__.py
│ ├── routes.py
│ ├── models.py
│ ├── forms.py
│ ├── static
│ └── templates
│ ├── base.html
│ ├── footer.html
│ ├── navbar.html
│ ├── index.html
│ └── about.html
├── users
│ ├── __init__.py
│ ├── routes.py # blueprint URL
│ └── templates
│ ├── register.html # 註冊頁面
│ └── signin.html # 登入頁面
└── instance
├── devp-data.sqlite
└── test-data.sqlite

藉由使用blueprint即可解決小型專案遇到的1~3問題🥳

Application Factory vs. Blueprint

起初以為Factory與Blueprint都是管理大型專案的一種方法,但後來實作後赫然驚覺各自的功能根本不一樣。Factory是讓開發人員可以快速切換database與config.py設定,而Blueprint是將大型專案功能逐一模組化。

結論

  • 使用Application Factory(工廠模式)快速切換DB與設定
  • 使用Blueprint(藍圖)管理大型專案