【Flask】 項目結構說明


項目結構

  Flask的一大優勢就是其極其輕量化。但是也需要注意到,如果我們要用Flask做一個大項目的話,把所有代碼寫在一個文件里肯定是不合適的。非常難以維護。但是和Django這種框架又不一樣,Flask並沒有規定項目一定要遵從某種必須遵守的目錄結構。最終,人們在長期的實踐中得到一些比較好用因此約定俗成的目錄結構。

  一個典型的flask項目的目錄結構是這樣的(再次明確,不是強制的,而是約定俗成的一種結構):

  

   這種結構有四個頂級文件夾,主體的程序代碼都放在app包中;migrations文件夾中一般存放數據庫遷移腳本;單元測試的編寫放在tests目錄中;venv文件夾包含了python的虛擬環境。

  再來看下最外層的幾個文件。config.py中包含了整個項目啟動時需要知道的配置信息,debug等級(開發、測試還是生產等);manage.py用於啟動程序以及其他程序任務。requirements.txt中記錄了所有依賴包,這樣可以便於讓其他主機重新生成相同的虛擬環境。下面將一個個剖開分析:

■  配置選項

  把所有代碼寫在一個文件中的一個大壞處就是沒有辦法動態地進行配置。一旦程序開始跑了,就沒有回頭路了。為了區別不同環境下對程序不同的配置需求,在這里添加了一個config.py這個文件。雖然具體的配置管理也沒有規定形式,但是這里介紹一種相比於用字典格式來進行管理更加高級的方式,用具有層級的配置類來管理。比如我們可以設計這樣一個類:

import os

class Config:
    SECRET_KEY = os.getenv('SECRET_KEY') or 'some string'
    SQLALCHEMY_COMMIT_ON_TEARDOWN = True

    @staticmethod
    def init_app(app):
        pass

class DevelopmentConfig(Config):
    '''開發環境配置'''
    DEBUG = True
    SQLALCHEMY_DATABASE_URI = os.environ.get('DEV_DATABASE_URI')

class TestingConfig(Config):
    '''測試環境配置'''
    TESTING = True
    SQLALCHEMY_DATABASE_URI = xxxx

class ProductionConfig(Config):
    SQLALCHEMY_DATABASE_URI = xxx

config = {
    'development':DevelopmentConfig,
    'testing':TestingConfig,
    'production':ProductionConfig,
    'default':DevelopmentConfig
}#用一個字典來統合地給出所有配置

  Config基類中的靜態方法init_app以程序實例為參數,這個方法后面還會再提到。得到了這些不同環境下的Config類之后,可以怎么用?可以在一些合適的地方from config import config把上面那個字典config給導入進去。然后調用app.config.from_object(config[config_mode])來進行配置的導入。

 

 ■  程序包

  我們把所有的代碼,模板和靜態文件全部都放到程序包中,在上面的這個例子中,程序包是app目錄,如果想的話也可以將app換成其他的相關名字。下面簡單說明一下包中每個文件的職責:

  templates和static分別盛放模板文件和靜態文件,都在app目錄下。然后再是email.py和models.py分別存放了電子郵件支持函數和數據庫模型的代碼。(電子郵件這方面之前文章中沒有提到過,其實Flask還有一個拓展模塊flask-mail來更好地支持電子郵件的收發。以后看情況如果有需要再回過頭去看補充)

  app/__init__.py是整個程序包的構造文件。在單個腳本構建項目的時候,往往只能運行一個app實例,而且一旦開始運行就無法回頭修改配置了。而在__init__.py這個構造文件中,我們可以設計一個所謂的“工廠函數”。比如def一個create_app(config_mode)函數,經過一些處理之后返回一個由Flask(__name__)得到的app實例對象。工廠函數可以幫助在想要實例的時候創建一個實例出來,並且函數可以接受一些配置選項作為參數,這也實現了配置的靈活加載。比如下面這一個例子__init__.py:

from flask import Flask,render_template
from flask.ext.bootstrap import Bootstrap
from flask.exit.moment import Moment
from flask.ext.sqlalchemy import SQLAlchemy
from config import config    #config就默認是上面寫過的那個config啦

bootstrap = Bootstrap()
moment = Moment()
db = SQLAlchemy()    #這里創建的三個擴展組件的對象都還是空對象,沒有約束具體的app對象

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

    bootstrap.init_app(app)
    moment.init_app(app)
    db.init_app(app)
    #現在三個插件約束了app

    '''要在這里添加一些路由和自定義錯誤處理的信息,按照下文的說明,可以注冊一個藍本對象'''
from .main import main as main_blueprint
app.register_blueprint(main_blueprint)
return app

 

  可以想象得到,以后在manage.py這個啟動腳本中可以from app import create_app,然后根據自己的需要來create_app('development')之類的得到app,這樣的一個app是自帶了一整套運行用的插件以及合適的配置的,就可以方便地讓app.run了。

  ●  藍本

  因為工廠函數返回的是一個app實例,如果沒有定義路由的話這個app實例也沒什么用。但直接在create_app函數中定義路由和錯誤處理(錯誤處理因為用app.errorhandler裝飾器,和路由設置是類似的)顯然不太合理。一個比較好的解決方案是利用藍本。藍本是一種概念,其體現可以是一個單獨的腳本文件或者一個包,藍本中可以像之前單腳本架構項目時那樣定義路由和錯誤處理。只不過藍本中的路由處於休眠狀態,只有當藍本被注冊到app實例上去之后路由才被激活然后開始發揮作用。

  在上面舉例的目錄結構中,藍本是main這個子包。在子包的構造文件__init__.py中可以根據需要來構建藍圖對象以及導入子包內相關的代碼,比如views來定義基礎路由和錯誤處理等,而forms是定義了表單類等等。具體創建的代碼:

from flask import Blueprint
main = Blueprint('main',__name__)    #這里的main只是為藍本取得一個名字,並不一定要和main這個主程序包一致
import views
import errors
#導入路由和錯誤處理文件。

   可以看到,Blueprint類創建實例時需要兩個參數,第一個是創建出來的藍本對象的名稱,這牽扯到后面進行url_for等函數參數的寫法。第二個參數一般寫__name__即可。

  這里有一個不太合理的地方,main/__init__.py這個文件和views.py以及errors.py等一些其他這個包中的文件互相循環導入了。__init__.py中為了把views,errors中的代碼和藍本聯系起來,必須在__init__.py這個文件里面顯式地import兩者,而在views和errors里,藍本的名字就像是單文件應用中的app那樣,需要用來定義路由等,所以也肯定要import 藍本名。這個循環導入的避免方法是在藍本所在的__init__.py文件中,import views和errors的語句放在整個文件的最后面。這樣可以保證外界的調用者先進來調用了__init__.py,確定建立了main這個對象之后,再導入后面兩個,后面兩個文件中的main就不會空手套白狼了。至於如views.py的文件的代碼:

from flask import render_template,url_for,redirect
from . import main
from .froms import NameForm
from .models import Student

@main.route('/',methods=['GET','POST'])
def index():
     return render_template('index.html')

@main.route('/form',metods=['GET','POST'])
def form():
    form = NameForm()
    if form.validate_on_submit():
        #...一些對表單的數據的處理
    return render_template('form.html',form=form)

 

  *這里沒有涉及到flask_sqlalchemy的導入,如果引入了flask_sqlalchemy來作為orm插件處理數據庫的話,那么就要注意db.create_all()這個操作。我已開始把這個操作試着放在好幾個地方不過都失敗了,后來我自己從stackoverflow上面看到了一個做法是在views下面寫一個@main.before_app_first_request然后在下面的函數中做create_all。可能不是最好的辦法,但是可以解決db初始化的問題。

  在創建完藍本之后,再回看create_app函數,現在我們不需要定義路由,只需要調用app.register_blueprint(藍本對象)就可以把app和藍本中的代碼動態地關聯。當有需要時才激活那些代碼。在藍本基礎上編寫錯誤處理函數和路由函數的時候要注意,第一修飾器不再是app打頭而是有藍本對象的名字打頭,第二url_for函數的用法會不一樣,在單腳本單應用的項目中,對於響應函數時index()的路由我們可以直接url_for('index')來獲取。但是在藍本中,我們必須得url_for('藍本名.index')來獲取。因為所有在藍本中設定的響應函數都被加載到藍本對象的命名空間中,這樣才能保證不污染外部命名空間並且允許不同藍本有相同名字的響應函數。另外,如果重定向是到本藍本的響應函數的話可以省略藍本名直接寫'.index',而重定向到其他藍本的就一定要寫全。

  用藍本的另一個好處就是,一個app可以關聯多個藍本。每一個藍本可以看作是一個代碼模塊,用來處理一些工作。這樣子就可以實現程序的模塊化管理和編寫了。在把app注冊到不同的藍本上去的時候,可以在register_blueprint方法中添加url_prefix參數,比如url_prefix='/test'。這樣就可以讓這個藍本中所有路由都默認是加在這個前綴的后面。

 

■  命令行啟動項目以及數據庫遷移

  上面說的一個在@main.before_first_app_request下面進行db.create_all的工作,但其實書上的,命令行啟動的方式更加好一點。之前沒有意識到,后來用了下之后發現確實很好用。

  原來我們的manage腳本可能就是簡單的from app import create_app,然后create_app(config_mode),然后就app.run()了。如果之前在config里面提到過debug的配置的話這里的run里面就不用寫debug。這樣做的話就使得啟動腳本只有一個啟動項目的功能,至於數據庫的初始化,數據庫遷移等等工作都沒能做。在flask-script和flask-migrate的支持下,我們的啟動腳本可以寫成這樣:

from app import create_app,db
from app.models import User #這里把需要初始化的表,對應的類拿出來
from flask_script import Manager,Shell
from flask_migrate import MigrateCommand,Migrate

if __name__ == '__main__':
    app = create_app('development')
    manager = Manager(app)
    migrate = Migrate(app,db)   #建立一個遷移數據庫的對象。
    def make_shell_context():
        return dict(app=app,db=db,User=User)
    manager.add_command('shell',Shell(make_context=make_shell_context)) #make_context的意義在於命令行中輸入db就知道是我們創建的那個db
    manager.add_command('db',MigrateCommand)

    manager.run()

 

  有了這樣一個manage.py之后,我們可以在命令行中鍵入python manage.py [命令]。這里面我們設置的命令有

  runserver  啟動項目,相當於原來的app.run(),在后面可以跟一些參數來使得服務器更加個性化。比如默認的服務IP時127.0.0.1,只有本地可訪問。可以在runserver后加上參數--host=0.0.0.0。

  shell  進入有當前app作為上下文的python shell,在這里我們可以直接鍵入db.create_all()以及db.drop_all()來進行表的初始化和清空

  ●  數據庫遷移相關命令

  所謂數據庫遷移,是指我們對model進行某些調整之后,要備份之前的數據庫表結構和一些其他信息。(經過試驗發現當表名發生變化,即使表結構不變,原表中的數據會不見。如果表名保持不變,只是對字段做一些增刪改的話,原表中的數據還是會被調整到新表中。

  db init  進行數據庫遷移倉庫的建立,會自動創建migrations等目錄和必要的文件

  db migrate -m "some message"  用來自動創建一個數據庫遷移腳本,會放在migrations/versions下面。因為是自動生成的不一定可靠,建議自己手動再去改改。

  db upgrade  執行上面自動生成腳本中upgrade函數,相當於是把數據庫表結構更新成當前models中定義的那樣

  db downgrade  執行上面腳本中的downgrade函數,把數據庫表結構回滾一次改變。

  依賴自動生成的腳本進行的數據庫升降級是很死板的,版本只能是一條線性地升降級而不能有任何分支。比如生成自動遷移腳本的時候經常會報個Target database is not up to date之類的錯。這個錯的原因可能是沒有在最新版本的數據庫版本上進行遷移腳本的生成,可能之前生成了一個版本的遷移腳本后來又修改了model而沒有生成遷移腳本,導致現在的遷移腳本指向的版本已經偏老了。測試的時候解決的辦法簡單粗暴,把所有自動生成的遷移腳本都刪除,然后重新db migrate。

 

 我的測試代碼:【https://github.com/wyzypa/Flasky_Test】


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM