Sie haben sich also für ein neues Projekt entschieden. Und dieses Projekt ist eine Webanwendung. Wie lange dauert die Erstellung eines Basisprototyps? Wie schwer ist es Was sollte eine moderne Website von Anfang an können?
In diesem Artikel werden wir versuchen, das Boilerplate einer einfachen Webanwendung mit der folgenden Architektur zu skizzieren:
Was wir behandeln werden:
- Einrichten der Entwicklungsumgebung in Docker-Compose.
- Backend-Erstellung auf Flask.
- Erstellen eines Frontends in Express.
- Erstellen Sie JS mit Webpack.
- Reagieren, Redux und serverseitiges Rendern.
- Aufgabenwarteschlangen mit RQ.
Einführung
Vor der Entwicklung müssen Sie natürlich erst entscheiden, was wir entwickeln! Als Modellanwendung für diesen Artikel habe ich beschlossen, eine primitive Wiki-Engine zu erstellen. Wir werden Karten in Markdown ausstellen lassen; Sie können angesehen werden und (irgendwann in der Zukunft) Änderungen anbieten. All dies werden wir als einseitige Anwendung mit serverseitigem Rendering arrangieren (was für die Indizierung unserer zukünftigen Terabyte an Inhalten unbedingt erforderlich ist).
Schauen wir uns die Komponenten, die wir dafür benötigen, etwas genauer an:
- Kunde Erstellen wir eine einseitige Anwendung (d. H. Mit Seitenübergängen unter Verwendung von AJAX) für das React + Redux- Bundle, das in der Front-End-Welt sehr verbreitet ist.
- Frontend . Erstellen wir einen einfachen Express- Server, der unsere React-Anwendung rendert (alle erforderlichen Daten im Backend asynchron anfordert) und an den Benutzer ausgibt.
- Backend . Unser Backend ist eine kleine Flask-Anwendung. Wir werden Daten (unsere Karten) im beliebten MongoDB- Dokumenten-Repository speichern und für die Task-Warteschlange und möglicherweise in Zukunft für das Caching Redis verwenden .
- ARBEITNEHMER . Ein separater Container für schwere Aufgaben wird von der RQ- Bibliothek gestartet.
Infrastruktur: git
Wahrscheinlich konnten wir nicht darüber sprechen, aber wir werden natürlich die Entwicklung im Git-Repository durchführen.
git init git remote add origin git@github.com:Saluev/habr-app-demo.git git commit --allow-empty -m "Initial commit" git push
(Hier sollten Sie sofort
.gitignore
.)
Der endgültige Entwurf kann
auf Github eingesehen werden. Jeder Abschnitt des Artikels entspricht einem Commit (ich habe viel zurückgewiesen, um dies zu erreichen!).
Infrastruktur: Docker-Compose
Beginnen wir mit der Einrichtung der Umgebung. Angesichts der Fülle an Komponenten wäre es eine sehr logische Entwicklungslösung, Docker-Compose zu verwenden.
Fügen Sie die Datei
docker-compose.yml
folgenden Inhalt zum Repository hinzu:
version: '3' services: mongo: image: "mongo:latest" redis: image: "redis:alpine" backend: build: context: . dockerfile: ./docker/backend/Dockerfile environment: - APP_ENV=dev depends_on: - mongo - redis ports: - "40001:40001" volumes: - .:/code frontend: build: context: . dockerfile: ./docker/frontend/Dockerfile environment: - APP_ENV=dev - APP_BACKEND_URL=backend:40001 - APP_FRONTEND_PORT=40002 depends_on: - backend ports: - "40002:40002" volumes: - ./frontend:/app/src worker: build: context: . dockerfile: ./docker/worker/Dockerfile environment: - APP_ENV=dev depends_on: - mongo - redis volumes: - .:/code
Werfen wir einen kurzen Blick darauf, was hier passiert.
- Ein MongoDB-Container und ein Redis-Container werden erstellt.
- Ein Container für unser Backend wird erstellt (den wir unten beschreiben). Die Umgebungsvariable APP_ENV = dev wird an sie übergeben (wir werden sie untersuchen, um zu verstehen, welche Flask-Einstellungen geladen werden sollen), und ihr Port 40001 wird außerhalb geöffnet (über sie geht unser Browser-Client zur API).
- Ein Container unseres Frontends wird erstellt. Es werden auch verschiedene Umgebungsvariablen eingefügt, die später für uns nützlich sein werden, und Port 40002 wird geöffnet. Dies ist der Hauptport unserer Webanwendung: Im Browser gehen wir zu http: // localhost: 40002 .
- Der Container unseres Arbeiters wird erstellt. Er benötigt keine externen Ports und nur in MongoDB und Redis ist Zugriff erforderlich.
Jetzt erstellen wir Docker-Dateien. Im Moment kommt eine
Reihe von Übersetzungen exzellenter Artikel über Docker nach Habré - Sie können sicher dorthin gehen, um alle Details zu erfahren.
Beginnen wir mit dem Backend.
# docker/backend/Dockerfile FROM python:stretch COPY requirements.txt /tmp/ RUN pip install -r /tmp/requirements.txt ADD . /code WORKDIR /code CMD gunicorn -w 1 -b 0.0.0.0:40001 --worker-class gevent backend.server:app
Es versteht sich, dass wir die Gunicorn Flask-Anwendung durchlaufen und uns unter der Namens-
app
im Modul
backend.server
verstecken.
Nicht weniger wichtig
docker/backend/.dockerignore
:
.git .idea .logs .pytest_cache frontend tests venv *.pyc *.pyo
Der Arbeiter ist im Allgemeinen dem Backend ähnlich, nur haben wir anstelle von Gunicorn den üblichen Start eines Pit-Moduls:
# docker/worker/Dockerfile FROM python:stretch COPY requirements.txt /tmp/ RUN pip install -r /tmp/requirements.txt ADD . /code WORKDIR /code CMD python -m worker
Wir werden die ganze Arbeit in
worker/__main__.py
.
.dockerignore
Worker ist dem
.dockerignore
Backend völlig ähnlich.
Endlich das Frontend. Es gibt einen
separaten Artikel über ihn über Habré, aber nach der
ausführlichen Diskussion über StackOverflow und den Kommentaren im Geiste von "Jungs, ist es schon 2018, gibt es noch keine normale Lösung?" dort ist nicht alles so einfach. Ich habe mich für diese Version der Docker-Datei entschieden.
# docker/frontend/Dockerfile FROM node:carbon WORKDIR /app # package.json package-lock.json npm install, . COPY frontend/package*.json ./ RUN npm install # , # PATH. ENV PATH /app/node_modules/.bin:$PATH # . ADD frontend /app/src WORKDIR /app/src RUN npm run build CMD npm run start
Vorteile:
- Alles wird wie erwartet zwischengespeichert (auf der untersten Ebene - Abhängigkeiten, auf der obersten Ebene - dem Build unserer Anwendung).
docker-compose exec frontend npm install --save newDependency
funktioniert wie es sollte und ändert package.json
in unserem Repository (was nicht der Fall wäre, wenn wir COPY verwenden würden, wie viele Leute vorschlagen). Es wäre ohnehin nicht wünschenswert, npm install --save newDependency
außerhalb des Containers npm install --save newDependency
, da einige Abhängigkeiten des neuen Pakets möglicherweise bereits vorhanden sind und auf einer anderen Plattform erstellt werden (z. B. unter der im Docker und nicht unter unserem funktionierenden Macbook) ), und dennoch möchten wir im Allgemeinen nicht die Anwesenheit von Node auf der Entwicklungsmaschine benötigen. Ein Docker, der sie alle regiert!
Na ja und natürlich
docker/frontend/.dockerignore
:
.git .idea .logs .pytest_cache backend worker tools node_modules npm-debug tests venv
Unser Containerrahmen ist also fertig und Sie können ihn mit Inhalt füllen!
Backend: Flask Framework
Fügen Sie
flask
,
flask-cors
,
gevent
und
gunicorn
zu
gevent
gunicorn
und erstellen Sie eine einfache Flask-Anwendung in
backend/server.py
.
Wir haben Flask
backend.{env}_settings
, die Einstellungen aus der
backend.{env}_settings
Datei
backend.{env}_settings
, was bedeutet, dass wir auch eine (mindestens leere) Datei
backend/dev_settings.py
damit alles abheben kann.
Jetzt können wir unser Backend offiziell ERHÖHEN!
habr-app-demo$ docker-compose up backend ... backend_1 | [2019-02-23 10:09:03 +0000] [6] [INFO] Starting gunicorn 19.9.0 backend_1 | [2019-02-23 10:09:03 +0000] [6] [INFO] Listening at: http://0.0.0.0:40001 (6) backend_1 | [2019-02-23 10:09:03 +0000] [6] [INFO] Using worker: gevent backend_1 | [2019-02-23 10:09:03 +0000] [9] [INFO] Booting worker with pid: 9
Wir gehen weiter.
Frontend: Express Framework
Beginnen wir mit der Erstellung eines Pakets. Nachdem wir den Frontend-Ordner erstellt und
npm init
darin ausgeführt haben, erhalten wir nach einigen ungekünstelten Fragen das fertige package.json im Geiste
{ "name": "habr-app-demo", "version": "0.0.1", "description": "This is an app demo for Habr article.", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "repository": { "type": "git", "url": "git+https://github.com/Saluev/habr-app-demo.git" }, "author": "Tigran Saluev <tigran@saluev.com>", "license": "MIT", "bugs": { "url": "https://github.com/Saluev/habr-app-demo/issues" }, "homepage": "https://github.com/Saluev/habr-app-demo#readme" }
In Zukunft brauchen wir Node.js auf dem Entwicklercomputer überhaupt nicht mehr (obwohl wir
npm init
über Docker ausweichen und starten könnten, aber na ja).
In
Dockerfile
wir
npm run build
und
npm run start
- Sie müssen
package.json
die entsprechenden Befehle
package.json
:
Der Befehl
build
führt noch nichts aus, ist aber für uns weiterhin nützlich.
Fügen Sie
Express- Abhängigkeiten hinzu und erstellen Sie eine einfache Anwendung in
index.js
:
Jetzt erhöht
docker-compose up frontend
unser Frontend! Außerdem sollte auf
http: // localhost: 40002 der Klassiker „Hallo Welt“ bereits zur
Geltung kommen.
Frontend: Mit Webpack und React-Anwendung erstellen
Es ist Zeit, in unserer Anwendung mehr als nur einfachen Text darzustellen. In diesem Abschnitt fügen wir die einfachste React-Komponente der
App
und konfigurieren die Assembly.
Bei der Programmierung in React ist es sehr praktisch,
JSX zu verwenden, einen JavaScript-Dialekt, der durch syntaktische Konstruktionen des Formulars erweitert wird
render() { return <MyButton color="blue">{this.props.caption}</MyButton>; }
JavaScript-Engines verstehen dies jedoch nicht. Daher wird normalerweise die Erstellungsphase zum Frontend hinzugefügt. Spezielle JavaScript-Compiler (yeah-yeah) verwandeln syntaktischen Zucker in
hässliches klassisches JavaScript, verarbeiten Importe, minimieren und so weiter.
2014 Jahr. apt-cache search javaDie einfachste React-Komponente sieht also sehr einfach aus.
Er wird unseren Gruß einfach mit einer überzeugenderen Stecknadel anzeigen.
Fügen Sie die Datei
frontend/src/template.js
die das minimale HTML-Framework unserer zukünftigen Anwendung enthält:
Fügen Sie einen Client-Einstiegspunkt hinzu:
Um all diese Schönheit aufzubauen, brauchen wir:
webpack ist ein modischer Jugendbauer für JS (obwohl ich drei Stunden lang keine Artikel im Frontend gelesen habe, bin ich mir also nicht sicher über die Mode);
babel ist ein Compiler für alle Arten von Lotionen wie JSX und gleichzeitig ein Polyfill-Anbieter für alle IE-Fälle.
Wenn die vorherige Iteration des Frontends noch ausgeführt wird, müssen Sie nur noch etwas tun
docker-compose exec frontend npm install --save \ react \ react-dom docker-compose exec frontend npm install --save-dev \ webpack \ webpack-cli \ babel-loader \ @babel/core \ @babel/polyfill \ @babel/preset-env \ @babel/preset-react
um neue Abhängigkeiten zu installieren. Konfigurieren Sie nun das Webpack:
Damit Babel funktioniert, müssen Sie
frontend/.babelrc
konfigurieren:
{ "presets": ["@babel/env", "@babel/react"] }
Machen Sie unseren Befehl
npm run build
aussagekräftig:
// frontend/package.json ... "scripts": { "build": "webpack", "start": "node /app/server.js", "test": "echo \"Error: no test specified\" && exit 1" }, ...
Jetzt durchläuft unser Client zusammen mit einem Bündel von Polyfills und all seinen Abhängigkeiten babel, kompiliert und faltet sich zu einer monolithisch minimierten Datei
../dist/client.js
. Fügen Sie die Möglichkeit hinzu, es als statische Datei in unsere Express-Anwendung hochzuladen, und in der Standardroute geben wir unser HTML zurück:
Erfolg! Wenn wir jetzt
docker-compose up --build frontend
, sehen wir "Hallo Welt!" In einem neuen, glänzenden Wrapper und wenn Sie die Erweiterung React Developer Tools (
Chrome ,
Firefox ) installiert haben, gibt es in den Entwicklertools auch einen React-Komponentenbaum:

Backend: Daten in MongoDB
Bevor Sie fortfahren und unserer Anwendung Leben einhauchen, müssen Sie sie zuerst in das Backend einatmen. Es scheint, dass wir die in Markdown markierten Karten aufbewahren wollten - es ist Zeit, dies zu tun.
Während
es in Python ORMs für MongoDB gibt, halte ich die Verwendung von ORMs für bösartig und überlasse Ihnen das Studium der geeigneten Lösungen. Stattdessen erstellen wir eine einfache Klasse für die Karte und das zugehörige
DAO :
(Wenn Sie in Python immer noch keine Typanmerkungen verwenden, lesen Sie unbedingt
diese Artikel !)
Erstellen wir nun eine Implementierung der
CardDAO
Schnittstelle, die ein
Database
aus
pymongo
(ja, Zeit, um
pymongo
zur
requirements.txt
hinzuzufügen):
Zeit, die Monga-Konfiguration in den Backend-Einstellungen zu registrieren. Wir haben unseren Container einfach mit
MONGO_HOST = "mongo"
benannt, also
MONGO_HOST = "mongo"
:
Jetzt müssen wir
MongoCardDAO
erstellen und der Flask-Anwendung Zugriff darauf gewähren. Obwohl wir jetzt eine sehr einfache Hierarchie von Objekten haben (Einstellungen → Pymongo-Client → Pymongo-Datenbank →
MongoCardDAO
), erstellen wir sofort eine zentralisierte King-Komponente, die die
Abhängigkeitsinjektion MongoCardDAO
(dies wird sich wieder als nützlich erweisen, wenn wir den Worker und die Tools
MongoCardDAO
).
Zeit, der Flask-Anwendung eine neue Route hinzuzufügen und die Ansicht zu genießen!
Starten Sie mit dem
docker-compose up --build backend
:

Ups ... oh genau. Wir müssen Inhalte hinzufügen! Wir öffnen den Tools-Ordner und fügen ein Skript hinzu, das eine Testkarte hinzufügt:
Der
docker-compose exec backend python -m tools.add_test_content
füllt unsere Monga mit Inhalten aus dem
docker-compose exec backend python -m tools.add_test_content
Container.

Erfolg! Jetzt ist es an der Zeit, dies am Frontend zu unterstützen.
Frontend: Redux
Jetzt wollen wir die Route
/card/:id_or_slug
, über die unsere React-Anwendung geöffnet wird, die Kartendaten aus der API laden und uns irgendwie anzeigen. Und hier beginnt vielleicht der schwierigste Teil, weil wir möchten, dass der Server uns sofort HTML mit dem Inhalt der Karte gibt, der für die Indizierung geeignet ist. Gleichzeitig empfängt die Anwendung beim Navigieren zwischen den Karten alle Daten in Form von JSON von der API und die Seite wird nicht überladen. Und das alles - ohne Copy-Paste!
Beginnen wir mit dem Hinzufügen von Redux. Redux ist eine JavaScript-Bibliothek zum Speichern des Status. Die Idee ist, dass anstelle der Tausenden impliziter Zustände, die Ihre Komponenten während Benutzeraktionen und anderen interessanten Ereignissen ändern, sie einen zentralen Status haben und Änderungen über einen zentralisierten Aktionsmechanismus vornehmen. Wenn wir also früher für die Navigation zuerst das GIF zum Laden aktiviert haben, dann eine Anfrage über AJAX gestellt und schließlich im Erfolgsrückruf die erforderlichen Teile der Seite aktualisiert haben, werden wir im Redux-Paradigma aufgefordert, die Aktion "Inhalt in ein GIF mit Animation ändern" zu senden ändert den globalen Status, sodass eine Ihrer Komponenten den vorherigen Inhalt löscht und die Animation einfügt, dann eine Anfrage stellt und eine weitere Aktion in ihrem erfolgreichen Rückruf sendet: "Ändern Sie den Inhalt in geladen". Im Allgemeinen werden wir es jetzt selbst sehen.
Beginnen wir mit der Installation neuer Abhängigkeiten in unserem Container.
docker-compose exec frontend npm install --save \ redux \ react-redux \ redux-thunk \ redux-devtools-extension
Das erste ist in der Tat Redux, das zweite ist eine spezielle Bibliothek zum Überqueren von React und Redux (geschrieben von Paarungsexperten), das dritte ist eine sehr notwendige Sache, deren Notwendigkeit in
der README- Datei gut begründet ist, und schließlich ist das vierte die Bibliothek, die für die Arbeit von
Redux DevTools erforderlich ist
Erweiterung .
Beginnen wir mit dem Redux-Code der Boilerplate: Erstellen eines Reduzierers, der nichts tut, und Initialisieren des Status.
Unser Kunde ändert sich ein wenig und bereitet sich mental auf die Arbeit mit Redux vor:
Jetzt können wir Docker-Compose Up - Build Frontend ausführen, um sicherzustellen, dass nichts kaputt ist, und unser primitiver Status wurde in Redux DevTools angezeigt:

Frontend: Kartenseite
Bevor Sie Seiten mit SSRs erstellen können, müssen Sie Seiten ohne SSRs erstellen! Lassen Sie uns endlich unsere geniale API für den Zugriff auf Karten verwenden und die Kartenseite im Frontend erstellen.
Zeit, die Intelligenz zu nutzen und unsere Staatsstruktur neu zu gestalten. Es
gibt viele Materialien zu diesem Thema, daher empfehle ich, die Intelligenz nicht zu missbrauchen und mich auf das Einfache zu konzentrieren. Zum Beispiel:
{ "page": { "type": "card", // // type=card: "cardSlug": "...", // "isFetching": false, // API "cardData": {...}, // ( ) // ... }, // ... }
Lassen Sie uns die "Karten" -Komponente erhalten, die den Inhalt von cardData als Requisiten verwendet (es ist eigentlich der Inhalt unserer Karte in Mongo):
Lassen Sie uns nun eine Komponente für die gesamte Seite mit der Karte erhalten. Er ist dafür verantwortlich, die erforderlichen Daten von der API abzurufen und auf die Karte zu übertragen. Und wir werden Daten auf React-Redux-Weise abrufen.
Erstellen Sie zunächst die Datei
frontend/src/redux/actions.js
und erstellen Sie eine Aktion, die den Inhalt der Karte aus der API extrahiert, falls noch nicht geschehen:
export function fetchCardIfNeeded() { return (dispatch, getState) => { let state = getState().page; if (state.cardData === undefined || state.cardData.slug !== state.cardSlug) { return dispatch(fetchCard()); } }; }
Die
fetchCard
Aktion, die den Abruf tatsächlich macht, ist etwas komplizierter:
function fetchCard() { return (dispatch, getState) => {
Oh, wir haben eine Aktion, die ETWAS TUT! Dies muss im Reduzierer unterstützt werden:
(Beachten Sie die trendige Syntax zum Klonen eines Objekts mit sich ändernden einzelnen Feldern.)Nachdem die gesamte Logik in Redux-Aktionen ausgeführt CardPage
wird, sieht die Komponente selbst relativ einfach aus:
Fügen Sie unserer Root-App-Komponente eine einfache page.type-Verarbeitung hinzu:
Und jetzt bleibt der letzte Moment - Sie müssen irgendwie initialisieren page.type
und page.cardSlug
abhängig von der URL der Seite.Es gibt zwar noch viele Abschnitte in diesem Artikel, aber wir können derzeit keine qualitativ hochwertige Lösung finden. Lass es uns erstmal dumm machen. Das ist einfach völlig dumm. Zum Beispiel eine regelmäßige bei der Initialisierung der Anwendung!
Jetzt können wir das Frontend mit Hilfe docker-compose up --build frontend
unserer Karte neu erstellen helloworld
...
Warten Sie also eine Sekunde ... und wo ist unser Inhalt? Oh, wir haben vergessen, Markdown zu analysieren!Arbeiter: RQ
Das Parsen von Markdown und das Generieren von HTML für eine Karte mit potenziell unbegrenzter Größe ist eine typische „schwere“ Aufgabe, die nicht direkt im Backend gelöst wird, während Änderungen gespeichert werden, sondern normalerweise auf separaten Arbeitsmaschinen in die Warteschlange gestellt und ausgeführt wird.Es gibt viele Open Source-Implementierungen von Task-Warteschlangen. Wir werden Redis und eine einfache Bibliothek RQ (Redis Queue) nehmen, die Aufgabenparameter im Pickle- Format überträgt und uns Laichprozesse für deren Verarbeitung organisiert.Zeit, Radieschen hinzuzufügen, abhängig von Einstellungen und Verkabelung!
Ein bisschen Code für den Arbeiter.
Verbinden Sie für das Parsen selbst die Mistune- Bibliothek und schreiben Sie eine einfache Funktion:
Logischerweise: Wir müssen CardDAO
den Quellcode der Karte erhalten und das Ergebnis speichern. Ein Objekt, das eine Verbindung zu einem externen Speicher enthält, kann jedoch nicht über pickle serialisiert werden. Dies bedeutet, dass diese Aufgabe nicht sofort übernommen und für RQ in die Warteschlange gestellt werden kann. Auf eine gute Weise müssen wir Wiring
einen Arbeiter auf die Seite stellen und ihn in alle möglichen Richtungen werfen ... Lass es uns tun:
Wir haben unsere Klasse von Jobs deklariert und die Verkabelung als zusätzliches Argument für alle Probleme verwendet. (Beachten Sie, dass jedes Mal eine NEUE Verkabelung erstellt wird, da einige Clients nicht vor dem Fork erstellt werden können, der in RQ vor der Verarbeitung der Aufgabe auftritt.) Damit alle unsere Aufgaben nicht von der Verkabelung abhängen, dh von ALLEN unseren Objekten, lassen Sie uns Machen wir einen Dekorateur, der nur das Nötigste aus der Verkabelung herausholt:
Fügen Sie unserer Aufgabe einen Dekorateur hinzu und genießen Sie das Leben: import mistune from backend.storage.card import CardDAO from backend.tasks.task import task @task def parse_card_markup(card_dao: CardDAO, card_id: str): card = card_dao.get_by_id(card_id) card.html = _parse_markdown(card.markdown) card_dao.update(card) _parse_markdown = mistune.Markdown(escape=True, hard_wrap=False)
Das Leben genießen? Ugh, ich wollte sagen, wir starten den Arbeiter: $ docker-compose up worker ... Creating habr-app-demo_worker_1 ... done Attaching to habr-app-demo_worker_1 worker_1 | 17:21:03 RQ worker 'rq:worker:49a25686acc34cdfa322feb88a780f00' started, version 0.13.0 worker_1 | 17:21:03 *** Listening on tasks... worker_1 | 17:21:03 Cleaning registries for queue: tasks
III ... er macht nichts! Natürlich, weil wir keine einzige Aufgabe gestellt haben!Schreiben wir unser Tool, das eine Testkarte erstellt, so um, dass es: a) nicht herunterfällt, wenn die Karte bereits erstellt wurde (wie in unserem Fall); b) Aufgabe beim Parsen von Marqdown.
Tools können jetzt nicht nur im Backend, sondern auch auf dem Worker ausgeführt werden. Im Prinzip ist uns das jetzt egal. Wir starten es docker-compose exec worker python -m tools.add_test_content
und in einem benachbarten Tab des Terminals sehen wir ein Wunder - der Arbeiter hat ETWAS getan! worker_1 | 17:34:26 tasks: backend.tasks.parse.parse_card_markup(card_id='5c715dd1e201ce000c6a89fa') (613b53b1-726b-47a4-9c7b-97cad26da1a5) worker_1 | 17:34:27 tasks: Job OK (613b53b1-726b-47a4-9c7b-97cad26da1a5) worker_1 | 17:34:27 Result is kept for 500 seconds
Nachdem wir den Container mit dem Backend neu erstellt haben, können wir endlich den Inhalt unserer Karte im Browser sehen:
Frontend-Navigation
Bevor wir zu SSR übergehen, müssen wir all unsere Reaktionsreaktionen ein wenig aussagekräftig machen und unsere Einzelseitenanwendung wirklich zu einer einzigen Seite machen. Lassen Sie uns unser Tool aktualisieren, um zwei (NICHT EINE, ZWEI! MAMMA, ICH JETZT BIG DATE DEVELOPER!) Karten zu erstellen, die miteinander verknüpft sind, und dann werden wir uns mit der Navigation zwischen ihnen befassen.Jetzt können wir den Links folgen und überlegen, wie unsere wunderbare Anwendung jedes Mal neu gestartet wird. Hör auf!Setzen Sie Ihren Handler zunächst auf Klicks auf die Links. Da HTML mit Links aus dem Backend stammt und die Anwendung mit React ausgeführt wird, benötigen wir einen kleinen reaktionsspezifischen Fokus.
Da die gesamte Logik beim Laden der Karten in unserer Komponente CardPage
in der Aktion selbst (erstaunlich!), Muss keine Aktion ausgeführt werden: export function navigate(link) { return { type: NAVIGATE, path: link.pathname } }
Fügen Sie für diesen Fall einen albernen Reduzierer hinzu:
Da sich jetzt der Status unserer Anwendung ändern kann, müssen CardPage
wir eine Methode hinzufügen, die componentDidUpdate
mit der bereits hinzugefügten identisch ist componentWillMount
. Nach dem Aktualisieren der Eigenschaften CardPage
(z. B. Eigenschaften cardSlug
während der Navigation) wird nun auch der Inhalt der Karte aus dem Backend angefordert (dies componentWillMount
geschah erst, als die Komponente initialisiert wurde).Okay, docker-compose up --build frontend
und wir haben eine funktionierende Navigation!
Ein aufmerksamer Leser wird feststellen, dass sich die URL der Seite beim Navigieren zwischen Karten nicht ändert - selbst im Screenshot sehen wir Hallo, die Weltkarte an der Adresse der Demokarte. Dementsprechend fiel auch die Vorwärts-Rückwärts-Navigation ab. Fügen wir sofort etwas schwarze Magie mit Geschichte hinzu, um das Problem zu beheben!Das Einfachste, was Sie tun können, ist, der Aktion etwas hinzuzufügen.navigate
eine Herausforderung history.pushState
. export function navigate(link) { history.pushState(null, "", link.href); return { type: NAVIGATE, path: link.pathname } }
Wenn Sie nun auf die Links klicken, ändert sich die URL in der Adressleiste des Browsers wirklich. Der Zurück-Knopf wird jedoch brechen !Damit es funktioniert, müssen wir das Ereignis des popstate
Objekts abhören window
. Wenn wir in diesem Fall sowohl vorwärts als auch rückwärts navigieren möchten (dh durch dispatch(navigate(...))
), müssen wir der Funktion navigate
ein spezielles "Nicht pushState
" -Flag hinzufügen (sonst wird alles noch mehr kaputt gehen!). Um zwischen „unseren“ Zuständen zu unterscheiden, sollten wir außerdem die Möglichkeit nutzen pushState
, Metadaten zu speichern. Es gibt viel Magie und Debugging, also lasst uns gleich zum Code kommen! So sieht die App aus:
Und hier ist die Navigationsaktion:
Jetzt wird die Geschichte funktionieren.Nun, die letzte Berührung: Da wir jetzt eine Aktion haben navigate
, warum geben wir nicht den zusätzlichen Code im Client auf, der den Anfangszustand berechnet? Wir können einfach die Navigation zum aktuellen Standort anrufen:
Kopieren-Einfügen zerstört!Frontend: Serverseitiges Rendern
Es ist Zeit für unsere wichtigsten (meiner Meinung nach) Chips - SEO-Freundlichkeit. Damit Suchmaschinen unseren Inhalt indizieren können, der vollständig dynamisch in React-Komponenten erstellt wird, müssen wir in der Lage sein, ihnen das Ergebnis des Renderns von React zu geben und zu lernen, wie dieses Ergebnis wieder interaktiv gemacht werden kann.Das allgemeine Schema ist einfach. Erstens: Wir müssen den von unserer React-Komponente generierten HTML-Code in unsere HTML-Vorlage einfügen App
. Dieser HTML-Code wird von Suchmaschinen (und Browsern mit deaktiviertem JS, hehe) angezeigt. Zweitens: Sie müssen der Vorlage ein Tag hinzufügen, das <script>
irgendwo (z. B. ein Objekt window
) einen Statusspeicherauszug speichert, aus dem dieser HTML-Code gerendert wurde. Dann können wir unsere Anwendung auf der Client-Seite sofort mit diesem Status initialisieren und zeigen, was benötigt wird (wir können sogar Hydrat verwendenauf den generierten HTML-Code, um den DOM-Baum der Anwendung nicht neu zu erstellen).Beginnen wir mit dem Schreiben einer Funktion, die gerenderten HTML-Code und den Endzustand zurückgibt.
Fügen Sie unserer Vorlage, über die wir oben gesprochen haben, neue Argumente und Logik hinzu:
Unser Express-Server wird etwas komplizierter:
Aber der Kunde ist einfacher:
Als Nächstes müssen Sie plattformübergreifende Fehler wie "Verlauf ist nicht definiert" bereinigen. Fügen Sie dazu irgendwo in eine einfache (bisher) Funktion hinzu utility.js
.
Dann wird es eine bestimmte Anzahl von Routineänderungen geben, die ich hier nicht einbringen werde (aber sie finden Sie im entsprechenden Commit ). Dadurch kann unsere React-Anwendung sowohl im Browser als auch auf dem Server rendern.Es funktioniert!
Aber es gibt, wie sie sagen, eine Einschränkung ...
LADEN? Alles, was Google in meinem supercoolen Modedienst sieht, ist LADEN ?!Nun, es scheint, dass all unser Asynchronismus gegen uns gespielt hat. Jetzt müssen wir dem Server eine Möglichkeit geben, zu verstehen, dass die Antwort vom Backend mit dem Inhalt der Karte warten muss, bevor die React-Anwendung in eine Zeichenfolge gerendert und an den Client gesendet wird. Und es ist wünschenswert, dass diese Methode ziemlich allgemein ist.Es kann viele Lösungen geben. Ein Ansatz besteht darin, in einer separaten Datei zu beschreiben, für welche Pfade welche Daten gesichert werden sollen, und dies vor dem Rendern der Anwendung ( Artikel ). Diese Lösung hat viele Vorteile. Es ist einfach, es ist explizit und es funktioniert.Als Experiment (der ursprüngliche Inhalt sollte zumindest irgendwo im Artikel enthalten sein!) Schlage ich ein anderes Schema vor. Lassen Sie uns jedes Mal, wenn wir etwas Asynchrones ausführen, auf das wir warten müssen, irgendwo in unserem Status das entsprechende Versprechen hinzufügen (z. B. das, das Fetch zurückgibt). So haben wir einen Ort, an dem Sie immer überprüfen können, ob alles heruntergeladen wurde.Fügen Sie zwei neue Aktionen hinzu.
Der erste wird aufgerufen, wenn der Abruf gestartet wird, der zweite - am Ende .then()
.Fügen Sie nun ihre Verarbeitung zum Reduzierer hinzu:
Jetzt werden wir die Aktion verbessern fetchCard
:
Es bleibt, dem initialState
leeren Array Versprechen hinzuzufügen und den Server auf sie alle warten zu lassen! Die Renderfunktion wird asynchron und hat folgende Form:
Aufgrund der erfassten render
Asynchronität ist der Request-Handler auch etwas komplizierter:
Et voilà!
Fazit
Wie Sie sehen, ist das Erstellen einer High-Tech-Anwendung nicht so einfach. Aber nicht so schwer! Die endgültige Anwendung befindet sich im Repository von Github, und theoretisch benötigen Sie nur Docker, um sie auszuführen.Wenn der Artikel gefragt ist, wird dieses Repository nicht einmal verlassen! Wir werden es mit etwas anderem Wissen betrachten können, das notwendig ist:- Protokollierung, Überwachung, Lasttest.
- Testen, CI, CD.
- coolere Funktionen wie Autorisierung oder Volltextsuche.
- Einrichtung und Entwicklung der Produktionsumgebung.
Vielen Dank für Ihre Aufmerksamkeit!