Erstellen und Bereitstellen einer Full-Stack-React-Anwendung

Hallo habr Ich präsentiere Ihnen die Übersetzung des Artikels "Erstellen und Bereitstellen einer Full-Stack-React-App" von Frank Zickert.

Infrastrukturkomponenten erleichtern das Erstellen, Ausführen und Bereitstellen einer vollständigen React-Anwendung. Mit diesen React-Komponenten können Sie sich darauf konzentrieren, die Geschäftslogik Ihrer Anwendung zu schreiben. Sie müssen sich nicht um die Konfiguration kümmern.

Willst du ein Full-Stack-Entwickler werden? Die Full-Stack-Anwendung ergänzt die interaktive React-Weboberfläche um einen Server und eine Datenbank. Aber diese App erfordert viel mehr Möglichkeiten als eine einfache Ein-Seiten-Anwendung.

Wir verwenden Komponenten der Infrastruktur . Mit diesen React-Komponenten können wir unsere Infrastrukturarchitektur als Teil unserer React-Anwendung definieren. Wir brauchen keine weiteren Einstellungen wie Webpack, Babel oder Serverless mehr.

Starten Sie


Sie können Ihr Projekt auf drei Arten einrichten:


Sobald Sie die Abhängigkeiten installiert haben (führen Sie npm install ), können Sie das Projekt mit einem Befehl npm run build : npm run build .

Das Build-Skript fügt package.json drei weitere Skripten hinzu:

  • npm run{your-project-name} startet Ihre React-Anwendung lokal (Hot-Dev-Modus, ohne Serverteil und Datenbank)
  • npm run start-{your-env-name} startet den gesamten Software-Stack lokal. Hinweis: Um die Datenbank im Offline-Modus erfordert Java 8 JDK ausführen. So können Sie das JDK installieren .
  • npm run deploy-{your-env-name} Sie npm run deploy-{your-env-name} Name} setzt Ihre Anwendung auf AWS.

Hinweis Um Ihre Anwendung für AWS bereitzustellen, benötigen Sie einen technischen IAM-Benutzer mit diesen Rechten. Fügen Sie die Benutzeranmeldeinformationen wie folgt in Ihre ENV-Datei ein:

 AWS_ACCESS_KEY_ID = *** AWS_SECRET_ACCESS_KEY = *** 

Definieren Sie Ihre Anwendungsarchitektur


Projekte, die auf Infrastrukturkomponenten basieren, sind klar strukturiert. Sie haben eine Komponente der obersten Ebene. Dies definiert die Gesamtarchitektur Ihrer Anwendung.

Unterkomponenten (untergeordnete Komponenten) verfeinern (erweitern) das Verhalten der Anwendung und fügen Funktionen hinzu.

Im folgenden Beispiel ist die Komponente <ServiceOrientedApp /> unsere Komponente auf oberster Ebene. Wir exportieren es als Standarddatei in unsere Einstiegspunktdatei ( src / index.tsx) .

 export default ( <ServiceOrientedApp stackName = "soa-dl" buildPath = 'build' region='eu-west-1'> <Environment name="dev" /> <Route path='/' name='My Service-Oriented React App' render={()=><DataForm />} /> <DataLayer id="datalayer"> <UserEntry /> <GetUserService /> <AddUserService /> </DataLayer> </ServiceOrientedApp> ); 

<ServiceOrientedApp /> ist eine interaktive Webanwendung. Sie können die Funktionalität dieser Anwendung mithilfe der von Ihnen bereitgestellten untergeordneten Komponenten erläutern (erweitern). Es unterstützt die <DataLayer /> <Environment /> , <Route /> , <Service /> und <DataLayer /> .

<Envrionment /> bestimmt die Laufzeit Ihrer Anwendung. Zum Beispiel können Sie eine Entwicklungs- und eine Produktversion haben. Sie können jedes einzeln ausführen und bereitstellen.

<Route /> ist die Seite Ihrer Anwendung. Es funktioniert wie <Route /> im React-Router. Hier finden Sie ein Tutorial zum Arbeiten mit Routen .

<Service /> definiert eine Funktion, die serverseitig ausgeführt wird. Es kann eine oder mehrere <Middleware /> - Komponenten als Kinder haben.

<Middleware /> funktioniert wie Express.js-Middleware.
<DataLayer /> fügt Ihrer Anwendung eine NoSQL-Datenbank hinzu. Akzeptiert <Entry /> - Komponenten als untergeordnete Elemente. <Entry /> beschreibt die Art der Elemente in Ihrer Datenbank.

Diese Komponenten sind alles, was wir brauchen, um unsere Full-Stack-Anwendung zu erstellen. Wie Sie sehen, hat unsere Anwendung: eine Laufzeit, eine Seite, zwei Dienste und eine Datenbank mit einem Datensatz.

Die Komponentenstruktur bietet eine klare Sicht auf Ihre Anwendung. Je größer Ihre Bewerbung wird, desto wichtiger wird sie.

Möglicherweise haben Sie festgestellt, dass <Service /> <DataLayer /> von <DataLayer /> . Dies hat eine einfache Erklärung. Wir wollen, dass unsere Dienste Zugriff auf die Datenbank haben. Es ist wirklich so einfach!

Datenbank-Design


<DataLayer /> erstellt Amazon DynamoDB. Dies ist eine Schlüsselwert-Datenbank (NoSQL). Es bietet eine hohe Leistung in jeder Größenordnung. Aber im Gegensatz zu der relationalen Datenbank, unterstützt es nicht komplexe Abfragen.

Das Datenbankschema enthält drei Felder: primaryKey , rangeKey und data . Dies ist wichtig, weil Sie müssen wissen, dass Sie Datensätze für seine Schlüssel nur finden können. Entweder mit primaryKey oder rangeKey oder mit beiden.

Schauen wir uns mit diesem Wissen unseren <Entry /> :

 export const USER_ENTRY_ID = "user_entry"; export default function UserEntry (props) { return <Entry id={ USER_ENTRY_ID } primaryKey="username" rangeKey="userid" data={{ age: GraphQLString, address: GraphQLString }} /> }; 

<Entry /> beschreibt die Struktur unserer Daten. Wir definieren Namen für unseren primaryKey und rangeKey. Sie können einen anderen Namen als einige DynamoDB-Schlüsselwörter verwenden, die Sie hier finden. Die von uns verwendeten Namen haben jedoch funktionale Auswirkungen:

  • Wenn wir unserer Datenbank Elemente hinzufügen, müssen wir Werte für diese Schlüsselnamen bereitstellen.
  • Die Kombination beider Schlüssel beschreibt ein eindeutiges Element in der Datenbank.
  • Es sollte keinen anderen <Entry /> mit denselben Tastennamen geben (ein Name kann derselbe sein, aber nicht beide).
  • Wir können Elemente in der Datenbank nur finden, wenn wir den Wert von mindestens einem Schlüsselnamen haben.

In unserem Beispiel bedeutet das:

  • Jeder Benutzer muss einen Benutzernamen und eine Benutzer-ID haben.
  • Es kann keinen zweiten Benutzer mit demselben Benutzernamen und derselben Benutzer-ID geben. Aus Datenbanksicht wäre es schön, wenn zwei Benutzer denselben Benutzernamen hätten, wenn sie unterschiedliche Benutzer-IDs hätten (oder umgekehrt).
  • Wir können nicht haben eine andere <Entry /> in unserer Datenbank primaryKey = «Benutzername» und rangeKey = «Benutzer-ID».
  • Wir können die Datenbank nach Benutzern abfragen, wenn wir einen Benutzernamen oder eine Benutzer- id . Wir können jedoch keine Anfragen nach Alter oder Adresse stellen.

Fügen Sie der Datenbank Elemente hinzu


Wir haben festgestellt , zwei <Service /> - Komponente in unserem <ServiceOrientedApp /> . POST Dienst, der den Benutzer zur Datenbank hinzufügt, und GET Dienst, der den Benutzer von dort abruft.

Beginnen wir mit <AddUserService /> . Hier ist der Code für diesen Dienst:

 import * as React from 'react'; import { callService, Middleware, mutate, Service, serviceWithDataLayer } from "infrastructure-components"; import { USER_ENTRY_ID, IUserEntry } from './user-entry'; const ADDUSER_SERVICE_ID = "adduser"; export default function AddUserService () { return <Service id={ ADDUSER_SERVICE_ID } path="/adduser" method="POST"> <Middleware callback={serviceWithDataLayer(async function (dataLayer, req, res, next) { const parsedBody: IUserEntry = JSON.parse(req.body); await mutate( dataLayer.client, dataLayer.setEntryMutation(USER_ENTRY_ID, parsedBody) ); res.status(200).set({ "Access-Control-Allow-Origin" : "*", // Required for CORS support to work }).send("ok"); })}/> </Service> }; . import * as React from 'react'; import { callService, Middleware, mutate, Service, serviceWithDataLayer } from "infrastructure-components"; import { USER_ENTRY_ID, IUserEntry } from './user-entry'; const ADDUSER_SERVICE_ID = "adduser"; export default function AddUserService () { return <Service id={ ADDUSER_SERVICE_ID } path="/adduser" method="POST"> <Middleware callback={serviceWithDataLayer(async function (dataLayer, req, res, next) { const parsedBody: IUserEntry = JSON.parse(req.body); await mutate( dataLayer.client, dataLayer.setEntryMutation(USER_ENTRY_ID, parsedBody) ); res.status(200).set({ "Access-Control-Allow-Origin" : "*", // Required for CORS support to work }).send("ok"); })}/> </Service> }; 

Komponente <Service /> - drei Parameter:

  • Der Bezeichner ( id ) muss eine eindeutige Zeichenfolge sein. Wir verwenden eine Kennung ( id ), wenn wir in anderen Komponenten den service anrufen müssen.
  • Der path (mit dem Anfangsbuchstaben /) gibt den relativen URL-Pfad Ihres Dienstes an
  • Die method muss GET , POST , UPDATE oder DELETE . Es gibt die HTTP-Anforderung an, die wir beim Aufrufen des Dienstes verwenden.

Wir fügen als Kind <Middleware /> . Diese <Middleware /> empfängt die Callback - Funktion als Parameter. Wir könnten Express.js Middleware direkt bereitstellen. Da wir auf die Datenbank zugreifen möchten, serviceWithDataLayer wir serviceWithDataLayer Funktion in serviceWithDataLayer . Dies fügt dataLayer als ersten Parameter zu unserem Rückruf.

DataLayer bietet Zugriff auf die Datenbank. Mal sehen wie!

Die mutate asynchrone Funktion wendet die Änderungen auf die Daten in unserer Datenbank an. Dies erfordert einen Client und einen Mutationsbefehl als Parameter.

Elementdaten sind ein Javascript-Objekt mit allen erforderlichen Schlüssel-Wert-Paaren. In unserem Service erhalten wir dieses Objekt vom Anforderungs-Body. Für User Objekt die folgende Struktur:

 export interface IUserEntry { username: string, userid: string, age: string, address: string } 

Dieses Objekt übernimmt die Namen primaryKey und rangeKey sowie alle Datenschlüssel, die wir in <Entry /> .

Hinweis: zur Zeit die einzige unterstützte Typ ist ein String, das entspricht der Definition von GraphQLString <Entry />.
Wir haben oben erwähnt, dass wir IUserEntry Daten aus dem Körper nehmen. Wie läuft das

Infrastrukturkomponenten stellen die asynchrone Funktion callService (serviceId, dataObject) . Diese Funktion übernimmt die Service - Kennung, Javascript-Seiten (wie für die Anforderung Körper mit Senden POST ), die Funktion des success - und eine Callback - Funktion mit einem Fehler.

Das folgende Snippet zeigt, wie wir mit dieser Funktion unseren <AddUserService /> . Wir geben serviceId . Und wir übergeben userData , die wir als Parameter für unsere Funktion übernehmen.

 export async function callAddUserService (userData: IUserEntry) { await callService( ADDUSER_SERVICE_ID, userData, (data: any) => { console.log("received data: ", data); }, (error) => { console.log("error: " , error) } ); }; - export async function callAddUserService (userData: IUserEntry) { await callService( ADDUSER_SERVICE_ID, userData, (data: any) => { console.log("received data: ", data); }, (error) => { console.log("error: " , error) } ); }; ) { export async function callAddUserService (userData: IUserEntry) { await callService( ADDUSER_SERVICE_ID, userData, (data: any) => { console.log("received data: ", data); }, (error) => { console.log("error: " , error) } ); }; 

Jetzt brauchen wir nur noch die Funktion callAddUserService , um einen neuen Benutzer hinzuzufügen. Rufen Sie es beispielsweise auf, wenn der Benutzer auf eine Schaltfläche klickt:

 <button onClick={() => callAddUserService({ username: username, userid: userid, age: age, address: address })}>Save</button> 

Wir nennen es nur das Objekt mit IUserEntry . Es ruft den richtigen Dienst auf (wie durch seine Kennung ( id ) angegeben). Sie fügt userData in den Anforderungshauptteil ein. <AddUserService /> entnimmt Daten aus dem Body und legt sie in der Datenbank ab.

Holen Sie sich Gegenstände aus der Datenbank


Holen Sie sich die Elemente aus der Datenbank so einfach ist, sie als Zugabe.

 export default function GetUserService () { return <Service id={ GETUSER_SERVICE_ID } path="/getuser" method="GET"> <Middleware callback={serviceWithDataLayer(async function (dataLayer, req, res, next) { const data = await select( dataLayer.client, dataLayer.getEntryQuery(USER_ENTRY_ID, { username: req.query.username, userid: req.query.userid }) ); res.status(200).set({ "Access-Control-Allow-Origin" : "*", // Required for CORS support to work }).send(JSON.stringify(data)); })}/> </Service> } - export default function GetUserService () { return <Service id={ GETUSER_SERVICE_ID } path="/getuser" method="GET"> <Middleware callback={serviceWithDataLayer(async function (dataLayer, req, res, next) { const data = await select( dataLayer.client, dataLayer.getEntryQuery(USER_ENTRY_ID, { username: req.query.username, userid: req.query.userid }) ); res.status(200).set({ "Access-Control-Allow-Origin" : "*", // Required for CORS support to work }).send(JSON.stringify(data)); })}/> </Service> } data, req, res, next) { export default function GetUserService () { return <Service id={ GETUSER_SERVICE_ID } path="/getuser" method="GET"> <Middleware callback={serviceWithDataLayer(async function (dataLayer, req, res, next) { const data = await select( dataLayer.client, dataLayer.getEntryQuery(USER_ENTRY_ID, { username: req.query.username, userid: req.query.userid }) ); res.status(200).set({ "Access-Control-Allow-Origin" : "*", // Required for CORS support to work }).send(JSON.stringify(data)); })}/> </Service> } 

Auch hier verwenden wir <Service />, <Middleware /> und eine Rückruffunktion mit Zugriff auf die Datenbank.

Anstelle der mutate Funktion, die der Datenbank ein Element hinzufügt, verwenden wir die select Funktion. Diese Funktion fragt nach dem Client, den wir von dataLayer . Der zweite Parameter ist der Befehl select . Wie beim mutation können wir mit dataLayer einen dataLayer .

Dieses Mal werden wir verwenden , um das getEntryQuery . Wir geben den Bezeichner ( id ) <Entry /> dessen Element wir erhalten möchten. primaryKey geben wir die Schlüssel ( primaryKey und rangeKey ) eines bestimmten Elements in einem Javascript-Objekt an. Da wir beide Schlüssel zur Verfügung stellen, erhalten wir ein Element zurück. Wenn es existiert.

Wie Sie sehen können, nehmen wir die Schlüsselwerte aus der Abfrage. Aber diesmal nehmen wir sie von request.query und nicht von request.body . Der Grund dafür ist, dass dieser Dienst die GET Methode verwendet. Diese Methode unterstützt den Body in der Anfrage nicht. Es liefert aber alle Daten als Abfrageparameter.

Funktion callService erledigt dies für uns. Wie in der callAddUserService-function wir den Bezeichner ( id ) <Service /> , den wir aufrufen möchten. Wir liefern die notwendigen Daten. Hier sind nur die Schlüssel. Und wir bieten Rückruffunktionen.

Ein erfolgreicher Rückruf liefert eine Antwort. Der Antworttext im JSON-Format enthält unser gefundenes Element. Auf dieses Element können wir über den Schlüssel get_user_entry . " Get_ " definiert die Anforderung, die wir in unsere Auswahlfunktion gestellt haben. " User_entry " ist der Schlüssel unseres <Entry /> .

 export async function callGetUserService (username: string, userid: string, onData: (userData: IUserEntry) => void) { await callService( GETUSER_SERVICE_ID, { username: username, userid: userid }, async function (response: any) { await response.json().then(function(data) { console.log(data[`get_${USER_ENTRY_ID}`]); onData(data[`get_${USER_ENTRY_ID}`]); }); }, (error) => { console.log("error: " , error) } ); } - export async function callGetUserService (username: string, userid: string, onData: (userData: IUserEntry) => void) { await callService( GETUSER_SERVICE_ID, { username: username, userid: userid }, async function (response: any) { await response.json().then(function(data) { console.log(data[`get_${USER_ENTRY_ID}`]); onData(data[`get_${USER_ENTRY_ID}`]); }); }, (error) => { console.log("error: " , error) } ); } - export async function callGetUserService (username: string, userid: string, onData: (userData: IUserEntry) => void) { await callService( GETUSER_SERVICE_ID, { username: username, userid: userid }, async function (response: any) { await response.json().then(function(data) { console.log(data[`get_${USER_ENTRY_ID}`]); onData(data[`get_${USER_ENTRY_ID}`]); }); }, (error) => { console.log("error: " , error) } ); } 

Schauen Sie sich Ihre Full-Stack-Anwendung in Aktion


Wenn Sie Ihre Anwendung noch nicht gestartet haben, können Sie dies jetzt tun: npm run start-{your-env-name} .

Sie können Ihre Anwendung sogar mit einem einzigen Befehl für AWS bereitstellen: npm run deploy-{your-env-name} . (Denken Sie daran, die AWS-Anmeldeinformationen in die ENV-Datei einzutragen.)

In diesem Beitrag wird nicht beschrieben, wie Sie die von Ihnen in die Datenbank eingegebenen Daten eingeben und wie Sie die Ergebnisse anzeigen. callAddUserService und callGetUserService kapseln alles, was für Dienste und die Datenbank spezifisch ist. Sie müssen nur das Javascript-Objekt dort einfügen und es zurückholen.

Den Quellcode für dieses Beispiel finden Sie in diesem GitHub-Repository . Es enthält eine sehr einfache Benutzeroberfläche.

Source: https://habr.com/ru/post/de476696/


All Articles