Heim  >  Artikel  >  Backend-Entwicklung  >  Das Flask-Framework von Python zum Aufbau der Struktur großer Webanwendungen

Das Flask-Framework von Python zum Aufbau der Struktur großer Webanwendungen

高洛峰
高洛峰Original
2017-03-03 15:14:321415Durchsuche

Obwohl Flask ein Framework ist, das für seine Leichtigkeit bekannt ist, bietet es auch viele praktische Funktionen wie Unit-Tests und Datenbankmigration für große Webanwendungen. Hier werfen wir einen Blick auf die Verwendung des Flask-Frameworks von Python zum Erstellen großer Webanwendungen . Beispiel für die Struktur eines Programms:

Während es für kleine Webanwendungen praktisch sein kann, ein einziges Skript zu haben, lässt sich dieser Ansatz nicht gut skalieren. Da Anwendungen immer komplexer werden, kann die Verarbeitung innerhalb einer einzigen großen Quelldatei problematisch werden.

Im Gegensatz zu den meisten anderen Web-Frameworks gibt es bei Flask keine spezielle Möglichkeit, große Projekte zu organisieren; die Struktur der Anwendung wird vollständig den Entwicklern selbst überlassen. In diesem Kapitel wird eine Möglichkeit vorgestellt, die Pakete und Module einer großen Anwendung zu organisieren und zu verwalten. Diese Struktur wird in den restlichen Beispielen des Buches verwendet.

1. Projektstruktur

Beispiel für eine grundlegende Multi-File-Flask-Anwendungsstruktur

|-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

Diese Struktur verfügt über vier Verzeichnisse der obersten Ebene:

  • Flask-Anwendungen werden im Allgemeinen in einem Verzeichnis namens app abgelegt. Das Verzeichnis

  • migrations enthält das Datenbankmigrationsskript, das mit dem zuvor erwähnten identisch ist.

  • Unit-Tests werden im Testverzeichnis abgelegt

  • Das venv-Verzeichnis enthält die virtuelle Python-Umgebung, die mit der zuvor erwähnten identisch ist.

Es gibt auch einige neue Dateien:

  • requirements.txt listet einige abhängige Pakete auf, damit sie problemlos auf verschiedenen Computern installiert werden können identische virtuelle Umgebung auf

  • config.py speichert einige Konfigurationseinstellungen.

  • manage.py wird zum Starten von Anwendungen und anderen Anwendungsaufgaben verwendet.

Um Ihnen zu helfen, diese Struktur vollständig zu verstehen, wird im Folgenden der gesamte Prozess der Änderung der hello.py-Anwendung beschrieben, um sie an diese Struktur anzupassen.

2. Konfigurationsoptionen
Anwendungen erfordern normalerweise mehrere Konfigurationseinstellungen. Das beste Beispiel ist die Notwendigkeit, während des Entwicklungsprozesses unterschiedliche Datenbanken, Test- und Produktionsumgebungen zu verwenden, damit diese sich nicht gegenseitig beeinträchtigen können.

Wir können eine Hierarchie von Konfigurationsklassen anstelle der einfachen wörterbuchähnlichen Strukturkonfiguration in hello.py verwenden. Die Datei config.py wird unten angezeigt.

config.py: Anwendungskonfiguration

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 = &#39;Flasky Admin <flasky@example.com>&#39; 
  FLASKY_ADMIN = os.environ.get(&#39;FLASKY_ADMIN&#39;)
  
  @staticmethod
  def init_app(app): 
    pass

class DevelopmentConfig(Config): 
  DEBUG = True

  MAIL_SERVER = &#39;smtp.googlemail.com&#39;
  MAIL_PORT = 587
  MAIL_USE_TLS = True
  MAIL_USERNAME = os.environ.get(&#39;MAIL_USERNAME&#39;)
  MAIL_PASSWORD = os.environ.get(&#39;MAIL_PASSWORD&#39;) 
  SQLALCHEMY_DATABASE_URI = os.environ.get(&#39;DEV_DATABASE_URL&#39;) or \
    &#39;sqlite:///&#39; + os.path.join(basedir, &#39;data-dev.sqlite&#39;)

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

class ProductionConfig(Config):
  SQLALCHEMY_DATABASE_URI = os.environ.get(&#39;DATABASE_URL&#39;) or \
    &#39;sqlite:///&#39; + os.path.join(basedir, &#39;data.sqlite&#39;)

config = {
  &#39;development&#39;: DevelopmentConfig,
  &#39;testing&#39;: TestingConfig,
  &#39;production&#39;: ProductionConfig,
  &#39;default&#39;: DevelopmentConfig
}

Config-Basisklasse enthält einige identische Konfigurationen; verschiedene Unterklassen definieren unterschiedliche Konfigurationen. Bei Bedarf kann eine zusätzliche Konfiguration hinzugefügt werden.

Um die Konfiguration flexibler und sicherer zu gestalten, können einige Einstellungen aus Umgebungsvariablen importiert werden. Beispielsweise kann SECRET_KEY aufgrund seiner Sensibilität in der Umgebung festgelegt werden, es muss jedoch ein Standardwert bereitgestellt werden, wenn er nicht in der Umgebung definiert ist.

Der Variablen SQLALCHEMY_DATABASE_URI können in den drei Konfigurationen unterschiedliche Werte zugewiesen werden. Auf diese Weise kann die Anwendung in verschiedenen Konfigurationen ausgeführt werden, die jeweils eine andere Datenbank verwenden.

Eine Konfigurationsklasse kann eine statische init_app()-Methode definieren, die eine Anwendungsinstanz als Parameter verwendet. Hier ist eine konfigurationsspezifische Initialisierung möglich. Hier implementiert die Config-Basisklasse eine leere init_app()-Methode.

Am Ende des Konfigurationsskripts werden diese verschiedenen Konfigurationen im Konfigurationswörterbuch registriert. Registrieren Sie eine der Konfigurationen (die Entwicklungskonfiguration) als Standardkonfiguration.

3. Anwendungspaket
Das Anwendungspaket platziert den gesamten Anwendungscode, die Vorlagen und die statischen Dateien. Sie heißt einfach App oder kann auf Wunsch mit einem anwendungsspezifischen Namen versehen werden. Die Vorlagen und statischen Verzeichnisse sind Teil der Anwendung, daher sollten diese beiden Verzeichnisse in der App platziert werden. Datenbankmodelle und E-Mail-Unterstützung sind ebenfalls in dieses Paket integriert, jeweils in eigenen Modulen in Form von app/models.py und app/email.py.

3.1. Eine Anwendungsfabrik verwenden

Eine Anwendung in einer einzigen Datei zu erstellen ist sehr praktisch, hat aber einen großen Nachteil. Da die Anwendung im globalen Bereich erstellt wird, gibt es keine Möglichkeit, sich dynamisch an Änderungen in der Anwendungskonfiguration anzupassen: Zum Zeitpunkt der Ausführung des Skripts wurde die Anwendungsinstanz bereits erstellt, sodass es zu spät ist, die Konfiguration zu ändern. Dies ist besonders wichtig für Unit-Tests, da es manchmal notwendig ist, die Anwendung in verschiedenen Konfigurationen auszuführen, um eine bessere Testabdeckung zu erhalten.

Die Lösung für dieses Problem besteht darin, die Anwendung in eine Factory-Funktion zu integrieren, um die Erstellung zu verzögern, sodass sie explizit vom Skript aufgerufen werden kann.

Dadurch hat das Skript nicht nur genügend Zeit, die Konfiguration einzurichten, es kann auch zum Erstellen mehrerer Instanzen der Anwendung verwendet werden – was beim Testen sehr nützlich ist. Die im Konstruktor des App-Pakets definierte Anwendungs-Factory-Funktion wird in Beispiel 7-3 gezeigt.

Dieser Konstruktor importiert die meisten Erweiterungen, die Sie derzeit verwenden müssen. Da es jedoch keine Anwendungsinstanz gibt, um sie zu initialisieren, kann sie erstellt, aber nicht initialisiert werden, indem keine Argumente an ihren Konstruktor übergeben werden. create_app() ist die Anwendungs-Factory-Funktion und muss den für die Anwendung verwendeten Konfigurationsnamen übergeben. Die Einstellungen in der Konfiguration werden in einer Klasse in config.py gespeichert und können direkt mit der from_object()-Methode des app.config-Konfigurationsobjekts von Flask importiert werden. Konfigurationsobjekte können anhand des Objektnamens aus dem Konfigurationswörterbuch ausgewählt werden. Sobald die Anwendung erstellt und konfiguriert ist, kann die Erweiterung initialisiert werden. Erstellen Sie die Initialisierungsarbeiten und schließen Sie sie ab, bevor Sie init_app() in der Erweiterung aufrufen.

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(&#39;main&#39;, __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(&#39;404.html&#39;), 404

@main.app_errorhandler(500) 
def internal_server_error(e):
  return render_template(&#39;500.html&#39;), 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(&#39;/&#39;, methods=[&#39;GET&#39;, &#39;POST&#39;]) 
def index():
  form = NameForm()
  if form.validate_on_submit():
    # ...
    return redirect(url_for(&#39;.index&#39;)) 
  return render_template(&#39;index.html&#39;,
              form=form, name=session.get(&#39;name&#39;),
              known=session.get(&#39;known&#39;, 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(&#39;FLASK_CONFIG&#39;) or &#39;default&#39;) 
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(&#39;db&#39;, MigrateCommand)

if __name__ == &#39;__main__&#39;: 
  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(&#39;testing&#39;)
    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[&#39;TESTING&#39;])

编写好的测试使用的是来自于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(&#39;tests&#39;) 
  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应用程序的结构相关文章请关注PHP中文网!

Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn