Erstellen eines kollaborativen Markdown-Editors in Echtzeit mit React Hooks, GraphQL und AWS AppSync

In diesem Beitrag wird alles behandelt, angefangen vom Erstellen der API über das Schreiben des Clientcodes bis hin zum Bereitstellen einer benutzerdefinierten Domäne.
Probieren Sie es aus - writewithme.dev
Überprüfen Sie den Code https://github.com/dabit3/write-with-me

Ich habe mich eingehend mit einigen interessanten Anwendungsfällen befasst, die einige der Echtzeitfähigkeiten von GraphQL-Abonnements zeigen.

Vor ein paar Wochen habe ich mit Hilfe von Ken Wheelers Drum-Machine HypeBeats, eine kollaborative Drum-Machine, erstellt und zuvor mit React Canvas Draw eine Echtzeit-Zeichenfläche erstellt.

Letzte Woche habe ich einen Tweet verschickt, um zu sehen, welche anderen Ideen die Leute hatten und es gab eine Menge:

Eine Idee, die mir sofort auffiel, kam von NagarajanFs auf Twitter:

Grundsätzlich war seine Idee Google Docs aber für Abschriften. Ich habe es geliebt, also habe ich beschlossen, es zu bauen! Das Ergebnis war writewithme.dev.

Anfangen

Als erstes habe ich eine neue React-Anwendung erstellt:

npx create-react-app schreib mit mir

Das nächste, was ich tun musste, war das Tool zu finden, mit dem ich Markdowns in einer React-App zulassen wollte. Ich stolperte über die Reaktion von Espen Hovlandsdal (Rexxars auf Twitter).

npm install react-markdown

React Markdown war super einfach zu bedienen. Sie importieren ReactMarkdown aus dem Paket und übergeben den Markdown, den Sie als Requisite rendern möchten:

const ReactMarkdown = require ('React-Markdown')

const input = '# Dies ist eine Überschrift \ n \ nUnd dies ist ein Absatz'

API erstellen

Nachdem wir nun das Markdown-Tool haben, wurde im nächsten Satz die API erstellt. Ich habe dazu AWS AppSync und AWS Amplify verwendet. Mit der Amplify-CLI können Sie einen Basistyp festlegen und einen Dekorator hinzufügen, um das gesamte Backend zu erstellen, indem Sie die GraphQL-Transformationsbibliothek nutzen.

verstärken init
amplify add api
// GraphQL gewählt
// Wählen Sie API Key als Authentifizierung

Das Schema, das ich verwendet habe, war folgendes:

Typ Post @model {
  Ich tat!
  clientId: ID!
  Abschlag: String!
  titel: string!
  createdAt: String
}
Der @Model-Dekorator weist Amplify an, ein GraphQL-Backend mit einer DynamoDB-Datenquelle und einem Schema (Typen und Operationen) für alle Rohoperationen und GraphQL-Subskriptionen bereitzustellen. Es werden auch die Resolver für die erstellten GraphQL-Operationen erstellt.

Nach dem Definieren und Speichern des Schemas stellen wir die AppSync-API bereit:

verstärken schieben

Wenn wir Amplify Push ausführen, haben wir auch die Möglichkeit, GraphQL-Codegen entweder in TypeScript, JavaScript oder Flow auszuführen. Dadurch wird Ihr GraphQL-Schema überprüft und automatisch der GraphQL-Code generiert, den Sie auf dem Client benötigen, um Abfragen, Mutationen und Abonnements auszuführen.

Abhängigkeiten installieren

Als nächstes installieren wir die anderen notwendigen Bibliotheken, die wir für die App benötigen:

npm installiere aws-amplify
  • uuid - um eindeutige IDs zur eindeutigen Identifizierung des Clients zu erstellen
  • reag-router-dom - um routing hinzuzufügen
  • aws-amplify - um mit der AppSync-API zu interagieren
  • glamour - für styling
  • debounce - Zum Hinzufügen einer Entprellung, wenn der Benutzer etwas eingibt

Code schreiben

Nachdem unser Projekt eingerichtet und unsere API bereitgestellt wurde, können wir mit dem Schreiben von Code beginnen!

Die App hat drei Hauptdateien:

  • Router.js - Definiert die Routen
  • Posts.js - Ruft die Posts ab und rendert sie
  • Post.js - Holt und rendert einen einzelnen Beitrag

Router.js

Das Einrichten der Navigation war ziemlich einfach. Wir brauchten zwei Routen: eine zum Auflisten aller Beiträge und eine zum Anzeigen eines einzelnen Beitrags. Ich habe mich für folgendes Streckenschema entschieden:

/ post /: id /: title

Wenn jemand die obige Route wählt, haben wir Zugriff auf alles, was wir zum Anzeigen eines Titels für den Beitrag sowie der ID zum Abrufen des Beitrags benötigen, wenn es sich um einen vorhandenen Beitrag handelt. Alle diese Informationen sind direkt in den Routenparametern verfügbar.

Reagieren Sie Hooks mit GraphQL

Wenn Sie sich jemals gefragt haben, wie Sie Hooks in eine GraphQL-Anwendung implementieren können, empfehlen wir Ihnen, auch meinen Beitrag zum Schreiben von benutzerdefinierten Hooks für GraphQL zu lesen, da dies genau das ist, was wir tun werden.

Anstatt den gesamten Code für die beiden Komponenten durchzulesen (wenn Sie den Code sehen möchten, klicken Sie auf die obigen Links), möchte ich mich darauf konzentrieren, wie wir die erforderlichen Funktionen mithilfe von GraphQL und Hooks implementiert haben.

Die wichtigsten API-Vorgänge, die wir für diese App benötigen, sollen Folgendes bewirken:

  1. Laden Sie alle Posts von der API
  2. Abonnieren Sie neue Beiträge, die von anderen erstellt werden
  3. Laden Sie einen einzelnen Beitrag
  4. Abonnieren Sie Änderungen in einem einzelnen Beitrag und rendern Sie die Komponente neu

Werfen wir einen Blick darauf.

Lädt alle Posts von der API

Um die Beiträge zu laden, ging ich mit einem useReducer-Hook in Kombination mit einem Funktionsaufruf in einem useEffect-Hook. Wir definieren zunächst den Ausgangszustand. Beim ersten Rendern der Komponente rufen wir die API auf und aktualisieren den Status mit der Funktion fetchPosts. Dieser Reduzierer übernimmt auch unser Abonnement:

{listPosts} aus './graphql/queries' importieren
{API, graphqlOperation} aus 'aws-amplify' importieren
const initialState = {
  Beiträge: [],
  Laden: wahr,
  Fehler: falsch
}
Funktionsreduzierer (Zustand, Aktion) {
  switch (action.type) {
    case 'fetchPostsSuccess':
      return {... state, posts: action.posts, loading: false}
    case 'addPostFromSubscription':
      return {... state, posts: [action.post, ... state.posts]}
    case 'fetchPostsError':
      return {... state, loading: false, error: true}
    Standard:
      wirf einen neuen Fehler ();
  }
}
async function fetchPosts (dispatch) {
  Versuchen {
    const postData = warte auf API.graphql (graphqlOperation (listPosts))
    Versand({
      Typ: 'fetchPostsSuccess',
      posts: postData.data.listPosts.items
    })
  } catch (err) {
    console.log ('Fehler beim Abrufen von Beiträgen ...:', err)
    Versand({
      Typ: 'fetchPostsError',
    })
  }
}
// im Haken
const [postsState, dispatch] = useReducer (Reducer, initialState)
useEffect (() => {
  fetchPosts (Versand)
}, [])

Abonnieren neuer Posts, die erstellt werden

Wir haben bereits unseren Status bereit, um mit dem obigen Code fortzufahren. Jetzt müssen wir nur noch die Änderungen im Hook abonnieren. Dazu verwenden wir einen useEffect-Hook und abonnieren das onCreatePost-Abonnement:

useEffect (() => {
  const subscriber = API.graphql (graphqlOperation (onCreatePost)). subscribe ({
    next: data => {
      const postFromSub = data.value.data.onCreatePost
      Versand({
        Typ: 'addPostFromSubscription',
        post: postFromSub
      })
    }
  });
  return () => subscriber.unsubscribe ()
}, [])

In diesem Hook richten wir ein Abonnement ein, das ausgelöst wird, wenn ein Benutzer einen neuen Beitrag erstellt. Wenn die Daten eingehen, rufen wir die Versandfunktion auf und übergeben die neuen Postdaten, die vom Abonnement zurückgegeben wurden.

Einen einzelnen Beitrag abrufen

Wenn ein Benutzer auf einer Route landet, können wir die Daten aus den Routenparametern verwenden, um den Postnamen und die ID zu identifizieren:

https://www.writewithme.dev/#/post/9999b0bb-63eb-4f5b-9805-23f6c2661478/%F0%9F%94%A5%20Write%20with%20me

In dieser Route wäre die ID 9999b0bb-63eb-4f5b-9805–23f6c2661478 und der Name wäre Write with me.

Wenn die Komponente geladen wird, extrahieren wir diese Parameter und verwenden sie.

Als erstes versuchen wir, einen neuen Beitrag zu erstellen. Wenn dies erfolgreich ist, sind wir fertig. Wenn dieser Beitrag bereits vorhanden ist, erhalten wir die Daten für diesen Beitrag aus dem API-Aufruf.

Das mag auf den ersten Blick merkwürdig erscheinen, oder? Warum nicht versuchen, & abzurufen, wenn die Erstellung fehlschlägt? Nun, wir möchten die Gesamtzahl der API-Aufrufe reduzieren. Wenn wir versuchen, einen neuen Beitrag zu erstellen und der Beitrag existiert, gibt die API tatsächlich die Daten des vorhandenen Beitrags zurück, sodass wir nur einen einzigen API-Aufruf tätigen können. Diese Daten sind in den Fehlern verfügbar:

err.errors [0] .data

Wir behandeln den Status in dieser Komponente mit useReducer hook.

// Ausgangszustand
const post = {
  id: params.id,
  titel: params.title,
  clientId: CLIENTID,
  markdown: '# Loading ...'
}
Funktionsreduzierer (Zustand, Aktion) {
  switch (action.type) {
    case 'updateMarkdown':
      return {... state, markdown: action.markdown, clientId: CLIENTID};
    case 'updateTitle':
      return {] ... state, title: action.title, clientId: CLIENTID};
    case 'updatePost':
      Rückkehr action.post
    Standard:
      wirf einen neuen Fehler ();
  }
}
Async-Funktion createNewPost (Post, Versand) {
  Versuchen {
    const postData = wait API.graphql (graphqlOperation (createPost, {input: post}))
    Versand({
      Geben Sie ein: 'updatePost',
      Beitrag: {
        ... postData.data.createPost,
        clientId: CLIENTID
      }
    })
  } catch (err) {
    if (err.errors [0] .errorType === "DynamoDB: ConditionalCheckFailedException") {
      const existingPost = err.errors [0] .data
      Versand({
        Geben Sie ein: 'updatePost',
        Beitrag: {
          ... BestehendePost,
          clientId: CLIENTID
        }
      })
    }
  }
}
// Initialisiere im Hook den Zustand
 const [postState, dispatch] = useReducer (Reducer, Post)
// Post holen
useEffect (() => {
  const post = {
    ... postState,
    Abschrift: Eingabe
  }
  createNewPost (Post, Versand)
}, [])

Beitragsänderung abonnieren

Das Letzte, was wir tun müssen, ist, Änderungen in einem Beitrag zu abonnieren. Dazu machen wir zwei Dinge:

  1. Aktualisieren Sie die API, wenn der Benutzer etwas eingibt (sowohl Titel als auch Abschrift ändern sich).
  2. Änderungen abonnieren, sobald der Beitrag aktualisiert wurde
async function updatePost (post) {
  Versuchen {
    warte auf API.graphql (graphqlOperation (UpdatePost, {input: post}))
    console.log ('Beitrag wurde aktualisiert!')
  } catch (err) {
    console.log ('error:', err)
  }
}
Funktion updateMarkdown (e) {
  Versand({
    Geben Sie ein: 'updateMarkdown',
    Abschrift: e.target.value,
  })
  const newPost = {
    id: post.id,
    Abschrift: e.target.value,
    clientId: CLIENTID,
    createdAt: post.createdAt,
    Titel: postState.title
  }
  updatePost (newPost, dispatch)
}
Funktion updatePostTitle (e) {
  Versand({
    Geben Sie ein: 'updateTitle',
    Titel: e.target.value
  })
  const newPost = {
    id: post.id,
    markdown: postState.markdown,
    clientId: CLIENTID,
    createdAt: post.createdAt,
    Titel: e.target.value
  }
  updatePost (newPost, dispatch)
}
useEffect (() => {
  const subscriber = API.graphql (graphqlOperation (onUpdatePost, {
    id: post.id
  })).abonnieren({
    next: data => {
      if (CLIENTID === data.value.data.onUpdatePost.clientId) zurückgeben
      const postFromSub = data.value.data.onUpdatePost
      Versand({
        Geben Sie ein: 'updatePost',
        post: postFromSub
      })
    }
  });
  return () => subscriber.unsubscribe ()
}, [])

Wenn der Benutzer etwas eingibt, aktualisieren wir sowohl den lokalen Status als auch die API. Im Abonnement prüfen wir zunächst, ob die eingehenden Abonnementdaten von derselben Client-ID stammen. Wenn es so ist, tun wir nichts. Wenn es von einem anderen Client stammt, aktualisieren wir den Status.

Bereitstellen der App in einer benutzerdefinierten Domäne

Nachdem wir die App erstellt haben, können Sie sie wie mit writewithme.dev auf einer benutzerdefinierten Domain bereitstellen.

Ich habe das mit GoDaddy, Route53 und der Amplify-Konsole gemacht.

Gehen Sie im AWS-Dashboard zu Route53 und klicken Sie auf Gehostete Zonen. Wählen Sie Create Hosted Zone. Geben Sie dort Ihren Domainnamen ein und klicken Sie auf "Erstellen".

Stellen Sie sicher, dass Sie Ihren Domain-Namen unverändert ohne www eingeben. Z.B. writewithme.dev

Jetzt sollten Sie im Route53-Dashboard 4 Nameserver erhalten. Legen Sie in Ihrem Hosting-Konto diese benutzerdefinierten Nameserver in Ihrer DNS-Einstellung für die von Ihnen verwendete Domain fest.

Diese Nameserver sollten ungefähr so ​​aussehen wie ns-1355.awsdns-41.org, ns-1625.awsdns-11.co.uk, etc…

Klicken Sie anschließend in der Amplify Console im Abschnitt Bereitstellen auf Erste Schritte.

Verbinde deinen GitHub Account und wähle dann das Repo & Branch aus, in dem dein Projekt lebt.

Auf diese Weise werden Sie durch die Bereitstellung der App in der Amplify Console geführt und können sie live schalten. Sobald Sie fertig sind, sollten Sie einige Informationen zum Build und einige Screenshots der App sehen:

Klicken Sie anschließend im linken Menü auf Domänenverwaltung. Klicken Sie anschließend auf die Schaltfläche Domain hinzufügen.

Hier sollte das Dropdown-Menü die Domain anzeigen, die Sie in Route53 haben. Wählen Sie diese Domain aus und klicken Sie auf Domain konfigurieren.

Dadurch sollte die App auf Ihrer Domain bereitgestellt werden (dies dauert zwischen 5 und 20 Minuten). Das Letzte ist, Weiterleitungen einzurichten. Klicken Sie auf Rewrites & Redirects.

Stellen Sie sicher, dass die Umleitung für die Domain folgendermaßen aussieht (d. H. Leiten Sie den https: // Websitenamen zu https: //www.websitename um):

Nächste Schritte

Als Nächstes möchte ich Authentifizierungs- und Kommentarfunktionen hinzufügen (damit die Nutzer jeden einzelnen Beitrag kommentieren können). Dies ist alles mit Amplify & AppSync möglich. Wenn wir die Authentifizierung hinzufügen möchten, können wir sie mit der CLI hinzufügen:

verstärken hinzufügen auth

Als Nächstes müssen wir eine Logik schreiben, um die Benutzer an- und abzumelden, oder wir können den withAuthenticator-HOC verwenden. Weitere Informationen hierzu finden Sie in diesen Dokumenten.

Als Nächstes möchten wir wahrscheinlich das Schema aktualisieren, um Kommentarfunktionen hinzuzufügen. Zu diesem Zweck könnten wir das Schema auf das aktualisieren, was ich hier erstellt habe. In diesem Schema haben wir ein Feld für die Diskussion hinzugefügt (in unserem Fall würde es wahrscheinlich Kommentar heißen).

Im Resolver könnten wir wahrscheinlich auch die Identität eines Benutzers mit der Nachricht korrelieren, aber die $ context.identity.sub verwenden, die verfügbar wäre, nachdem der Benutzer angemeldet ist.

Den endgültigen Code finden Sie unter https://github.com/dabit3/write-with-me
Mein Name ist Nader Dabit. Ich bin Developer Advocate bei AWS und arbeite mit Projekten wie AWS AppSync und AWS Amplify.
Ich bin auch der Autor von "React Native in Action" und der Herausgeber von "React Native Training" und "OpenGraphQL".