In einem früheren Artikel haben wir gezeigt, wie man eine GraphQL-API mit FaunaDB erstellt. Wir haben auch eine Reihe von Artikeln [1, 2, 3, 4] geschrieben, die erklären, wie traditionelle Datenbanken, die für globale Skalierbarkeit entwickelt wurden, auf eventually consistency (vs. strong consistency) setzen und/oder Kompromisse bei Beziehungen und Indizierungsmöglichkeiten eingehen müssen. FaunaDB ist anders, da sie diese Kompromisse nicht eingeht. Sie ist auf Skalierbarkeit ausgelegt, sodass sie Ihr zukünftiges Startup sicher bedienen kann, egal wie groß es wird, ohne auf Beziehungen und konsistente Daten zu verzichten.
In diesem Artikel freuen wir uns sehr, all dies in einer realen App mit hochdynamischen Daten auf serverlose Weise zusammenzubringen, indem wir React Hooks, FaunaDB und Cloudinary verwenden. Wir werden die Fauna Query Language (FQL) anstelle von GraphQL verwenden und mit einem reinen Frontend-Ansatz beginnen, der direkt auf die serverlose Datenbank FaunaDB für Datenspeicherung, Authentifizierung und Autorisierung zugreift.

Der Goldstandard für Beispielanwendungen, die eine bestimmte Technologie vorstellen, ist eine To-Do-App – hauptsächlich, weil sie einfach ist. Jede Datenbank kann eine sehr einfache Anwendung bedienen und glänzen.
Und genau deshalb wird diese App anders sein! Wenn wir wirklich zeigen wollen, wie FaunaDB für reale Anwendungen glänzt, dann müssen wir etwas Fortgeschritteneres bauen.
Fwitter vorstellen
Als wir bei Twitter anfingen, waren Datenbanken schlecht. Als wir gingen, waren sie immer noch schlecht
Evan Weaver
Da FaunaDB von ehemaligen Twitter-Ingenieuren entwickelt wurde, die diese Einschränkungen aus erster Hand erlebt haben, erschien eine Twitter-ähnliche Anwendung als angemessen sentimentale Wahl. Und da wir sie mit FaunaDB bauen, nennen wir dieses serverlose Baby „Fwitter”.
Unten finden Sie ein kurzes Video, das zeigt, wie es aussieht, und der vollständige Quellcode ist auf GitHub verfügbar.
Wenn Sie das Repository klonen und mit der Erkundung beginnen, werden Sie möglicherweise eine Fülle von gut kommentierten Beispielabfragen feststellen, die in diesem Artikel nicht behandelt werden. Das liegt daran, dass wir Fwitter in zukünftigen Artikeln als unsere bevorzugte Beispielanwendung verwenden und im Laufe der Zeit zusätzliche Funktionen hinzufügen werden.
Aber vorerst hier eine grundlegende Übersicht dessen, was wir hier behandeln werden
- Modellierung der Daten
- Einrichtung des Projekts
- Erstellung des Frontends
- Der FaunaDB JavaScript-Treiber
- Erstellung von Daten
- Sichern Ihrer Daten mit UDFs und ABAC-Rollen
- Implementierung der Authentifizierung
- Hinzufügen von Cloudinary für Medien
- Abrufen von Daten
- Mehr im Code
Wir haben diese Funktionen erstellt, ohne Operationen konfigurieren oder Server für Ihre Datenbank einrichten zu müssen. Da sowohl Cloudinary als auch FaunaDB von Haus aus skalierbar und verteilt sind, müssen wir uns nie Gedanken über die Einrichtung von Servern in mehreren Regionen machen, um niedrige Latenzzeiten für Benutzer in anderen Ländern zu erreichen.
Legen wir los!
Modellierung der Daten
Bevor wir zeigen können, wie FaunaDB bei Beziehungen glänzt, müssen wir die Arten von Beziehungen im Datenmodell unserer Anwendung behandeln. FaunaDB-Datenelemente werden in Dokumenten gespeichert, die dann in Sammlungen gespeichert werden – wie Zeilen in Tabellen. Beispielsweise werden die Details jedes Benutzers durch ein Benutzerdokument dargestellt, das in einer Benutzersammlung gespeichert ist. Und wir planen schließlich, sowohl Single Sign-On- als auch passwortbasierte Anmeldemethoden für einen einzelnen Benutzer zu unterstützen, die jeweils als Konto-Dokument in einer Konten-Sammlung dargestellt werden.
Zu diesem Zeitpunkt hat ein Benutzer ein Konto, sodass es keine Rolle spielt, welches Element den Verweis (d. h. die Benutzer-ID) speichert. Wir könnten die Benutzer-ID in einem Eins-zu-eins-Verhältnis entweder im Konto- oder im Benutzerdokument gespeichert haben

Da ein Benutzer jedoch schließlich mehrere Konten (oder Authentifizierungsmethoden) haben wird, werden wir ein Eins-zu-viele-Modell haben.

Bei einer Eins-zu-viele-Beziehung zwischen Benutzern und Konten verweist jedes Konto nur auf einen Benutzer, daher ist es sinnvoll, die Benutzerreferenz im Konto zu speichern

Wir haben auch viele-zu-viele-Beziehungen, wie die Beziehungen zwischen Fweets und Benutzern, aufgrund der komplexen Arten, wie Benutzer miteinander über Likes, Kommentare und Refweets interagieren.

Darüber hinaus werden wir eine dritte Sammlung, Fweetstats, verwenden, um Informationen über die Interaktion zwischen einem Benutzer und einem Fweet zu speichern.

Die Daten von Fweetstats helfen uns beispielsweise festzustellen, ob die Symbole, die dem Benutzer anzeigen, dass er einen Fweet bereits geliked, kommentiert oder refweetet hat, farblich hervorgehoben werden sollen oder nicht. Es hilft uns auch zu bestimmen, was das Klicken auf das Herz bedeutet: „entliken“ oder „liken“.

Das endgültige Modell für die Anwendung wird wie folgt aussehen:

Fweets stehen im Mittelpunkt des Modells, da sie die wichtigsten Daten des Fweets enthalten, wie z. B. Informationen zur Nachricht, die Anzahl der Likes, Refweets, Kommentare und die Cloudinary-Medien, die angehängt wurden. FaunaDB speichert diese Daten in einem JSON-Format, das wie folgt aussieht:

Wie im Modell und in diesem Beispiel-JSON gezeigt, werden Hashtags als Liste von Referenzen gespeichert. Wenn wir wollten, könnten wir die vollständigen Hashtag-JSONs hier speichern, und das ist die bevorzugte Lösung in eingeschränkteren dokumentenbasierten Datenbanken, denen Beziehungen fehlen. Das würde jedoch bedeuten, dass unsere Hashtags überall dupliziert würden (wie in eingeschränkteren Datenbanken) und es wäre schwieriger, nach Hashtags zu suchen und/oder Fweets für einen bestimmten Hashtag abzurufen, wie unten gezeigt.

Beachten Sie, dass ein Fweet keinen Link zu Kommentaren enthält, aber die Kommentarsammlung einen Verweis auf den Fweet enthält. Das liegt daran, dass ein Kommentar zu einem Fweet gehört, aber ein Fweet viele Kommentare haben kann – ähnlich wie bei der Eins-zu-viele-Beziehung zwischen Benutzern und Konten.
Schließlich gibt es eine FollowerStats-Sammlung, die im Wesentlichen Informationen darüber speichert, wie stark Benutzer miteinander interagieren, um ihre jeweiligen Feeds zu personalisieren. Wir werden dies in diesem Artikel nicht viel behandeln, aber Sie können mit den Abfragen im Quellcode experimentieren und auf einen zukünftigen Artikel über erweiterte Indizierung gespannt bleiben.
Hoffentlich beginnen Sie zu sehen, warum wir uns für etwas Komplexeres als eine To-Do-App entschieden haben. Obwohl Fwitter bei weitem nicht die Komplexität des echten Twitter-Apps, auf dem es basiert, erreicht, wird bereits deutlich, dass die Implementierung einer solchen Anwendung ohne Beziehungen ein ernster Denksport wäre.
Wenn Sie das Projekt noch nicht über das GitHub-Repository eingerichtet haben, ist es jetzt endlich an der Zeit, unser Projekt lokal zum Laufen zu bringen!
Einrichtung des Projekts
Um das Projekt einzurichten, gehen Sie zum FaunaDB Dashboard und melden Sie sich an. Sobald Sie sich im Dashboard befinden, klicken Sie auf New Database, geben Sie einen Namen ein und klicken Sie auf Save. Sie sollten sich nun auf der „Overview“-Seite Ihrer neuen Datenbank befinden.
Als Nächstes benötigen wir einen Schlüssel, den wir in unseren Setup-Skripten verwenden werden. Klicken Sie in der linken Seitenleiste auf den Reiter Security und dann auf die Schaltfläche New key.
Im Formular „New key“ sollte die aktuelle Datenbank bereits ausgewählt sein. Lassen Sie für „Role“ die Einstellung „Admin“. Optional können Sie einen Schlüsselnamen hinzufügen. Klicken Sie dann auf Save und kopieren Sie das Schlüsselgeheimnis, das auf der nächsten Seite angezeigt wird. Es wird nicht erneut angezeigt.

Nachdem Sie Ihr Datenbankgeheimnis haben, klonen Sie das Git-Repository und folgen Sie der Readme. Wir haben einige Skripte vorbereitet, sodass Sie nur die folgenden Befehle ausführen müssen, um Ihre App zu initialisieren, alle Sammlungen zu erstellen und Ihre Datenbank zu befüllen. Die Skripte geben Ihnen weitere Anweisungen
// install node modules
npm install
// run setup, this will create all the resources in your database
// provide the admin key when the script asks for it.
// !!! the setup script will give you another key, this is a key
// with almost no permissions that you need to place in your .env.local as the
// script suggestions
npm run setup
npm run populate
// start the frontend
Nach dem Skript sollte Ihre .env.local-Datei den Bootstrap-Schlüssel enthalten, den das Skript Ihnen zur Verfügung gestellt hat (nicht den Admin-Schlüssel)
REACT_APP_LOCAL___BOOTSTRAP_FAUNADB_KEY=<bootstrap key>
Sie können optional ein Konto bei Cloudinary erstellen und Ihren Cloudnamen und eine öffentliche Vorlage (es gibt eine Standardvorlage namens „ml_default“, die Sie öffentlich machen können) in die Umgebungsvariablen einfügen, um Bilder und Videos in den Fweets einzubinden.
REACT_APP_LOCAL___CLOUDINARY_CLOUDNAME=<cloudinary cloudname>
REACT_APP_LOCAL___CLOUDINARY_TEMPLATE=<cloudinary template>
Ohne diese Variablen funktioniert die Schaltfläche zum Einbetten von Medien nicht, aber der Rest der App sollte einwandfrei laufen

Erstellung des Frontends

Für das Frontend haben wir Create React App verwendet, um eine Anwendung zu generieren, und dann die Anwendung in Seiten und Komponenten unterteilt. Seiten sind Top-Level-Komponenten mit eigenen URLs. Die Login- und Register-Seiten sprechen für sich. Home ist der Standard-Feed von Fweets von den Autoren, denen wir folgen; dies ist die Seite, die wir sehen, wenn wir uns in unser Konto einloggen. Und die Benutzer- und Tag-Seiten zeigen die Fweets für einen bestimmten Benutzer oder Tag in umgekehrter chronologischer Reihenfolge.
Wir verwenden React Router, um je nach URL zu diesen Seiten zu navigieren, wie Sie in der Datei src/app.js sehen können.
<Router>
<SessionProvider value={{ state, dispatch }}>
<Layout>
<Switch>
<Route exact path="/accounts/login">
<Login />
</Route>
<Route exact path="/accounts/register">
<Register />
</Route>
<Route path="/users/:authorHandle" component={User} />
<Route path="/tags/:tag" component={Tag} />
<Route path="/">
<Home />
</Route>
</Switch>
</Layout>
</SessionProvider>
</Router>
Das einzig andere erwähnenswerte Element im obigen Snippet ist der SessionProvider, der ein React-Kontext ist, um die Benutzerinformationen nach dem Login zu speichern. Wir werden darauf im Abschnitt Authentifizierung zurückkommen. Vorerst reicht es zu wissen, dass dies uns von jeder Komponente aus Zugriff auf die Konto- (und damit Benutzer-) Informationen gibt.
Werfen Sie einen kurzen Blick auf die Homepage (src/pages/home.js), um zu sehen, wie wir eine Kombination von Hooks verwenden, um unsere Daten zu verwalten. Der Großteil der Anwendungslogik ist in FaunaDB-Abfragen implementiert, die sich im Ordner src/fauna/queries befinden. Alle Aufrufe an die Datenbank laufen über den Query-Manager, den wir in einem zukünftigen Artikel in Serverless-Funktionsaufrufe refaktorisieren werden. Aber im Moment stammen diese Aufrufe aus dem Frontend und wir werden die sensiblen Teile davon mit den ABAC-Sicherheitsregeln und User Defined Functions (UDFs) von FaunaDB absichern. Da FaunaDB als Token-gesicherte API fungiert, müssen wir uns keine Gedanken über eine Begrenzung der Verbindungsanzahl machen, wie es bei traditionellen Datenbanken der Fall wäre.
Der FaunaDB JavaScript-Treiber
Als Nächstes werfen Sie einen Blick auf die Datei src/fauna/query-manager.js, um zu sehen, wie wir FaunaDB mit dem JavaScript-Treiber von FaunaDB an unsere Anwendung anbinden. Dies ist nur ein Node-Modul, das wir mit `npm install` installiert haben. Wie bei jedem Node-Modul importieren wir es in unsere Anwendung wie folgt:
import faunadb from 'faunadb'
Und erstellen einen Client, indem wir ein Token bereitstellen.
this.client = new faunadb.Client({
secret: token || this.bootstrapToken
})
Wir werden Tokens im Abschnitt Authentifizierung noch etwas näher behandeln. Vorerst erstellen wir einige Daten!
Daten erstellen
Die Logik zum Erstellen eines neuen Fweet-Dokuments finden Sie in der Datei src/fauna/queries/fweets.js. FaunaDB-Dokumente sind wie JSON, und jeder Fweet folgt der gleichen grundlegenden Struktur:
const data = {
data: {
message: message,
likes: 0,
refweets: 0,
comments: 0,
created: Now()
}
}
Die Funktion Now() wird verwendet, um die Zeit der Abfrage einzufügen, damit die Fweets im Feed eines Benutzers chronologisch sortiert werden können. Beachten Sie, dass FaunaDB jedem Datenbankelement automatisch Zeitstempel für zeitliche Abfragen hinzufügt. Der FaunaDB-Zeitstempel repräsentiert jedoch die Zeit, zu der das Dokument zuletzt aktualisiert wurde, nicht die Zeit, zu der es erstellt wurde, und das Dokument wird jedes Mal aktualisiert, wenn ein Fweet geliked wird; für unsere gewünschte Sortierreihenfolge benötigen wir die Erstellungszeit.
Als Nächstes senden wir diese Daten mit der Funktion Create() an FaunaDB. Indem wir Create() den Verweis auf die Fweets-Sammlung mit Collection(‘fweets’) übergeben, geben wir an, wohin die Daten gesendet werden müssen.
const query = Create(Collection('fweets'), data )
Wir können diese Abfrage nun in eine Funktion verpacken, die einen Nachrichtenparameter entgegennimmt und sie mit client.query() ausführt, was die Abfrage an die Datenbank sendet. Erst wenn wir client.query() aufrufen, wird die Abfrage an die Datenbank gesendet und ausgeführt. Zuvor kombinieren wir so viele FQL-Funktionen, wie wir möchten, um unsere Abfrage zu erstellen.
function createFweet(message, hashtags) {
const data = …
const query = …
return client.query(query)
}
Beachten Sie, dass wir einfache JavaScript-Variablen verwendet haben, um diese Abfrage zu erstellen, und im Wesentlichen einfach Funktionen aufgerufen haben. Das Schreiben von FQL dreht sich alles um Funktionskomposition; Sie erstellen Abfragen, indem Sie kleine Funktionen zu größeren Ausdrücken kombinieren. Dieser funktionale Ansatz hat sehr starke Vorteile. Er ermöglicht es uns, native Sprachfunktionen wie JavaScript-Variablen zum Erstellen von Abfragen zu verwenden und gleichzeitig Higher-Order-FQL-Funktionen zu schreiben, die vor Injection geschützt sind.
Zum Beispiel fügen wir in der folgenden Abfrage Hashtags zum Dokument mit einer `CreateHashtags()`-Funktion hinzu, die wir an anderer Stelle mit FQL definiert haben.
const data = {
data: {
// ...
hashtags: CreateHashtags(tags),
likes: 0,
// ...
}
Die Art und Weise, wie FQL aus dem Host-Sprachmodul (in diesem Fall JavaScript) heraus funktioniert, macht FQL zu einem eDSL (embedded domain-specific language). Funktionen wie `CreateHashtags()` verhalten sich genauso wie eine native FQL-Funktion, da beides Funktionen sind, die Eingaben entgegennehmen. Das bedeutet, dass wir die Sprache leicht mit unseren eigenen Funktionen erweitern können, wie in dieser Open-Source-FQL-Bibliothek aus der Fauna-Community.
Es ist auch wichtig zu beachten, dass wir zwei Entitäten in zwei verschiedenen Sammlungen in einer Transaktion erstellen. Wenn also etwas schiefgeht, besteht keine Gefahr, dass der Fweet erstellt wird, die Hashtags aber nicht. In technischeren Begriffen ist FaunaDB transaktional und konsistent, egal ob Sie Abfragen über mehrere Sammlungen hinweg ausführen oder nicht, eine Eigenschaft, die bei skalierbaren verteilten Datenbanken selten ist.
Als Nächstes müssen wir den Autor zur Abfrage hinzufügen. Zuerst können wir die FQL-Funktion Identity() verwenden, um eine Referenz auf das aktuell angemeldete Dokument zurückzugeben. Wie bereits im Abschnitt zur Datenmodellierung diskutiert, ist dieses Dokument vom Typ Konto und von Benutzern getrennt, um SSO in einer späteren Phase zu unterstützen.

Dann müssen wir Identity() in ein Get() verpacken, um auf das vollständige Konto-Dokument zuzugreifen und nicht nur auf die Referenz darauf.
Get(Identity())
Schließlich verpacken wir das alles in ein Select(), um das Feld data.user aus dem Konto-Dokument auszuwählen und es zum Daten-JSON hinzuzufügen.
const data = {
data: {
// ...
hashtags: CreateHashtags(tags),
author: Select(['data', 'user'], Get(Identity())),
likes: 0,
// ...
}
}
Nachdem wir nun die Abfrage konstruiert haben, fassen wir alles zusammen und rufen client.query(query) auf, um sie auszuführen.
function createFweet(message, hashtags) {
const data = {
data: {
message: message,
likes: 0,
refweets: 0,
comments: 0,
author: Select(['data', 'user'], Get(Identity())),
hashtags: CreateHashtags(tags),
created: Now()
}
}
const query = Create(Collection('fweets'), data )
return client.query(query)
}
Durch die Verwendung von funktionaler Komposition können Sie alle Ihre fortgeschrittenen Logiken einfach in einer Abfrage kombinieren, die in einer einzigen Transaktion ausgeführt wird. Schauen Sie sich die Datei src/fauna/queries/fweets.js an, um das Endergebnis zu sehen, das noch mehr von der Funktionskomposition nutzt, um Ratenbegrenzung usw. hinzuzufügen.
Datenabsicherung mit UDFs und ABAC-Rollen
Der aufmerksame Leser wird sich inzwischen Gedanken über die Sicherheit gemacht haben. Wir erstellen im Wesentlichen Abfragen in JavaScript und rufen diese Abfragen vom Frontend aus auf. Was hindert einen böswilligen Benutzer daran, diese Abfragen zu ändern?
FaunaDB bietet zwei Funktionen, die es uns ermöglichen, unsere Daten zu sichern: Attribute-Based Access Control (ABAC) und User Defined Functions (UDF). Mit ABAC können wir kontrollieren, auf welche Sammlungen oder Entitäten ein bestimmter Schlüssel oder Token zugreifen kann, indem wir Rollen schreiben.
Mit UDFs können wir FQL-Anweisungen in die Datenbank verschieben, indem wir CreateFunction() verwenden.
CreateFunction({
name: 'create_fweet',
body: <your FQL statement>,
})
Sobald die Funktion als UDF in der Datenbank gespeichert ist, wo die Anwendung sie nicht mehr ändern kann, rufen wir diese UDF vom Frontend auf.
client.query(
Call(Function('create_fweet'), message, hashTags)
)
Da die Abfrage nun in der Datenbank gespeichert ist (ähnlich wie ein Stored Procedure), kann der Benutzer sie nicht mehr manipulieren.
Ein Beispiel dafür, wie UDFs verwendet werden können, um einen Aufruf zu sichern, ist, dass wir den Autor des Fweets nicht übergeben. Der Autor des Fweets wird stattdessen von der `Identity()`-Funktion abgeleitet, was es einem Benutzer unmöglich macht, einen Fweet im Namen eines anderen zu schreiben.
Natürlich müssen wir immer noch definieren, dass der Benutzer Zugriff auf den Aufruf der UDF hat. Dazu verwenden wir eine sehr einfache ABAC-Rolle, die eine Gruppe von Rollenmitgliedern und deren Berechtigungen definiert. Diese Rolle wird `logged_in_role` heißen, ihre Mitglieder umfassen alle Dokumente in der Accounts-Sammlung, und allen diesen Mitgliedern wird die Berechtigung erteilt, die `create_fweet`-UDF aufzurufen.
CreateRole(
name: 'logged_in_role',
privileges: [
{
resource: q.Function('create_fweet'),
actions: {
call: true
}
}
],
membership: [{ resource: Collection('accounts') }],
)
Wir wissen jetzt, dass diese Berechtigungen einem Konto gewährt werden, aber wie werden wir zu einem Konto? Indem wir die FaunaDB-Funktion Login() verwenden, um unsere Benutzer zu authentifizieren, wie im nächsten Abschnitt erklärt.
Authentifizierung in FaunaDB implementieren

Wir haben gerade eine Rolle gezeigt, die Konten die Berechtigungen zum Aufrufen der Funktion `create_fweets` gibt. Aber wie werden wir zu einem Konto?
Zuerst erstellen wir ein neues Konto-Dokument, speichern Anmeldeinformationen neben allen anderen mit dem Konto verbundenen Daten (in diesem Fall die E-Mail-Adresse und die Referenz zum Benutzer).
return Create(Collection('accounts'), {
credentials: { password: password },
data: {
email: email,
user: Select(['ref'], Var('user'))
}
})
}
Wir können dann Login() auf der Konto-Referenz aufrufen, was ein Token zurückgibt.
Login(
Match( < Account reference > ,
{ password: password }
)
)
Wir verwenden dieses Token im Client, um das Konto zu impersonieren. Da alle Konten Mitglieder der Konto-Sammlung sind, erfüllt dieses Token die Mitgliedschaftsanforderung der `logged_in_role` und erhält Zugriff auf den Aufruf der `create_fweet`-UDF.
Um diesen gesamten Prozess zu bootstrapen, haben wir zwei sehr wichtige Rollen.
bootstrap_role: kann nur die UDFs `login` und `register` aufrufenlogged_in_role: kann andere Funktionen wie `create_fweet` aufrufen
Das Token, das Sie beim Ausführen des Setup-Skripts erhalten haben, ist im Wesentlichen ein Schlüssel, der mit der `bootstrap_role` erstellt wurde. Ein Client wird mit diesem Token in src/fauna/query-manager.js erstellt, der nur registrieren oder einloggen kann. Sobald wir uns einloggen, verwenden wir das neue Token, das von `Login()` zurückgegeben wird, um einen neuen FaunaDB-Client zu erstellen, der nun Zugriff auf andere UDF-Funktionen wie `create_fweet` gewährt. Ausloggen bedeutet, dass wir zum Bootstrap-Token zurückkehren. Sie können diesen Prozess in src/fauna/query-manager.js sehen, zusammen mit komplexeren Rollenbeispielen in der Datei src/fauna/setup/roles.js.
Die Session in React implementieren
Zuvor, im Abschnitt „Erstellung des Frontends“, erwähnten wir die SessionProvider-Komponente. In React gehören Provider zu einem React Context, einem Konzept zur Erleichterung des Datenaustauschs zwischen verschiedenen Komponenten. Dies ist ideal für Daten wie Benutzerinformationen, die Sie überall in Ihrer Anwendung benötigen. Indem wir den SessionProvider frühzeitig in das HTML einfügen, haben wir sichergestellt, dass jede Komponente Zugriff darauf hat. Nun muss eine Komponente nur noch den Kontext importieren und den „useContext“-Hook von React verwenden, um auf die Benutzerdetails zuzugreifen.
import SessionContext from '../context/session'
import React, { useContext } from 'react'
// In your component
const sessionContext = useContext(SessionContext)
const { user } = sessionContext.state
Aber wie gelangt der Benutzer in den Kontext? Als wir den SessionProvider einschlossen, übergaben wir einen Wert, der aus dem aktuellen Zustand und einer Dispatch-Funktion besteht.
const [state, dispatch] = React.useReducer(sessionReducer, { user: null })
// ...
<SessionProvider value={{ state, dispatch }}>
Der Zustand ist einfach der aktuelle Zustand, und die Dispatch-Funktion wird aufgerufen, um den Kontext zu ändern. Diese Dispatch-Funktion ist tatsächlich der Kern des Kontexts, da das Erstellen eines Kontexts nur das Aufrufen von React.createContext() beinhaltet, was Ihnen Zugriff auf einen Provider und einen Consumer gibt.
const SessionContext = React.createContext({})
export const SessionProvider = SessionContext.Provider
export const SessionConsumer = SessionContext.Consumer
export default SessionContext
Wir sehen, dass Zustand und Dispatch aus etwas extrahiert werden, das React einen Reducer nennt (mithilfe von React.useReducer), also schreiben wir einen Reducer.
export const sessionReducer = (state, action) => {
switch (action.type) {
case 'login': {
return { user: action.data.user }
}
case 'register': {
return { user: action.data.user }
}
case 'logout': {
return { user: null }
}
default: {
throw new Error(`Unhandled action type: ${action.type}`)
}
}
}
Dies ist die Logik, die es Ihnen ermöglicht, den Kontext zu ändern. Im Wesentlichen empfängt er eine Aktion und entscheidet, wie der Kontext angesichts dieser Aktion geändert werden soll. In meinem Fall ist die Aktion einfach ein Typ mit einem String. Wir verwenden diesen Kontext, um Benutzerinformationen zu speichern, was bedeutet, dass wir ihn bei erfolgreicher Anmeldung mit folgendem aufrufen:
sessionContext.dispatch({ type: 'login', data: e })
Cloudinary für Medien hinzufügen
Als wir einen Fweet erstellten, haben wir noch keine Assets berücksichtigt. FaunaDB soll Anwendungsdaten speichern, keine Bildblobs oder Videodaten. Wir können die Medien jedoch problemlos auf Cloudinary speichern und nur einen Link in FaunaDB behalten. Das Folgende fügt das Cloudinary-Skript ein (in app.js)
loadScript('https://widget.cloudinary.com/v2.0/global/all.js')
Wir erstellen dann ein Cloudinary Upload Widget (in src/components/uploader.js)
window.cloudinary.createUploadWidget(
{
cloudName: process.env.REACT_APP_LOCAL___CLOUDINARY_CLOUDNAME,
uploadPreset: process.env.REACT_APP_LOCAL___CLOUDINARY_TEMPLATE,
},
(error, result) => {
// ...
}
)
Wie bereits erwähnt, müssen Sie einen Cloudinary Cloudnamen und eine Vorlage in den Umgebungsvariablen (.env.local-Datei) angeben, um diese Funktion nutzen zu können. Das Erstellen eines Cloudinary-Kontos ist kostenlos und sobald Sie ein Konto haben, können Sie den Cloudnamen aus dem Dashboard entnehmen.

Sie haben auch die Möglichkeit, API-Schlüssel zu verwenden, um Uploads zu sichern. In diesem Fall laden wir direkt vom Frontend hoch, sodass der Upload eine öffentliche Vorlage verwendet. Um eine Vorlage hinzuzufügen oder zu ändern und sie öffentlich zu machen, klicken Sie auf das Zahnrad-Symbol in der oberen Menüleiste, gehen Sie zum Reiter Upload und klicken Sie auf Add upload preset.
Sie könnten auch die Vorlage ml_default bearbeiten und sie einfach öffentlich machen.

Nun rufen wir widget.open() auf, wenn unser Medienbutton geklickt wird.
const handleUploadClick = () => {
widget.open()
}
return (
<div>
<FontAwesomeIcon icon={faImage} onClick={handleUploadClick}></FontAwesomeIcon>
</div>
)
Dies gibt uns einen kleinen Medienbutton, der das Cloudinary Upload Widget öffnet, wenn er angeklickt wird.

Wenn wir das Widget erstellen, können wir auch Stile und Schriftarten angeben, um ihm das Aussehen unserer eigenen Anwendung zu verleihen, wie wir es oben getan haben (in src/components/uploader.js):
const widget = window.cloudinary.createUploadWidget(
{
cloudName: process.env.REACT_APP_LOCAL___CLOUDINARY_CLOUDNAME,
uploadPreset: process.env.REACT_APP_LOCAL___CLOUDINARY_TEMPLATE,
styles: {
palette: {
window: '#E5E8EB',
windowBorder: '#4A4A4A',
tabIcon: '#000000',
// ...
},
fonts: {
Sobald wir Medien auf Cloudinary hochgeladen haben, erhalten wir eine Menge Informationen über die hochgeladenen Medien, die wir dann beim Erstellen eines Fweets zu den Daten hinzufügen.

Wir können dann einfach die gespeicherte id (die Cloudinary als publicId bezeichnet) mit der Cloudinary React-Bibliothek (in src/components/asset.js) verwenden
import { Image, Video, Transformation } from 'cloudinary-react'
Um das Bild in unserem Feed anzuzeigen.
<div className="fweet-asset">
<Image publicId={asset.id}
cloudName={cloudName} fetchFormat="auto" quality="auto" secure="true" />
</div>
Wenn Sie die ID anstelle der direkten URL verwenden, führt Cloudinary eine ganze Reihe von Optimierungen durch, um die Medien im optimalsten möglichen Format zu liefern. Wenn Sie beispielsweise ein Video-Bild wie folgt hinzufügen
<div className="fweet-asset">
<Video playsInline autoPlay loop={true} controls={true} cloudName={cloudName} publicId={publicId}>
<Transformation width="600" fetchFormat="auto" crop="scale" />
</Video>
</div>
Cloudinary skaliert das Video automatisch auf eine Breite von 600 Pixeln und liefert es als WebM (VP9) an Chrome-Browser (482 KB), als MP4 (HEVC) an Safari-Browser (520 KB) oder als MP4 (H.264) an Browser, die keines der beiden Formate unterstützen (821 KB). Cloudinary nimmt diese Optimierungen serverseitig vor, was die Ladezeit der Seite und das gesamte Benutzererlebnis erheblich verbessert.
Daten abrufen
Wir haben gezeigt, wie man Daten hinzufügt. Jetzt müssen wir noch Daten abrufen. Das Abrufen der Daten unseres Fwitter-Feeds birgt viele Herausforderungen. Wir müssen:
- Fweets von Leuten abrufen, denen Sie folgen, in einer bestimmten Reihenfolge (unter Berücksichtigung von Zeit und Beliebtheit)
- den Autor des Fweets abrufen, um sein Profilbild und seinen Handle anzuzeigen
- die Statistiken abrufen, um anzuzeigen, wie viele Likes, Refweets und Kommentare es hat
- die Kommentare abrufen, um diese unter dem Fweet aufzulisten.
- Informationen darüber abrufen, ob Sie diesen spezifischen Fweet bereits geliked, refweeted oder kommentiert haben.
- Wenn es sich um einen Refweet handelt, den Original-Fweet abrufen.
Diese Art von Abfrage ruft Daten aus vielen verschiedenen Sammlungen ab und erfordert fortgeschrittene Indizierungs-/Sortierverfahren, aber wir beginnen einfach. Wie erhalten wir die Fweets? Wir beginnen damit, eine Referenz auf die Fweets-Sammlung mithilfe der Funktion Collection() abzurufen.
Collection('fweets')
Und wir verpacken das in die Funktion Documents(), um alle Dokumentreferenzen der Sammlung zu erhalten.
Documents(Collection('fweets'))
Wir paginieren dann über diese Referenzen.
Paginate(Documents(Collection('fweets')))
Paginate() erfordert eine Erklärung. Bevor wir Paginate() aufrufen, hatten wir eine Abfrage, die einen hypothetischen Datensatz zurückgab. Paginate() materialisiert diese Daten tatsächlich in Seiten von Entitäten, die wir lesen können. FaunaDB verlangt, dass wir diese Funktion Paginate() verwenden, um uns vor ineffizienten Abfragen zu schützen, die jedes Dokument aus einer Sammlung abrufen, denn in einer für massive Skalierung konzipierten Datenbank könnte diese Sammlung Millionen von Dokumenten enthalten. Ohne die Absicherung durch Paginate() könnte das sehr teuer werden!
Speichern wir diese teilweise Abfrage in einer einfachen JavaScript-Variablen `references`, auf der wir weiter aufbauen können.
const references = Paginate(Documents(Collection('fweets')))
Bisher gibt unsere Abfrage nur eine Liste von Referenzen auf unsere Fweets zurück. Um die eigentlichen Dokumente zu erhalten, machen wir genau das, was wir in JavaScript tun würden: Wir mappen die Liste mit einer anonymen Funktion durch. In FQL ist ein Lambda nur eine anonyme Funktion.
const fweets = Map(
references,
Lambda(['ref'], Get(Var('ref')))
)
Das mag umständlich erscheinen, wenn Sie prozedurale Abfragesprachen wie SQL gewohnt sind, bei denen Sie deklarieren, was Sie wollen, und die Datenbank die Umsetzung übernimmt. Im Gegensatz dazu deklariert FQL sowohl, was Sie wollen als auch, wie Sie es wollen, was es prozeduraler macht. Da Sie definieren, wie Sie Ihre Daten haben möchten, und nicht die Abfrage-Engine, sind die Kosten und die Leistungsauswirkungen Ihrer Abfrage vorhersehbar. Sie können genau bestimmen, wie viele Lesevorgänge diese Abfrage kostet, ohne sie auszuführen, was ein erheblicher Vorteil ist, wenn Ihre Datenbank eine riesige Datenmenge enthält und Sie nach Nutzungsgebühr bezahlen. Es mag also eine Lernkurve geben, aber sie ist das Geld und die Mühe, die sie Ihnen spart, wert. Und sobald Sie gelernt haben, wie FQL funktioniert, werden Sie feststellen, dass Abfragen wie normaler Code gelesen werden.
Bereiten wir unsere Abfrage vor, um sie einfach mit `Let` erweitern zu können. `Let` erlaubt es uns, Variablen zu binden und sie sofort in der nächsten Variablenbindung wiederzuverwenden, was es Ihnen ermöglicht, Ihre Abfrage eleganter zu strukturieren.
const fweets = Map(
references,
Lambda(
['ref'],
Let(
{
fweet: Get(Var('ref'))
},
// Just return the fweet for now
Var('fweet')
)
)
)
Jetzt, da wir diese Struktur haben, ist das Abrufen zusätzlicher Daten einfach. Holen wir uns also den Autor.
const fweets = Map(
references,
Lambda(
['ref'],
Let(
{
fweet: Get(Var('ref')),
author: Get(Select(['data', 'author'], Var('fweet')))
},
{ fweet: Var('fweet'), author: Var('author') }
)
)
)
Obwohl wir keinen Join geschrieben haben, haben wir gerade Benutzer (den Autor) mit den Fweets verbunden. Wir werden diese Bausteine in einem Folgeartikel weiter ausbauen. In der Zwischenzeit können Sie src/fauna/queries/fweets.js durchsuchen, um die endgültige Abfrage und mehrere weitere Beispiele anzuzeigen.
Mehr im Code
Wenn Sie das nicht bereits getan haben, öffnen Sie bitte die Codebasis für diese Fwitter-Beispiel-App. Sie finden dort eine Fülle gut kommentierter Beispiele, die wir hier noch nicht behandelt haben, aber in zukünftigen Artikeln behandeln werden. Dieser Abschnitt berührt einige Dateien, die wir Ihrer Meinung nach ansehen sollten.
Überprüfen Sie zuerst die Datei src/fauna/queries/fweets.js, um Beispiele zu sehen, wie komplexe Übereinstimmungen und Sortierungen mit den Indizes von FaunaDB durchgeführt werden (die Indizes werden in src/fauna/setup/fweets.js erstellt). Wir haben drei verschiedene Zugriffsmuster implementiert, um Fweets nach Popularität und Zeit, nach Handle und nach Tag abzurufen.

Das Abrufen von Fweets nach Popularität und Zeit ist ein besonders interessantes Zugriffsmuster, da die Fweets tatsächlich nach einer Art abnehmender Popularität sortiert werden, die auf der Interaktion der Benutzer miteinander basiert.
Schauen Sie sich auch src/fauna/queries/search.js an, wo wir Autocomplete basierend auf FaunaDB Indizes und Index Bindings implementiert haben, um nach Autoren und Tags zu suchen. Da FaunaDB über mehrere Collections indizieren kann, können wir einen Index schreiben, der eine Autocomplete-Suche sowohl für Benutzer als auch für Tags unterstützt.

Wir haben diese Beispiele implementiert, weil die Kombination aus flexiblen und leistungsstarken Indizes mit Beziehungen in skalierbaren verteilten Datenbanken selten ist. Datenbanken, denen Beziehungen und flexible Indizes fehlen, erfordern, dass Sie im Voraus wissen, wie auf Ihre Daten zugegriffen wird, und Sie werden auf Probleme stoßen, wenn sich Ihre Geschäftslogik ändern muss, um den sich entwickelnden Anwendungsfällen Ihrer Clients gerecht zu werden.
In FaunaDB, wenn Sie eine bestimmte Art des Datenzugriffs nicht vorhergesehen haben, keine Sorge – fügen Sie einfach einen Index hinzu! Wir haben Bereichsindizes, Termindizes und zusammengesetzte Indizes, die jederzeit spezifiziert werden können, ohne dass Sie sich mit Eventually Consistency auseinandersetzen müssen.
Ein Ausblick auf das Kommende
Wie in der Einleitung erwähnt, führen wir diese Fwitter-App ein, um komplexe, reale Anwendungsfälle zu demonstrieren. Allerdings fehlen noch einige Funktionen, die in zukünftigen Artikeln behandelt werden, darunter Streaming, Paginierung, Benchmarks und ein fortschrittlicheres Sicherheitsmodell mit kurzlebigen Tokens, JWT-Tokens, Single Sign-On (möglicherweise unter Verwendung eines Dienstes wie Auth0), IP-basierter Ratenbegrenzung (mit Cloudflare Workers), E-Mail-Verifizierung (mit einem Dienst wie SendGrid) und HttpOnly-Cookies.

Das Endergebnis wird ein Stack sein, der auf Diensten und serverlosen Funktionen basiert und einer dynamischen JAMstack-App sehr ähnlich ist, abzüglich des statischen Site-Generators. Bleiben Sie dran für die Folgeartikel und abonnieren Sie den Fauna Blog und verfolgen Sie CSS-Tricks für weitere FaunaDB-bezogene Artikel.
Ich glaube, es gibt einen Tippfehler bei „user“
Danke, Mann! Habe es aktualisiert
Das ist schlichtweg unglaublich!!! Vielen Dank, ich freue mich auf die Fortsetzungen :)
Danke, Brian!
Bleiben Sie dran und helfen Sie uns, die Nachricht zu verbreiten, wenn es Ihnen gefallen hat :)
Ich fand es wirklich cool, das zu lesen. Habe ziemlich viel mit Fauna für ein Finanztool bei der Arbeit herumgespielt, aber oft den Kopf gekratzt. Das hier hat viele Dinge mit einem Beispiel erklärt, das ausnahmsweise keine Todo-Liste ist. Ich freue mich auf das nächste!
Sehr geschätzt! :)
Wenn Sie sich wieder einmal den Kopf kratzen, treten Sie unserem Community-Slack bei: http://community.fauna.com/
Sie werden feststellen, dass unsere Community sehr hilfsbereit ist.
Großartige Arbeit, ich kann gar nicht beschreiben, wie sehr mir der Artikel geholfen hat. Ich freue mich so auf die Fortsetzungen!
Danke! Diese Art von Kommentar macht alles lohnenswert! :)
Schöner Artikel!
Ich freue mich auf die SSO-Integration.
Ausgezeichneter Beitrag. Ich warte auf den versprochenen nächsten Beitrag, der Cloudflare Workers beinhaltet.
Danke und entschuldigen Sie dafür. Das hätte VIEL schneller passieren sollen!
Es ist aber immer noch geplant! Ich versuche, andere Arbeiten abzuschließen und dann wieder daran zu arbeiten :)
Hallo Brecht
Vielen Dank dafür. Ich habe hier viel über Fauna gelernt. Ich kann die Fortsetzungen kaum erwarten!
Danke fürs Lesen. Die Fortsetzungen kommen immer noch. Sie schreiten jedoch nur langsam voran und könnten sich leicht ändern, da noch viel andere Arbeit zu erledigen ist.
Unglaubliches Material. Ich versuche, Fauna zu lernen, um sie mit meiner React-App zu verbinden. Vielen Dank. Ich kann nicht genug von mehr bekommen.
Viiiiiieeeeelen Dank! :)
Sehr geschätzt!
Habe gerade ein Projekt abgeschlossen, das Ihrem ähnlich ist (außer Next.js für React und useSWR+Graphql-request anstelle von FQL).
Danke fürs Teilen und Respekt
Großartig, das sollte ich mir ansehen.
Großartiger Artikel!!!
Meine größte Frage hier ist: Womit haben Sie diese erstaunlichen DB-Diagramme erstellt?