angle-uparrow-clockwisearrow-counterclockwisearrow-down-uparrow-leftatcalendarcard-listchatcheckenvelopefolderhouseinfo-circlepencilpeoplepersonperson-fillperson-plusphoneplusquestion-circlesearchtagtrashx

Zwei Flask Apps, Frontend und Admin, auf einer Domain mit DispatcherMiddleware

Mit der Dispatcher-Middleware von Werkzeug kombinieren wir zwei Apps zu einer größeren und versenden basierend auf einem Präfix in der URL.

9 Oktober 2019
post main image
unsplash.com/@ytcount

Die Flask Anwendung, die ich schreibe, um diese Website auszuführen, hat den gesamten Code in einer einzigen'App'. Ich habe bereits einige Reorganisationen durchgeführt, da ich eine vollständige Trennung von Frontend-Code und Verwaltungscode wollte. Jetzt ist es Zeit für eine vollständige Trennung, d.h. das Frontend zu einer Flask App und der Admin zu einer anderen Flask App zu machen, während beide in der gleichen Domäne laufen und sich beide im selben Projektverzeichnis befinden. Da wir den Code und die Daten, die zwischen beiden Apps geteilt werden, nicht duplizieren wollen, erstellen wir ein "gemeinsames Verzeichnis", in dem die statischen Elemente, das Datenmodell usw. leben.

Die Dispatcher-Middleware-Lösung verwendet nur eine Instanz von gunicorn. Wahrscheinlich gibt es andere Möglichkeiten, dies zu tun, z.B. mehrere Instanzen von Schießpulver, die jeweils eine App bedienen, aber ich habe das nicht untersucht.

Zwei Apps

Wir haben zwei Flask Apps im selben Projektverzeichnis. Eine wird als Frontend und die andere als Admin bezeichnet. Beide Apps laufen auf der gleichen Domäne und das Präfix'admin' wird verwendet, um Anfragen entweder an die Frontend-App oder die Admin-App zu senden. Angenommen, der Port ist 5000 und dann Anfrage:

http://127.0.0.1:5000/

wird an die Frontend-Applikation gesendet und angefordert:

http://127.0.0.1:5000/admin

Bevor wir das Application Dispatching auf die eigentliche Anwendung anwenden, wollen wir zunächst testen, ob dies wirklich funktioniert. Dazu habe ich eine virtuelle Umgebung erstellt und installiert Flask und Gunicorn:

pip3 install flask
pip3 install gunicorn

In der virtuellen Umgebung habe ich die folgende Verzeichnisstruktur erstellt. Dies zeigt bereits die Dateien an, die ich verwenden werde:


│
├── flask_dispatch
│   ├── bin
│   ├── include
│   ├── lib
│   ├── lib64
│   ├── share
│   ├── pyvenv.cfg
│   │
│   ├── project
│   │   ├── app_admin
│   │   │   └── __init__.py
│   │   ├── app_frontend
│   │   │   └── __init__.py
│   │   ├── __init__.py
│   │   ├── run_admin.py
│   │   ├── run_both.py
│   │   ├── run_frontend.py
│   │   ├── wsgi_admin.py
│   │   ├── wsgi_both.py
│   │   └── wsgi_frontend.py

Es gibt zwei Apps, Frontend und Admin. Die Frontend-App befindet sich im Verzeichnis app_frontend. Sie besteht aus nur einer Datei __init__.py:

# app_frontend/__init__.py

from flask import Flask, request

def create_app():
    app_name = 'frontend'
    print('app_name = {}'.format(app_name))

    # create app
    app = Flask(__name__, instance_relative_config=True)

    @app.route("/")
    def hello():
        return 'Hello ' + app_name + '! request.url = ' + request.url
    
    # return app
    return app

Die Admin-App befindet sich im Verzeichnis app_admin. Sie ist fast identisch mit der Frontend-Applikation. In beiden Apps habe ich den Anwendungsnamen fest programmiert, um sicherzustellen, dass wir wirklich die richtige App sehen:

# app_admin/__init__.py

from flask import Flask, request

def create_app():
    app_name = 'admin'
    print('app_name = {}'.format(app_name))

    # create app
    app = Flask(__name__, instance_relative_config=True)

    @app.route("/")
    def hello():
        return 'Hello ' + app_name + '! request.url = ' + request.url
    
    # return app
    return app

Führen Sie die beiden Apps mit dem Entwicklungsserver von 's Flaskaus.

Um zu überprüfen, ob sie laufen können, habe ich zwei Dateien erstellt, run_frontend.py und run_admin.py:

# run_frontend.py

# import frontend
from app_frontend import create_app as app_frontend_create_app
frontend = app_frontend_create_app()

if __name__ == '__main__':
    frontend.run(host='0.0.0.0')
# run_admin.py

# import admin
from app_admin import create_app as app_admin_create_app
admin = app_admin_create_app()

if __name__ == '__main__':
    admin.run(host='0.0.0.0')

Geben Sie im Projektverzeichnis den folgenden Befehl ein:

python3 run_frontend.py

Damit wird der FlaskEntwicklungsserver gestartet:

app_name = frontend
 * Serving Flask app "app_frontend" (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: off
 * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)

Zeigen Sie dann Ihren Browser auf:

http://127.0.0.1:5000/

und Sie sollten den folgenden Text sehen:

Hello frontend! request.url = http://127.0.0.1:5000/

Um den Admin zu überprüfen, geben Sie in das Projektverzeichnis ein:

python3 run_admin.py

und zeigen Sie auf Ihren Browser:

http://127.0.0.1:5000/

und du solltest sehen:

Hello admin! request.url = http://127.0.0.1:5000/

Das war nicht wirklich besonders, wir haben jetzt zwei funktionierende Apps.

Führen Sie die beiden Apps mit dem Gunicorn WSGI-Server aus.

Um die beiden mit dem Gunicorn Server ausführen zu können, habe ich wieder zwei Dateien erstellt:

# wsgi_frontend.py

from run_frontend import frontend
# wsgi_admin.py

from run_admin import admin

Jetzt betreiben wir den Gunicorn Server. Ich gehe hier nicht auf alle Details ein, Sie sollten sich vielleicht über die Konfigurationsoptionen von 's Gunicorninformieren, einschließlich des Arbeitsverzeichnisses/Python Pfades. Das Wichtigste dabei ist, dass wir mit dem absoluten Pfad beginnen Gunicorn müssen.

/var/www/.../flask_dispatch/bin/gunicorn -b :5000 wsgi_frontend:frontend

Das Terminal sollte angezeigt werden:

[2019-10-09 11:07:31 +0200] [28073] [INFO] Starting gunicorn 19.9.0
[2019-10-09 11:07:31 +0200] [28073] [INFO] Listening at: http://0.0.0.0:5000 (28073)
[2019-10-09 11:07:31 +0200] [28073] [INFO] Using worker: sync
[2019-10-09 11:07:31 +0200] [28076] [INFO] Booting worker with pid: 28076
app_name = frontend

Zeigen Sie auf Ihren Browser:

http://127.0.0.1:5000/

und du solltest sehen:

Hello frontend! request.url = http://127.0.0.1:5000/

Du kannst das Gleiche für den Admin tun. Nichts Besonderes, wir haben jetzt zwei Apps, von Gunicorndenen beide bedient werden können.

Anwendungs-Dispatching mit dem Entwicklungsserver des Unternehmens Flask

Mit der DispatcherMiddleware von Werkzeug ist es sehr einfach, beide Anwendungen zu einer zu kombinieren, die vom gunicorn WSGI HTTP-Server bedient werden kann. Dies ist im Flask Dokument Application Dispatching beschrieben, siehe Referenzen weiter unten. Beachten Sie, dass DispatcherMiddleware ab Werkzeug 0.15 von werkzeug.wsgi nach werkzeug.middleware.dispatcher verschoben wurde. Auch dies wollen wir zunächst mit dem FlaskEntwicklungsserver von 's testen. Dazu habe ich eine Datei run_both.py erstellt:

# run_both.py

from werkzeug.middleware.dispatcher import DispatcherMiddleware
from werkzeug.serving import run_simple

# import frontend
from app_frontend import create_app as app_frontend_create_app
frontend = app_frontend_create_app()

# import admin
from app_admin import create_app as app_admin_create_app
admin = app_admin_create_app()

# merge
application = DispatcherMiddleware(
    frontend, {
    '/admin': admin
})

if __name__ == '__main__':
    run_simple(
        hostname='localhost',
        port=5000,
        application=application,
        use_reloader=True,
        use_debugger=True,
        use_evalex=True)

Das DispatcherMiddleware-Objekt hat keine Methode'run'. Stattdessen können wir 'run_simple' verwenden.

Nach dem Start des FlaskEntwicklungs-Servers:

python3 run_both.py

Du solltest das sehen:

app_name = frontend
app_name = admin
 * Running on http://localhost:5000/ (Press CTRL+C to quit)
 * Restarting with stat
app_name = frontend
app_name = admin
 * Debugger is active!
 * Debugger PIN: 136-162-082

Wir zeigen unseren Browser auf:

http://127.0.0.1:5000/

sehen wir:

Hello frontend! request.url = http://127.0.0.1:5000/

Und wenn wir mit unserem Browser darauf zeigen:

http://127.0.0.1:5000/admin/

sehen wir:

Hello admin! request.url = http://127.0.0.1:5000/admin/

Großartig, wir haben beide Apps, die auf einer einzigen Domain laufen, 127.0.0.0.1, und das Präfix sendet den Request entweder an die Frontend-App oder die Admin-App.

Anwendungs-Dispatching mit dem Gunicorn Server

Um beide Anwendungen mit dem Gunicorn Server auszuführen, habe ich die Datei wsgi_both.py erstellt:

# wsgi_both.py

from run_both import application

Nach dem Start des Gunicorn Servers:

/var/www/.../flask_dispatch/bin/gunicorn -b :5000 wsgi_both:application

die das Terminal anzeigt:

[2019-10-09 11:17:25 +0200] [28508] [INFO] Starting gunicorn 19.9.0
[2019-10-09 11:17:25 +0200] [28508] [INFO] Listening at: http://0.0.0.0:5000 (28508)
[2019-10-09 11:17:25 +0200] [28508] [INFO] Using worker: sync
[2019-10-09 11:17:25 +0200] [28511] [INFO] Booting worker with pid: 28511
app_name = frontend
app_name = admin

Nun noch einmal, indem Sie den Browser auf:

http://127.0.0.1:5000/

zeigt:

Hello frontend! request.url = http://127.0.0.1:5000/

und zeigt den Browser auf:

http://127.0.0.1:5000/admin/

zeigt:

Hello admin! request.url = http://127.0.0.1:5000/admin/

script_root und Pfade

Es ist wichtig zu verstehen, dass beim Aufruf der Admin-URL durch den Dispatcher die script_root (request.script_root) der Anwendung von leer nach '/admin' wechselt. Auch der Pfad (request.path) enthält nicht '/admin'.

(Siehe Link unten: ) 'Der Pfad ist der Pfad innerhalb Ihrer Anwendung, auf dem das Routing durchgeführt wird. Das script_root befindet sich außerhalb Ihrer Anwendung, wird aber von url_for behandelt.'

Da wir normalerweise url_for() nur für die URL-Generierung verwenden, gibt es kein Problem. Wenn Sie jedoch in Ihrer Anwendung einen Flask URL-Pfad wie request.path, current_app.static_url_path verwenden, müssen Sie diesem mit der script_root voranstellen. Ein Beispiel für die Verwendung des Pfades in einer Vorlage, bevor:

    {{ request.path }}

danach:

    {{ request.script_root + request.path }}

Wenn Sie nicht wissen, was Sie tun, versuchen Sie, den Flask URL-Pfad nicht direkt im Code zu verwenden und verwenden Sie url_for().

Teilen von statischen Elementen

Blog-Posts können ein oder mehrere Bilder enthalten. Das Frontend bedient die Bilder aus seinem statischen Ordner. Der Admin enthält Funktionen zum Hochladen eines Bildes und zum Zuweisen eines Bildes zu einem Blogbeitrag. Um die Dinge einfach zu halten, verschiebe ich den statischen Ordner in den Ordner, in dem sich die Ordner app_frontend und app_admin befinden, so dass er nicht nur freigegeben wird, sondern auch geteilt aussieht.

Die einzige Sache, die wir ändern müssen, um diese Arbeit zu machen, ist, den static_folder beim Erstellen des Flask Objekts zu übergeben:

    app = Flask(__name__, 
        instance_relative_config=True,
        static_folder='/home/flask/project/shared/static')

Dies geschieht nur zur Entwicklung.

Sharing-Konstanten und Datenmodell

Du solltest niemals Code duplizieren. Die Konstanten und das Datenmodell gehören zu den ersten Dingen, die wir zwischen Frontend und Admin teilen. Wir haben die app_constants.py und models.py in das gemeinsame Verzeichnis gelegt. Danach ersetzen wir die Verweise auf sie in den Anwendungsdateien:

from shared.app_constants import *
from shared.models import <classes to import>

Blueprints teilen

Eine Reihe von Blueprints können zwischen Frontend und Administrator geteilt werden. Eine davon ist der auth Blueprint, mit dem man sich ein- und ausloggen kann. Ein weiterer ist der Seiten Blueprint, der Seiten anzeigt. Blueprints zu teilen ist einfach, wir erstellen einfach ein'shared/ 'blueprintsVerzeichnis und legen das blueprints hier ab. Im Frontend und in der create_app() Funktion des Administrators in __init__.py ändern wir:

    from .blueprints.auth.views import auth_blueprint
    app.register_blueprint(auth_blueprint, url_prefix='/<lang_code>')

zu:

    from shared.blueprints.auth.views import auth_blueprint
    app.register_blueprint(auth_blueprint, url_prefix='/<lang_code>')

Die View-Funktionen im blueprints Aufruf render_template , d.h. wir müssen sicherstellen, dass die Vorlagen sowohl im Frontend als auch im Admin-Bereich vorhanden sind. Später können wir auch das Vorlagenverzeichnis für diese freigegebenen blueprints.

Probleme mit dem Logger

Ich verwende den Logger sowohl in der Frontend-App als auch in der Admin-App, die Frontend-App loggt in einer Datei app_frontend.log und die Admin-App loggt in einer Datei app_admin.log.

Nach der Verwendung von DispatcherMiddleware stellte sich heraus, dass immer eine beliebige Protokollnachricht in beide Protokolldateien geschrieben wurde, Nachrichten von der Frontend-App in app_frontend.log und app_admin.log geschrieben wurden und Nachrichten von der Admin-App in app_frontend.log und app_admin.log geschrieben wurden.

Es scheint, dass dies mit der Tatsache zu tun hat, dass app.logger immer den Namen flask.app hat. Obwohl es Wege gibt, dies zu umgehen, ist es besser, auf 1.1 (oder 1.1.1) zu aktualisieren Flask , wobei app.logger nun den gleichen Namen wie app.name trägt. Nach dem Upgrade wurde die Protokollierung für Frontend und Admin getrennt.

Statisches Verzeichnis für staging und Produktion

Ich benutze Gunicorn mit einer Nginx Rückseite proxy. Für die Seiten funktioniert das gut, aber das statische Verzeichnis wurde nicht richtig zugeordnet. Bilder wurden im Admin-Modus, d.h. auf der URL'/admin', nicht angezeigt. Ich kenne keinen anderen Weg, als eine weitere Standortdirektive zur Nginx Kompensation des /admin hinzuzufügen. Also gut:

  location /static/ {
    alias /var/www/.../static/;
  }

Und nach der Verwendung von DispatcherMiddleware:

  location /admin/static/ {
    alias /var/www/.../static/;
  }

  location /static/ {
    alias /var/www/.../static/;
  }

Zusammenfassung

Mit der DispatcherMiddleware von Werkzeug ist es einfach, zwei Apps auf einer Domain laufen zu lassen. Für die Entwicklung ist es wahrscheinlich eine gute Idee, zwei Flask Entwicklungsserver zu verwenden, einen für die Frontend-App und einen für die Admin-App.

Erste Schritte, die dies in meiner Anwendung implementieren, gaben nur wenige Dinge, die behoben werden mussten. Was ich getan habe, war, den App-Ordner nach app_frontend zu verschieben und app_frontend nach app_admin zu kopieren. Im Entwicklungsmodus musste in hostname='localhost' auf hostname='0.0.0.0.0.0' geändert werden, aber das ist ein Problem beim Ausführen mit docker. Als nächstes musste ich die Namen der Session-Cookies ändern, um Konflikte zu vermeiden, '/admin' aus dem url_prefix in der Funktion register_blueprint zu entfernen, die Protokolldateien umzubenennen und den Speicherort der Modelle zu ändern, 'von App-Importmodellen' zu 'von App_frontend-Importmodellen'. Viele Änderungen können durch die Verwendung von rekursivem regex-Ersatz vorgenommen werden, siehe'regexxer' für Linux/Ubuntu.

Dann lief es. Laufen zwei Apps auf der gleichen Domain und werden sie von Ihnen selbst geschrieben, immer vollständig getrennt? Ich glaube nicht. Ich habe einen gemeinsamen Ordner eingeführt, in dem wir den statischen Ordner ablegen. Der gemeinsame Ordner wird auch verwendet, um das Datenmodell und einige Codes zwischen Frontend und Administrator zu teilen.

Zeit, um Admin-Code aus dem Frontend und Frontend-Code aus dem Admin zu entfernen und zu teilen, was freigegeben werden muss!

Links / Impressum

Add a prefix to all Flask routes
https://stackoverflow.com/questions/18967441/add-a-prefix-to-all-flask-routes

Application Dispatching
https://flask.palletsprojects.com/en/1.1.x/patterns/appdispatch/

DispatcherMiddleware with different loggers per app in flask 1.0 #2866
https://github.com/pallets/flask/issues/2866

Flask 1.1 Released
https://palletsprojects.com/blog/flask-1-1-released/

How do I run multiple python apps in 1 command line under 1 WSGI app?
https://www.slideshare.net/onceuponatimeforever/how-do-i-run-multiple-python-apps-in-1-command-line-under-1-wsgi-app

How to implement Flask Application Dispatching by Path with WSGI?
https://stackoverflow.com/questions/30906489/how-to-implement-flask-application-dispatching-by-path-with-wsgi

request.path doesn't include request.script_root when running under a subdirectory #3032
https://github.com/pallets/flask/issues/3032

Serving WSGI Applications
https://werkzeug.palletsprojects.com/en/0.15.x/serving/

Einen Kommentar hinterlassen

Kommentieren Sie anonym oder melden Sie sich zum Kommentieren an.

Kommentare (3)

Eine Antwort hinterlassen

Antworten Sie anonym oder melden Sie sich an, um zu antworten.

avatar

Very helpful. Thanks.
One thing:
http://127.0.0.1:8000/en/blog/two-flask-apps-frontend-and-admin-on-one-domain-using-dispatchermiddleware
should be
https://www.peterspython.com/en/blog/two-flask-apps-frontend-and-admin-on-one-domain-using-dispatchermiddleware

avatar
user59176594 3 Jahre vor

I would like to integrate several DashApps into a website running under Flask (the html frame comes from Flask, the DashApp should be embedded in the html frame). I want to avoid the iframe method.
The DashApps should not create their own Flask instance, as they normally do, but should be transferred to the Flask instance of the running website.
Can that be done with the Dispatcher Middleware?

avatar
peter 3 Jahre vor user59176594

I do not think this is possible (without iframes) because DispatcherMiddleware is used for a different purpose. Did you check this information on the plotly site: https://dash.plotly.com/integrating-dash