One place for hosting & domains

      So erstellen Sie ein neuronales Netz zum Übersetzen der Gebärdensprache ins Englische


      Der Autor hat Code.org ausgewählt, um im Rahmen des Programms Write for DOnations eine Spende zu erhalten.

      Einführung

      Computer Vision (deutsch: computerbasiertes Sehen) ist ein Teilbereich der Informatik, mit dem ein höherrangiges Verstehen von Bildern und Videos ermöglicht werden soll. Damit werden Technologien wie lustige Video-Chat-Filter, die Gesichtserkennung Ihres Mobilgeräts und selbstfahrende Autos unterstützt.

      In diesem Tutorial nutzen Sie Computer Vision, um einen Übersetzer für die amerikanische Gebärdensprache zu entwickeln, der mithilfe Ihrer Webcam arbeitet. Während des Tutorial werden Sie OpenCV, eine Computer-Vision-Bibliothek, PyTorch zum Einrichten eines tiefen neuronalen Netzes und onnx zum Exportieren Ihres neuronalen Netzes verwenden. Zudem werden Sie eine Computer-Vision-Anwendung erstellen und dabei folgende Konzepte anwenden:

      • Sie verwenden dieselbe dreistufige Methode, die auch im Tutorial How To Apply Computer Vision to Build an Emotion-Based Dog Filter (So wenden Sie Computer Vision beim Erstellen eines emotionsbasierten Hundefilters an) genutzt wird: Vorverarbeitung eines Datensatzes, Trainieren eines Modells und Bewertung des Modells.
      • Außerdem werden Sie jeden dieser einzelnen Schritte erweitern: Sie nutzen Data Augmentation (Datenanreicherung) für den Umgang mit gedrehten oder nicht zentrierten Händen, Sie ändern die Learning Rate Schedules (Zeitpläne für die Lernrate), um die Modellgenauigkeit zu verbessern, und Sie exportieren Modelle für eine höhere Inferenzgeschwindigkeit.
      • Überdies werden Sie auch verwandte Konzepte im maschinellen Lernen erkunden.

      Am Ende dieses Tutorials verfügen Sie über einen Übersetzer für die amerikanische Gebärdensprache sowie über ein umfassendes Know-how über das Deep Learning. Sie können auch auf den kompletten Quellcode für dieses Projekt zugreifen.

      Voraussetzungen

      Um dieses Tutorial zu absolvieren, benötigen Sie Folgendes:

      • Eine lokale Entwicklungsumgebung für Python 3 mit mindestens 1 GB RAM. Unter How to Install and Set Up a Local Programming Environment for Python 3 (Installieren und Einrichten einer lokalen Programmierumgebung für Python 3) finden Sie Informationen darüber, wie Sie die benötigten Konfigurationen vornehmen.
      • Eine funktionierende Webcam zur Nutzung der Bilderkennung in Echtzeit.
      • (Empfohlen) Build an Emotion-Based Dog Filter (Erstellen eines emotionsbasierten Hundes); dieses Tutorial wird zwar nicht explizit verwendet, aber es wird dasselbe Wissen vermittelt und darauf aufgebaut.

      Schritt 1 – Erstellen des Projekts und Installieren von Abhängigkeiten

      Wir wollen einen Arbeitsbereich für dieses Projekt erstellen und die Abhängigkeiten installieren, die wir benötigen.

      Beginnen Sie mit den Linux-Distributionen, indem Sie Ihre Systempaketverwaltung vorbereiten und das Python3 virtualenv-Paket installieren. Verwenden Sie Folgendes:

      • apt-get update
      • apt-get upgrade
      • apt-get install python3-venv

      Wir nennen unseren Arbeitsbereich SignLanguage (Gebärdensprache):

      Navigieren Sie zum Verzeichnis SignLanguage:

      Erstellen Sie dann eine neue virtuelle Umgebung für das Projekt:

      • python3 -m venv signlanguage

      Aktivieren Sie Ihre Umgebung:

      • source signlanguage/bin/activate

      Installieren Sie anschließend PyTorch, ein Deep-Learning-Framework für Python, das wir in diesem Tutorial verwenden werden.

      Auf macOS installieren Sie Pytorch mit dem folgenden Befehl:

      • python -m pip install torch==1.2.0 torchvision==0.4.0

      Auf Linux und Windows verwenden Sie die folgenden Befehle für einen reinen CPU-Build:

      • pip install torch==1.2.0+cpu torchvision==0.4.0+cpu -f https://download.pytorch.org/whl/torch_stable.html
      • pip install torchvision

      Installieren Sie nun vorgefertigte Binärdateien für OpenCV, numpy und onnx, die als Bibliotheken für Computer Vision, lineare Algebra, den Export des KI-Modells und die Ausführung des KI-Modells dienen. OpenCV bietet Hilfsfunktionen wie Bilddrehung und numpy bietet Hilfsfunktionen für lineare Algebra an, z. B. eine Matrixinversion:

      • python -m pip install opencv-python==3.4.3.18 numpy==1.14.5 onnx==1.6.0 onnxruntime==1.0.0

      Auf Linux-Distributionen müssen Sie libSM.so installieren:

      • apt-get install libsm6 libxext6 libxrender-dev

      Wenn die Abhängigkeiten installiert wurden, erstellen wir die erste Version unseres Gebärdensprachenübersetzers: einen Gebärdensprachen-Classifier.

      Schritt 2 — Vorbereiten des Datensatzes für die Klassifikation der Gebärdensprache

      In diesen nächsten drei Abschnitten erstellen Sie einen Gebärdensprachen-Classifier mithilfe eines neuronalen Netzes. Ihr Ziel besteht darin, ein Modell zu erstellen, das ein Bild einer Hand als Eingabe annimmt und einen Buchstaben ausgibt.

      Für das Erstellen eines Klassifizierungsmodells für das maschinelle Lernen sind Sie die folgenden drei Schritte erforderlich:

      1. Vorbearbeiten der Daten: Wenden Sie one-hot encoding (One-Hot-Kodierung) auf Ihre Labels an und umschließen Sie Ihre Daten mit PyTorch-Tensoren. Trainieren Sie Ihr Modell auf angereicherten Daten, um es auf „unübliche“ Eingabedaten vorzubereiten, z. B. eine außermittige oder eine gedrehte Hand.
      2. Legen Sie das Modell fest und trainieren Sie es: Richten Sie ein neuronales Netz mit PyTorch ein. Legen Sie die Hyperparameter für das Training fest (z. B. wie lange das Training dauern soll) und führen Sie ein stochastisches Gradientenverfahren durch. Variieren Sie zudem einen bestimmten Hyperparameter für das Training: Learning Rate Schedule. Dadurch wird die Modellgenauigkeit erhöht.
      3. Führen Sie eine Vorhersage mit dem Modell aus: Bewerten Sie das neuronale Netz anhand Ihrer Validierungsdaten, um dessen Genauigkeit zu erfassen. Exportieren Sie dann das Modell in ein Format namens ONNX, um höhere Inferenzgeschwindigkeiten zu erreichen.

      In diesem Abschnitt des Tutorials führen Sie Schritt 1 von 3 durch. Sie werden die Daten herunterladen, ein Dataset-Objekt erstellen, das wiederholt auf Ihre Daten angewendet wird, und abschließend noch die Data Augmentation anwenden. Am Ende dieses Schritts verfügen Sie über ein Programm, mit dem Sie auf Bilder und Labels in Ihrem Datensatz zugreifen können, um Ihr Modell zu füttern.

      Laden Sie zuerst den Datensatz in Ihr aktuelles Arbeitsverzeichnis herunter:

      Anmerkung: Auf makOS ist wget standardmäßig nicht verfügbar. Installieren Sie dazu HomeBrew, indem Sie diesem DigitalOcean Tutorial folgen. Führen Sie dann brew install wget aus.

      • wget https://assets.digitalocean.com/articles/signlanguage_data/sign-language-mnist.tar.gz

      Entzippen Sie die Zip-Datei, die das Verzeichnis data/ enthält:

      • tar -xzf sign-language-mnist.tar.gz

      Erstellen Sie eine neue Datei namens step_2_dataset.py:

      Importieren Sie wie zuvor die erforderlichen Hilfsfunktionen und erstellen Sie die Klasse, die Ihre Daten enthalten soll. Erstellen Sie die Trainings- und Testdaten zum Zwecke der Datenverarbeitung. Sie implementieren die Dataset-Schnittstelle von PyTorch, damit Sie die integrierte Daten-Pipeline von PyTorch laden und für den Datensatz Ihrer Gebärdensprachenklassifikation verwenden können:

      step_2_dataset.py

      from torch.utils.data import Dataset
      from torch.autograd import Variable
      import torch.nn as nn
      import numpy as np
      import torch
      
      import csv
      
      
      class SignLanguageMNIST(Dataset):
          """Sign Language classification dataset.
      
          Utility for loading Sign Language dataset into PyTorch. Dataset posted on
          Kaggle in 2017, by an unnamed author with username `tecperson`:
          https://www.kaggle.com/datamunge/sign-language-mnist
      
          Each sample is 1 x 1 x 28 x 28, and each label is a scalar.
          """
          pass
      

      Löschen Sie den Platzhalter pass in der Klasse SignLanguageMNIST. Fügen Sie an seiner Stelle eine Methode hinzu, um ein Label Mapping zu generieren:

      step_2_dataset.py

          @staticmethod
          def get_label_mapping():
              """
              We map all labels to [0, 23]. This mapping from dataset labels [0, 23]
              to letter indices [0, 25] is returned below.
              """
              mapping = list(range(25))
              mapping.pop(9)
              return mapping
      

      Die Labels reichen von 0 bis 25. Die Buchstaben J (9) und Z (25) sind jedoch ausgeschlossen. Das bedeutet, dass es nur 24 gültige Label-Werte gibt. Damit der Satz aller von 0 ausgehenden Label-Werte zusammenhängend ist, werden alle Labels [0, 23] zugeordnet. Dieses Mapping von den Datensätzen [0, 23] bis zu den Buchstabenindizes [0, 25] wird mithilfe der Methode get_label_mapping herbeigeführt.

      Als Nächstes fügen Sie eine Methode hinzu, um Labels und Beispielproben aus einer CSV-Datei zu extrahieren. Im Folgenden wird davon ausgegangen, dass jede Zeile mit dem label startet, auf das 784-Pixelwerte folgen. Diese 784 Pixelwerte repräsentieren ein 28x28 Bild:

      step_2_dataset.py

          @staticmethod
          def read_label_samples_from_csv(path: str):
              """
              Assumes first column in CSV is the label and subsequent 28^2 values
              are image pixel values 0-255.
              """
              mapping = SignLanguageMNIST.get_label_mapping()
              labels, samples = [], []
              with open(path) as f:
                  _ = next(f)  # skip header
                  for line in csv.reader(f):
                      label = int(line[0])
                      labels.append(mapping.index(label))
                      samples.append(list(map(int, line[1:])))
              return labels, samples
      

      Eine Erklärung darüber, wie diese 784 Werte ein Bild repräsentieren, finden Sie unter Build an Emotion-Based Dog Filter, Step 4 (Erstellen eines emotionsbasierten Hundes, Schritt 4).

      Beachten Sie, dass jede Zeile im csv.reader-Iterable eine Liste von Zeichenfolgen ist; die Aufrufe int und map(int, ...) wandeln alle Zeichenfolgen in Ganzzahlen um. Fügen Sie direkt unter unserer statischen Methode eine Funktion hinzu, die unseren Datenbehälter initialisieren wird:

      step_2_dataset.py

          def __init__(self,
                  path: str="data/sign_mnist_train.csv",
                  mean: List[float]=[0.485],
                  std: List[float]=[0.229]):
              """
              Args:
                  path: Path to `.csv` file containing `label`, `pixel0`, `pixel1`...
              """
              labels, samples = SignLanguageMNIST.read_label_samples_from_csv(path)
              self._samples = np.array(samples, dtype=np.uint8).reshape((-1, 28, 28, 1))
              self._labels = np.array(labels, dtype=np.uint8).reshape((-1, 1))
      
              self._mean = mean
              self._std = std
      

      Diese Funktion startet mit dem Laden von Samples und Labels. Dann umschließt sie die Daten mit NumPy-Arrays. Die mittlere und Standardabweichung wird kurz im folgenden Abschnitt __getitem__ erklärt.

      Fügen Sie direkt nach der Funktion __init__ eine Funktion __len__ hinzu. Diese Methode wird für das Dataset benötigt, um zu ermitteln, wann das Iterieren über die Daten beendet werden muss.

      step_2_dataset.py

      ...
          def __len__(self):
              return len(self._labels)
      

      Fügen Sie abschließend die Methode __getitem__ hinzu, die ein Wörterbuch zurückgibt, das das Sample und das Label enthält:

      step_2_dataset.py

          def __getitem__(self, idx):
              transform = transforms.Compose([
                  transforms.ToPILImage(),
                  transforms.RandomResizedCrop(28, scale=(0.8, 1.2)),
                  transforms.ToTensor(),
                  transforms.Normalize(mean=self._mean, std=self._std)])
      
              return {
                  'image': transform(self._samples[idx]).float(),
                  'label': torch.from_numpy(self._labels[idx]).float()
              }
      

      Sie verwenden eine Technik namens Data Augmentation, bei der Samples während des Trainings gestört werden, um die Robustheit des Modells gegenüber diesen Störungen zu erhöhen. Hierfür wird insbesondere das Bild über RandomResizedCrop in variierenden Werten und an verschiedenen Stellen eingezoomt. Beachten Sie, dass sich das Einzoomen nicht auf die finale Gebärdensprachenklasse auswirken sollte. So wird das Label nicht transformiert. Sie normalisieren die Eingaben zusätzlich, damit die Bildwerte wie erwartet auf den Bereich [0, 1] neu skaliert werden anstatt auf [0, 255]; verwenden Sie bei der Normalisierung den Datensatz _mean und _std, um dies zu erreichen.

      Ihre abgeschlossene Klasse SignLanguageMNIST sieht wie folgt aus:

      step_2_dataset.py

      from torch.utils.data import Dataset
      from torch.autograd import Variable
      import torchvision.transforms as transforms
      import torch.nn as nn
      import numpy as np
      import torch
      
      from typing import List
      
      import csv
      
      
      class SignLanguageMNIST(Dataset):
          """Sign Language classification dataset.
      
          Utility for loading Sign Language dataset into PyTorch. Dataset posted on
          Kaggle in 2017, by an unnamed author with username `tecperson`:
          https://www.kaggle.com/datamunge/sign-language-mnist
      
          Each sample is 1 x 1 x 28 x 28, and each label is a scalar.
          """
      
          @staticmethod
          def get_label_mapping():
              """
              We map all labels to [0, 23]. This mapping from dataset labels [0, 23]
              to letter indices [0, 25] is returned below.
              """
              mapping = list(range(25))
              mapping.pop(9)
              return mapping
      
          @staticmethod
          def read_label_samples_from_csv(path: str):
              """
              Assumes first column in CSV is the label and subsequent 28^2 values
              are image pixel values 0-255.
              """
              mapping = SignLanguageMNIST.get_label_mapping()
              labels, samples = [], []
              with open(path) as f:
                  _ = next(f)  # skip header
                  for line in csv.reader(f):
                      label = int(line[0])
                      labels.append(mapping.index(label))
                      samples.append(list(map(int, line[1:])))
              return labels, samples
      
          def __init__(self,
                  path: str="data/sign_mnist_train.csv",
                  mean: List[float]=[0.485],
                  std: List[float]=[0.229]):
              """
              Args:
                  path: Path to `.csv` file containing `label`, `pixel0`, `pixel1`...
              """
              labels, samples = SignLanguageMNIST.read_label_samples_from_csv(path)
              self._samples = np.array(samples, dtype=np.uint8).reshape((-1, 28, 28, 1))
              self._labels = np.array(labels, dtype=np.uint8).reshape((-1, 1))
      
              self._mean = mean
              self._std = std
      
          def __len__(self):
              return len(self._labels)
      
          def __getitem__(self, idx):
              transform = transforms.Compose([
                  transforms.ToPILImage(),
                  transforms.RandomResizedCrop(28, scale=(0.8, 1.2)),
                  transforms.ToTensor(),
                  transforms.Normalize(mean=self._mean, std=self._std)])
      
              return {
                  'image': transform(self._samples[idx]).float(),
                  'label': torch.from_numpy(self._labels[idx]).float()
              }
      

      Wie zuvor überprüfen Sie unsere Datensatz-Hilfsfunktionen, indem Sie den Datensatz SignLanguageMNIST laden. Fügen Sie am Ende Ihrer Datei hinter der Klasse SignLanguageMNIST den folgenden Code hinzu:

      step_2_dataset.py

      def get_train_test_loaders(batch_size=32):
          trainset = SignLanguageMNIST('data/sign_mnist_train.csv')
          trainloader = torch.utils.data.DataLoader(trainset, batch_size=batch_size, shuffle=True)
      
          testset = SignLanguageMNIST('data/sign_mnist_test.csv')
          testloader = torch.utils.data.DataLoader(testset, batch_size=batch_size, shuffle=False)
          return trainloader, testloader
      

      Dieser Code initialisiert den Datensatz mithilfe der Klasse SignLanguageMNIST. Für die Trainings- und Validierungssätze wird dann der Datensatz mit einem DataLoader umschlossen. Dadurch wird der Datensatz für den späteren Gebrauch in ein Iterable umgewandelt.

      Nun überprüfen Sie, ob die Datensatz-Hilfsfunktionen funktionieren. Erstellen Sie einen Sample-Datensatz-Loader mithilfe von DataLoader und drucken Sie das erste Element dieses Loaders aus. Fügen Sie Folgendes am Ende der Datei hinzu:

      step_2_dataset.py

      if __name__ == '__main__':
          loader, _ = get_train_test_loaders(2)
          print(next(iter(loader)))
      

      Sie können überprüfen, ob Ihre Datei mit der Datei step_2_dataset in diesem (repository) übereinstimmt. Beenden Sie Ihren Editor und führen Sie das Skript mit Folgendem aus:

      Dadurch werden folgende Tensoren paarweise ausgegeben. Unsere Datenpipeline gibt zwei Samples und zwei Labels aus. Dies zeigt an, dass unsere Datenpipeline eingerichtet ist und bereit ist, fortzufahren:

      Output

      {'image': tensor([[[[ 0.4337, 0.5022, 0.5707, ..., 0.9988, 0.9646, 0.9646], [ 0.4851, 0.5536, 0.6049, ..., 1.0502, 1.0159, 0.9988], [ 0.5364, 0.6049, 0.6392, ..., 1.0844, 1.0844, 1.0673], ..., [-0.5253, -0.4739, -0.4054, ..., 0.9474, 1.2557, 1.2385], [-0.3369, -0.3369, -0.3369, ..., 0.0569, 1.3584, 1.3242], [-0.3712, -0.3369, -0.3198, ..., 0.5364, 0.5364, 1.4783]]], [[[ 0.2111, 0.2796, 0.3481, ..., 0.2453, -0.1314, -0.2342], [ 0.2624, 0.3309, 0.3652, ..., -0.3883, -0.0629, -0.4568], [ 0.3309, 0.3823, 0.4337, ..., -0.4054, -0.0458, -1.0048], ..., [ 1.3242, 1.3584, 1.3927, ..., -0.4054, -0.4568, 0.0227], [ 1.3242, 1.3927, 1.4612, ..., -0.1657, -0.6281, -0.0287], [ 1.3242, 1.3927, 1.4440, ..., -0.4397, -0.6452, -0.2856]]]]), 'label': tensor([[24.], [11.]])}

      Sie haben nun sichergestellt, dass Ihre Datenpipeline funktioniert. Damit ist der erste Schritt – die Verarbeitung Ihrer Daten – abgeschlossen. Nun beinhaltet sie Data Augmentation für eine erhöhte Modellstabilität. Als Nächstes definieren Sie das neuronale Netz und den Optimierer.

      Schritt 3 – Erstellen und Trainieren des Gebärdensprachen-Classifiers mittels Deep Learning

      Mit einer funktionierenden Datenpipeline definieren Sie nun ein Modell und trainieren es anhand der Daten. Insbesondere erstellen Sie ein neuronales Netz mit sechs Schichten, definieren einen Verlust, einen Optimierer und optimieren abschließend die Verlustfunktion für die Voraussagen mit Ihrem neuronalen Netz. Am Ende dieses Schritts verfügen Sie über einen funktionierenden Gebärdensprachen-Klassifizierer.

      Erstellen Sie eine neue Datei namens step_3_train.py:

      Importieren Sie die erforderlichen Dienstprogramme:

      step_3_train.py

      from torch.utils.data import Dataset
      from torch.autograd import Variable
      import torch.nn as nn
      import torch.nn.functional as F
      import torch.optim as optim
      import torch
      
      from step_2_dataset import get_train_test_loaders
      

      Definieren Sie ein neuronales PyTorch-Netz, das drei Convolutional Layers (faltende Schichten) enthält, gefolgt von drei vollständig zusammenhängenden Schichten. Fügen Sie dies am Ende Ihres bestehenden Skripts hinzu:

      step_3_train.py

      class Net(nn.Module):
          def __init__(self):
              super(Net, self).__init__()
              self.conv1 = nn.Conv2d(1, 6, 3)
              self.pool = nn.MaxPool2d(2, 2)
              self.conv2 = nn.Conv2d(6, 6, 3)
              self.conv3 = nn.Conv2d(6, 16, 3)
              self.fc1 = nn.Linear(16 * 5 * 5, 120)
              self.fc2 = nn.Linear(120, 48)
              self.fc3 = nn.Linear(48, 24)
      
          def forward(self, x):
              x = F.relu(self.conv1(x))
              x = self.pool(F.relu(self.conv2(x)))
              x = self.pool(F.relu(self.conv3(x)))
              x = x.view(-1, 16 * 5 * 5)
              x = F.relu(self.fc1(x))
              x = F.relu(self.fc2(x))
              x = self.fc3(x)
              return x
      

      Initialisieren Sie nun das neuronale Netz, definieren Sie eine Verlustfunktion und legen Sie Hyperparameter zur Optimierung fest, indem Sie am Ende des Skripts den folgenden Code hinzufügen:

      step_3_train.py

      def main():
          net = Net().float()
          criterion = nn.CrossEntropyLoss()
          optimizer = optim.SGD(net.parameters(), lr=0.01, momentum=0.9)
      

      Schließlich trainieren Sie es für zwei Epochen:

      step_3_train.py

      def main():
          net = Net().float()
          criterion = nn.CrossEntropyLoss()
          optimizer = optim.SGD(net.parameters(), lr=0.01, momentum=0.9)
      
          trainloader, _ = get_train_test_loaders()
          for epoch in range(2):  # loop over the dataset multiple times
              train(net, criterion, optimizer, trainloader, epoch)
          torch.save(net.state_dict(), "checkpoint.pth")
      

      Sie definieren eine Epoche, die eine Iteration des Trainings ist, bei der jedes Trainings-Sample genau einmal verwendet wurde. Am Ende der Hauptfunktion werden die Modellparameter in einer Datei namens „checkpoint.pth“ gespeichert.

      Fügen Sie am Ende Ihres Skripts den folgenden Code hinzu, um Bild und Label aus dem Datensatz-Loader zu extrahieren und dann jede mit einer PyTorch Variable zu umschließen:

      step_3_train.py

      def train(net, criterion, optimizer, trainloader, epoch):
          running_loss = 0.0
          for i, data in enumerate(trainloader, 0):
              inputs = Variable(data['image'].float())
              labels = Variable(data['label'].long())
              optimizer.zero_grad()
      
              # forward + backward + optimize
              outputs = net(inputs)
              loss = criterion(outputs, labels[:, 0])
              loss.backward()
              optimizer.step()
      
              # print statistics
              running_loss += loss.item()
              if i % 100 == 0:
                  print('[%d, %5d] loss: %.6f' % (epoch, i, running_loss / (i + 1)))
      

      Dieser Code führt auch die Vorwärtsrechnung und dann die Fehlerrückführung über den Verlust und das neuronale Netz aus.

      Fügen Sie am Ende Ihrer Datei Folgendes hinzu, um die Funktion main aufzurufen:

      step_3_train.py

      if __name__ == '__main__':
          main()
      

      Prüfen Sie nochmals, ob Ihre Datei dem Folgenden entspricht:

      step_3_train.py

      from torch.utils.data import Dataset
      from torch.autograd import Variable
      import torch.nn as nn
      import torch.nn.functional as F
      import torch.optim as optim
      import torch
      
      from step_2_dataset import get_train_test_loaders
      
      
      class Net(nn.Module):
          def __init__(self):
              super(Net, self).__init__()
              self.conv1 = nn.Conv2d(1, 6, 3)
              self.pool = nn.MaxPool2d(2, 2)
              self.conv2 = nn.Conv2d(6, 6, 3)
              self.conv3 = nn.Conv2d(6, 16, 3)
              self.fc1 = nn.Linear(16 * 5 * 5, 120)
              self.fc2 = nn.Linear(120, 48)
              self.fc3 = nn.Linear(48, 25)
      
          def forward(self, x):
              x = F.relu(self.conv1(x))
              x = self.pool(F.relu(self.conv2(x)))
              x = self.pool(F.relu(self.conv3(x)))
              x = x.view(-1, 16 * 5 * 5)
              x = F.relu(self.fc1(x))
              x = F.relu(self.fc2(x))
              x = self.fc3(x)
              return x
      
      
      def main():
          net = Net().float()
          criterion = nn.CrossEntropyLoss()
          optimizer = optim.SGD(net.parameters(), lr=0.01, momentum=0.9)
      
          trainloader, _ = get_train_test_loaders()
          for epoch in range(2):  # loop over the dataset multiple times
              train(net, criterion, optimizer, trainloader, epoch)
          torch.save(net.state_dict(), "checkpoint.pth")
      
      
      def train(net, criterion, optimizer, trainloader, epoch):
          running_loss = 0.0
          for i, data in enumerate(trainloader, 0):
              inputs = Variable(data['image'].float())
              labels = Variable(data['label'].long())
              optimizer.zero_grad()
      
              # forward + backward + optimize
              outputs = net(inputs)
              loss = criterion(outputs, labels[:, 0])
              loss.backward()
              optimizer.step()
      
              # print statistics
              running_loss += loss.item()
              if i % 100 == 0:
                  print('[%d, %5d] loss: %.6f' % (epoch, i, running_loss / (i + 1)))
      
      
      if __name__ == '__main__':
          main()
      

      Speichern und schließen Sie sie. Starten Sie dann unser Proof-of-Concept-Training, indem Sie Folgendes ausführen:

      Während das neuronale Netz trainiert wird, sehen Sie ein Ergebnis, das dem Folgenden ähnelt:

      Output

      [0, 0] loss: 3.208171 [0, 100] loss: 3.211070 [0, 200] loss: 3.192235 [0, 300] loss: 2.943867 [0, 400] loss: 2.569440 [0, 500] loss: 2.243283 [0, 600] loss: 1.986425 [0, 700] loss: 1.768090 [0, 800] loss: 1.587308 [1, 0] loss: 0.254097 [1, 100] loss: 0.208116 [1, 200] loss: 0.196270 [1, 300] loss: 0.183676 [1, 400] loss: 0.169824 [1, 500] loss: 0.157704 [1, 600] loss: 0.151408 [1, 700] loss: 0.136470 [1, 800] loss: 0.123326

      Um die Verluste niedrig zu halten, können Sie die Anzahl der Epochen auf 5, 10 oder sogar 20 erhöhen. Nach einer bestimmten Trainingszeit wird sich der Netzverlust trotz einer Steigerung der Trainingszeit nicht mehr verringern. Geben Sie einen Learning Rate Schedule vor, der die Lernrate im Laufe der Zeit verringert, um das Problem zu umgehen, das mit der zunehmenden Trainingszeit einhergeht. Wenn Sie verstehen wollen, warum das funktioniert, sehen Sie sich die Visualisierung von Distill an unter „Why Momentum Really Works“ („Warum das Momentum wirklich funktioniert“).

      Ändern Sie Ihre Funktion main mit den folgenden zwei Zeilen ab, definieren Sie einen Scheduler und rufen Sie scheduler.step auf. Ändern Sie außerdem die Anzahl der Epochen in 12:

      step_3_train.py

      def main():
          net = Net().float()
          criterion = nn.CrossEntropyLoss()
          optimizer = optim.SGD(net.parameters(), lr=0.01, momentum=0.9)
          scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.1)
      
          trainloader, _ = get_train_test_loaders()
          for epoch in range(12):  # loop over the dataset multiple times
              train(net, criterion, optimizer, trainloader, epoch)
              scheduler.step()
          torch.save(net.state_dict(), "checkpoint.pth")
      

      Überprüfen Sie, ob Ihre Datei mit der Datei aus Schritt 3 in diesem Repository übereinstimmt. Die Ausführung des Trainings dauert etwa 5 Minuten. Ihre Ausgabe wird in etwa wie folgt aussehen:

      Output

      [0, 0] loss: 3.208171 [0, 100] loss: 3.211070 [0, 200] loss: 3.192235 [0, 300] loss: 2.943867 [0, 400] loss: 2.569440 [0, 500] loss: 2.243283 [0, 600] loss: 1.986425 [0, 700] loss: 1.768090 [0, 800] loss: 1.587308 ... [11, 0] loss: 0.000302 [11, 100] loss: 0.007548 [11, 200] loss: 0.009005 [11, 300] loss: 0.008193 [11, 400] loss: 0.007694 [11, 500] loss: 0.008509 [11, 600] loss: 0.008039 [11, 700] loss: 0.007524 [11, 800] loss: 0.007608

      Der erhaltene Verlust beträgt 0,007608, was 3 Größenordnungen kleiner als der anfängliche Verlust von 3,20 ist. Damit wird der zweite Schritt unseres Workflows abgeschlossen, in dem wir das neuronale Netz eingerichtet und trainiert haben. Doch auch ein kleiner Verlust hat eine Bedeutung, wenn auch nur eine sehr kleine. Um die Leistung des Modells in Perspektive zu setzen, werden wir seine Genauigkeit berechnen – der Prozentsatz der Bilder, die das Modell richtig klassifiziert hat.

      Schritt 4 – Bewerten des Gebärdensprachen-Classifiers

      Sie werden nun Ihren Gebärdensprachen-Klassifizierer bewerten, indem Sie seine Genauigkeit im Validierungssatz berechnen, ein Satz von Bildern, die das Modell während des Trainings nicht gesehen hat. Dadurch erhalten wir ein besseres Bild über die Leistung des Modells als durch den endgültigen Verlustwert. Zudem fügen Sie Dienstprogramme hinzu, um unser trainiertes Modell am Ende des Trainings zu speichern, und laden unser vorab trainiertes Modell, während die Inferenz durchgeführt wird.

      Erstellen Sie eine neue Datei namens step_4_evaluate.py.

      Importieren Sie die erforderlichen Hilfsfunktionen:

      step_4_evaluate.py

      from torch.utils.data import Dataset
      from torch.autograd import Variable
      import torch.nn as nn
      import torch.nn.functional as F
      import torch.optim as optim
      import torch
      import numpy as np
      
      import onnx
      import onnxruntime as ort
      
      from step_2_dataset import get_train_test_loaders
      from step_3_train import Net
      

      Definieren Sie als Nächstes eine Hilfsfunktion, um die Leistung des neuronalen Netzes zu bewerten. Die folgende Funktion vergleicht den vom neuronalen Netz vorhergesagten Buchstaben mit dem tatsächlichen Buchstaben:

      step_4_evaluate.py

      def evaluate(outputs: Variable, labels: Variable) -> float:
          """Evaluate neural network outputs against non-one-hotted labels."""
          Y = labels.numpy()
          Yhat = np.argmax(outputs, axis=1)
          return float(np.sum(Yhat == Y))
      

      outputs ist eine Liste von Klassenwahrscheinlichkeiten für jedes Sample. Beispielsweise können Outputs für ein einziges Sample [0,1, 0,3, 0,4,] betragen. labels ist eine Liste von Label-Klassen. Die Label-Klasse kann beispielsweise 3 sein.

      Y = ... wandelt die Labels in ein NumPy-Array um. Als Nächstes konvertiert Yhat = np.argmax(...) die Wahrscheinlichkeiten der Klasse outputs in vorausgesagte Klassen. Die Liste der Klassenwahrscheinlichkeiten [0,1, 0,3, 0,4, 0,2] würde die vorausgesagte Klasse 2 ergeben, da der Indexwert 2 von 0,4 der größte Wert ist.

      Da Y und Yhat nun Klassen sind, können Sie sie vergleichen. Yhat == Y prüft, ob die vorhergesagte Klasse mit der Label-Klasse übereinstimmt, und np.sum(...) ist ein Trick, der die Anzahl der truth-y-Werte berechnet. Anders ausgedrückt: np.sum gibt die Anzahl der Samples aus, die richtig klassifiziert wurden.

      Fügen Sie die zweite Funktion batch_evaluate hinzu, die die erste Funktion evaluate auf alle Bilder anwendet:

      step_4_evaluate.py

      def batch_evaluate(
              net: Net,
              dataloader: torch.utils.data.DataLoader) -> float:
          """Evaluate neural network in batches, if dataset is too large."""
          score = n = 0.0
          for batch in dataloader:
              n += len(batch['image'])
              outputs = net(batch['image'])
              if isinstance(outputs, torch.Tensor):
                  outputs = outputs.detach().numpy()
              score += evaluate(outputs, batch['label'][:, 0])
          return score / n
      

      Batch ist eine Gruppe von Bildern, die als ein einzelner Tensor gespeichert werden. Zuerst erhöhen Sie die Gesamtzahl der Bilder, die Sie evaluieren (n) um die Anzahl der Bilder in diesem Batch. Als Nächstes führen Sie in dem neuronalen Netz eine Inferenz mit diesem Batch von Bildern aus, outputs = net(...). Die Typenprüfung if isinstance(...) konvertiert die Ergebnisse in einen NumPy-Array bei Bedarf. Schließlich verwenden Sie evaluate, um die Anzahl der richtig klassifizierten Samples zu berechnen. Am Ende der Funktion berechnen Sie den prozentualen Anteil der Samples, die Sie richtig klassifiziert haben, score / n.

      Fügen Sie abschließend das folgende Skript hinzu, um die vorherigen Hilfsfunktionen zu nutzen:

      step_4_evaluate.py

      def validate():
          trainloader, testloader = get_train_test_loaders()
          net = Net().float()
      
          pretrained_model = torch.load("checkpoint.pth")
          net.load_state_dict(pretrained_model)
      
          print('=' * 10, 'PyTorch', '=' * 10)
          train_acc = batch_evaluate(net, trainloader) * 100.
          print('Training accuracy: %.1f' % train_acc)
          test_acc = batch_evaluate(net, testloader) * 100.
          print('Validation accuracy: %.1f' % test_acc)
      
      
      if __name__ == '__main__':
          validate()
      

      Dadurch wird ein vorab trainiertes neuronales Netz geladen und seine Leistung auf dem bereitgestellten Gebärdensprachen-Datensatz bewertet. Das Skript gibt hier insbesondere Genauigkeit über die Bilder aus, die Sie für das Training verwendet haben, und einen separaten Satz von Bildern, die Sie für Testzwecke aufgehoben haben und die validation set heißen.

      Als Nächstes exportieren Sie das PyTorch in eine ONNX-Binärdatei. Diese Binärdatei kann dann in der Produktion verwendet werden, um mit Ihrem Modell eine Inferenz auszuführen. Am wichtigsten ist, dass der Code, der diese Binärdatei ausführt, keine Kopie der ursprünglichen Netzwerkdefinition benötigt. Fügen Sie am Ende der Funktion validate Folgendes hinzu:

      step_4_evaluate.py

          trainloader, testloader = get_train_test_loaders(1)
      
          # export to onnx
          fname = "signlanguage.onnx"
          dummy = torch.randn(1, 1, 28, 28)
          torch.onnx.export(net, dummy, fname, input_names=['input'])
      
          # check exported model
          model = onnx.load(fname)
          onnx.checker.check_model(model)  # check model is well-formed
      
          # create runnable session with exported model
          ort_session = ort.InferenceSession(fname)
          net = lambda inp: ort_session.run(None, {'input': inp.data.numpy()})[0]
      
          print('=' * 10, 'ONNX', '=' * 10)
          train_acc = batch_evaluate(net, trainloader) * 100.
          print('Training accuracy: %.1f' % train_acc)
          test_acc = batch_evaluate(net, testloader) * 100.
          print('Validation accuracy: %.1f' % test_acc)
      

      Dadurch wird das ONNX-Modell exportiert, das exportierte Modell überprüft und dann eine Inferenz mit dem exportierten Modell ausgeführt. Überprüfen Sie nochmals, ob Ihre Datei mit der Datei aus Schritt 4 in diesem Repository übereinstimmt.

      step_4_evaluate.py

      from torch.utils.data import Dataset
      from torch.autograd import Variable
      import torch.nn as nn
      import torch.nn.functional as F
      import torch.optim as optim
      import torch
      import numpy as np
      
      import onnx
      import onnxruntime as ort
      
      from step_2_dataset import get_train_test_loaders
      from step_3_train import Net
      
      
      def evaluate(outputs: Variable, labels: Variable) -> float:
          """Evaluate neural network outputs against non-one-hotted labels."""
          Y = labels.numpy()
          Yhat = np.argmax(outputs, axis=1)
          return float(np.sum(Yhat == Y))
      
      
      def batch_evaluate(
              net: Net,
              dataloader: torch.utils.data.DataLoader) -> float:
          """Evaluate neural network in batches, if dataset is too large."""
          score = n = 0.0
          for batch in dataloader:
              n += len(batch['image'])
              outputs = net(batch['image'])
              if isinstance(outputs, torch.Tensor):
                  outputs = outputs.detach().numpy()
              score += evaluate(outputs, batch['label'][:, 0])
          return score / n
      
      
      def validate():
          trainloader, testloader = get_train_test_loaders()
          net = Net().float().eval()
      
          pretrained_model = torch.load("checkpoint.pth")
          net.load_state_dict(pretrained_model)
      
          print('=' * 10, 'PyTorch', '=' * 10)
          train_acc = batch_evaluate(net, trainloader) * 100.
          print('Training accuracy: %.1f' % train_acc)
          test_acc = batch_evaluate(net, testloader) * 100.
          print('Validation accuracy: %.1f' % test_acc)
      
          trainloader, testloader = get_train_test_loaders(1)
      
          # export to onnx
          fname = "signlanguage.onnx"
          dummy = torch.randn(1, 1, 28, 28)
          torch.onnx.export(net, dummy, fname, input_names=['input'])
      
          # check exported model
          model = onnx.load(fname)
          onnx.checker.check_model(model)  # check model is well-formed
      
          # create runnable session with exported model
          ort_session = ort.InferenceSession(fname)
          net = lambda inp: ort_session.run(None, {'input': inp.data.numpy()})[0]
      
          print('=' * 10, 'ONNX', '=' * 10)
          train_acc = batch_evaluate(net, trainloader) * 100.
          print('Training accuracy: %.1f' % train_acc)
          test_acc = batch_evaluate(net, testloader) * 100.
          print('Validation accuracy: %.1f' % test_acc)
      
      
      if __name__ == '__main__':
          validate()
      

      Führen Sie Folgendes aus, um den Checkpoint vom letzten Schritt zu verwenden und zu evaluieren:

      • python step_4_evaluate.py

      Dadurch erhalten Sie eine ähnliche Ausgabe wie die Folgende, die nicht nur bestätigt, dass Ihr exportiertes Modell funktioniert, sondern auch Ihr ursprüngliches PyTorch-Modell bestätigt:

      Output

      ========== PyTorch ========== Training accuracy: 99.9 Validation accuracy: 97.4 ========== ONNX ========== Training accuracy: 99.9 Validation accuracy: 97.4

      Ihr neuronales Netz erreicht eine Trainingsgenauigkeit von 99,9 % und eine Validierungsgenauigkeit von 97,4 %. Diese Lücke zwischen Trainings- und Validierungsgenauigkeit deutet auf eine Überanpassung Ihres Modells hin. Das heißt, dass Ihr Modell die Trainingsdaten gespeichert hat, anstatt generalisierbare Muster zu erlernen. Weiterführende Informationen zu den Implikationen und Ursachen der Überanpassung finden Sie in Understanding Bias-Variance Tradeoffs (Das Spannungsfeld von Verzerrungsvarianzen verstehen).

      Nun haben wir einen Gebärdensprachen-Klassifizierer fertiggestellt. Im Grunde genommen kann unser Modell die Gebärden fast immer korrekt erkennen und voneinander unterscheiden. Da dies bereits ein recht gutes Modell ist, fahren wir nun mit der letzten Stufe unserer Anwendung fort. Wir werden diesen Gebärdensprachen-Classifier in einer Webcam-Anwendung in Echtzeit einsetzen.

      Schritt 5 – Verknüpfen der Kameraaufzeichnungen

      Ihr nächstes Ziel besteht darin, die Kamera des Computers mit Ihrem Gebärdensprachen-Klassifizierer zu verknüpfen. Sie erfassen die Eingangsdaten der Kamera, klassifizieren die angezeigte Gebärdensprache und melden dann die klassifizierte Gebärde an den Benutzer zurück.

      Erstellen Sie nun ein Python-Skript für die Gesichtserkennung. Erstellen Sie die Datei step_6_camera.py mit nano oder Ihrem bevorzugten Texteditor:

      Fügen Sie in der Datei den folgenden Code hinzu:

      step_5_camera.py

      """Test for sign language classification"""
      import cv2
      import numpy as np
      import onnxruntime as ort
      
      def main():
          pass
      
      if __name__ == '__main__':
          main()
      

      Dieser Code importiert OpenCV, die Ihre Bildprogramme enthält, und die ONNX-Laufzeit. Das ist alles, was Sie mit Ihrem Modell in der Inferenz ausführen müssen. Der Rest des Codes ist eine typische Python-Programmvorgabe.

      Ersetzen Sie nun pass in der Funktion main durch den folgenden Code, der einen Gebärdensprachen-Classifier mit den zuvor trainierten Parametern initialisiert. Fügen Sie zudem ein Mapping zwischen den Indizes und den Buchstaben sowie den Bildstatistiken hinzu:

      step_5_camera.py

      def main():
          # constants
          index_to_letter = list('ABCDEFGHIKLMNOPQRSTUVWXY')
          mean = 0.485 * 255.
          std = 0.229 * 255.
      
          # create runnable session with exported model
          ort_session = ort.InferenceSession("signlanguage.onnx")
      

      Sie werden Elemente dieses Testskripts aus der offiziellen OpenCV verwenden. Aktualisieren Sie insbesondere den Körper der Funktion main. Zuerst initialisieren Sie ein VideoCapture-Objekt, das so eingestellt ist, dass es Live-Einspeisungen von der Kamera Ihres Computers erfassen kann. Platzieren Sie das ans Ende der Funktion main:

      step_5_camera.py

      def main():
          ...
          # create runnable session with exported model
          ort_session = ort.InferenceSession("signlanguage.onnx")
      
          cap = cv2.VideoCapture(0)
      

      Fügen Sie dann eine while-Schleife hinzu, die bei jedem Zeitschritt von der Kamera liest:

      step_5_camera.py

      def main():
          ...
          cap = cv2.VideoCapture(0)
          while True:
              # Capture frame-by-frame
              ret, frame = cap.read()
      

      Schreiben Sie eine Hilfsfunktion, die das zentrale Cropping für das Kamerabild übernimmt. Platzieren Sie diese Funktion vor main:

      step_5_camera.py

      def center_crop(frame):
          h, w, _ = frame.shape
          start = abs(h - w) // 2
          if h > w:
              frame = frame[start: start + w]
          else:
              frame = frame[:, start: start + h]
          return frame
      

      Führen Sie als Nächstes das zentrale Cropping für das Kamerabild durch, konvertieren Sie es in Graustufen, normalisieren Sie es and ändern Sie die Größe auf 28x28. Platzieren Sie dies in die while-Schleife innerhalb der Funktion main:

      step_5_camera.py

      def main():
          ...
          while True:
              # Capture frame-by-frame
              ret, frame = cap.read()
      
              # preprocess data
              frame = center_crop(frame)
              frame = cv2.cvtColor(frame, cv2.COLOR_RGB2GRAY)
              x = cv2.resize(frame, (28, 28))
              x = (frame - mean) / std
      

      Führen Sie noch innerhalb der while-Schleife eine Inferenz mit der ONNX-Laufzeit aus. Konvertieren Sie die Ausgaben in einen Klassenindex und dann in einen Buchstaben:

      step_5_camera.py

              ...
              x = (frame - mean) / std
      
              x = x.reshape(1, 1, 28, 28).astype(np.float32)
              y = ort_session.run(None, {'input': x})[0]
      
              index = np.argmax(y, axis=1)
              letter = index_to_letter[int(index)]
      

      Zeigen Sie den vorhergesagten Buchstaben innerhalb des Bildrahmens und das Bild wieder dem Benutzer an:

      step_5_camera.py

              ...
              letter = index_to_letter[int(index)]
      
              cv2.putText(frame, letter, (100, 100), cv2.FONT_HERSHEY_SIMPLEX, 2.0, (0, 255, 0), thickness=2)
              cv2.imshow("Sign Language Translator", frame)
      

      Fügen Sie am Ende der while-Schleife diesen Code hinzu, um zu prüfen, ob der Benutzer das Zeichen q wählt und falls das der Fall ist, beenden Sie die Anwendung. Diese Zeile hält das Programm für 1 Millisekunde an. Fügen Sie Folgendes hinzu:

      step_5_camera.py

              ...
              cv2.imshow("Sign Language Translator", frame)
      
              if cv2.waitKey(1) & 0xFF == ord('q'):
                  break
      

      Lösen Sie abschließend die Aufnahme aus und schließen Sie alle Fenster. Platzieren Sie sie außerhalb der while-Schleife, um die Funktion main zu beenden.

      step_5_camera.py

      ...
      
          while True:
              ...
              if cv2.waitKey(1) & 0xFF == ord('q'):
                  break
      
      
          cap.release()
          cv2.destroyAllWindows()
      

      Überprüfen Sie nochmals, ob Ihre Datei mit dem folgenden oder diesem Repository übereinstimmt:

      step_5_camera.py

      import cv2
      import numpy as np
      import onnxruntime as ort
      
      
      def center_crop(frame):
          h, w, _ = frame.shape
          start = abs(h - w) // 2
          if h > w:
              return frame[start: start + w]
          return frame[:, start: start + h]
      
      
      def main():
          # constants
          index_to_letter = list('ABCDEFGHIKLMNOPQRSTUVWXY')
          mean = 0.485 * 255.
          std = 0.229 * 255.
      
          # create runnable session with exported model
          ort_session = ort.InferenceSession("signlanguage.onnx")
      
          cap = cv2.VideoCapture(0)
          while True:
              # Capture frame-by-frame
              ret, frame = cap.read()
      
              # preprocess data
              frame = center_crop(frame)
              frame = cv2.cvtColor(frame, cv2.COLOR_RGB2GRAY)
              x = cv2.resize(frame, (28, 28))
              x = (x - mean) / std
      
              x = x.reshape(1, 1, 28, 28).astype(np.float32)
              y = ort_session.run(None, {'input': x})[0]
      
              index = np.argmax(y, axis=1)
              letter = index_to_letter[int(index)]
      
              cv2.putText(frame, letter, (100, 100), cv2.FONT_HERSHEY_SIMPLEX, 2.0, (0, 255, 0), thickness=2)
              cv2.imshow("Sign Language Translator", frame)
      
              if cv2.waitKey(1) & 0xFF == ord('q'):
                  break
      
          cap.release()
          cv2.destroyAllWindows()
      
      if __name__ == '__main__':
          main()
      

      Schließen Sie Ihre Datei und führen Sie das Skript aus.

      Sobald das Skript ausgeführt wird, wird ein Fenster mit der Live-Einspeisung der Webcam angezeigt. Der vorhergesagte Gebärdensprachenbuchstabe wird oben links angezeigt. Halten Sie die Hand hoch und machen Sie Ihre Lieblingsgebärde, um Ihren Classifier in Aktion zu sehen. Hier sind einige Beispiel-Ergebnisse, die den Buchstaben L und D zeigen.

      Screenshot Ihres Sample-OpenCV-Programms für den Fingerbuchstaben ‚L‘.
       Screenshot Ihres Sample-OpenCV-Programms für den Fingerbuchstaben ,D‘.

      Beachten Sie während des Tests, dass der Hintergrund für diesen Übersetzer ziemlich klar sein muss, damit er funktioniert. Dies bringt die Sauberkeit des Datensatzes leider mit sich. Würde der Datensatz Bilder von Handzeichen mit verschiedenen Hintergründen enthalten, hätte das Netz kein Problem mit rauschenden Hintergründen. Der Datensatz bietet jedoch leere Hintergründe und ziemlich zentrierte Hände. Die Webcam funktioniert daher am besten, wenn die Hand ebenfalls zentriert ist und sich vor einem leeren Hintergrund befindet.

      Damit ist die Übersetzungsanwendung für die Gebärdensprache vollendet.

      Zusammenfassung

      In diesem Tutorial haben Sie einen Übersetzer für die amerikanische Gebärdensprache mittels Computer Vision und einem Modell für maschinelles Lernen entwickelt. Sie haben vor allem neue Aspekte des Trainings eines Modells für maschinelles Lernen gesehen – genauer gesagt Data Augmentation für die Modellstabilität, Learning Rate Schedules für geringere Verluste und Exportvorgänge von KI-Modellen mithilfe von ONNX zu Produktionszwecken. Dies führte schlussendlich zu der Schaffung einer Echtzeit-Computer-Vision-Anwendung, die Gebärdensprache mithilfe einer Pipeline, die Sie erstellt haben, in Buchstaben übersetzt. Es sollte allerdings bedacht werden, dass der finale Klassifizierer instabil ist. Glücklicherweise gibt es jedoch Methoden, die man einzeln oder auch zusammen einsetzen kann, um gegen diese Instabilität vorzugehen. Diese stellen wir nachfolgend vor. Die folgenden Themen beinhalten weiterführende Erläuterungen zu Verbesserung Ihrer Anwendung:

      • Generalisierung: Hierbei handelt es sich keineswegs um einen Unterbereich von Computer Vision, sondern um ein beständiges Problem, das im Grunde bei jedem Aspekt zum Thema maschinelles Lernen auftritt. Siehe Understanding Bias-Variance Tradeoffs​​​ (Das Spannungsfeld von Verzerrungsvarianzen verstehen)​​​
      • Domain Adaptation (Domänenanpassung): Angenommen, Ihr Modell ist für Domäne A trainiert (z. B. sonnige Umgebungen). Können Sie das Modell in diesem Fall auf Domäne B umstellen (z. B. wolkige Umgebungen)?
      • Adversarial Examples (gegnerische Beispiele): Angenommen ein Kontrahent erstellt absichtlich Bilder, um Ihr Modell zu täuschen. Wie können Sie solche Bilder gestalten? Wie können Sie gegen solche Bilder vorgehen?



      Source link

      So testen Sie ein Node.js-Modul mit Mocha und Assert


      Die Autorin wählte den Open Internet/Free Speech Fund, um eine Spende im Rahmen des Programms Write for DOnations zu erhalten.

      Einführung

      Testen ist ein integraler Bestandteil der Softwareentwicklung. Es ist üblich, dass Programmierer Code ausführen, der ihre Anwendung testet, während sie darin Änderungen vornehmen. So können sie bestätigen, dass sich die Anwendung so verhält, wie sie es gerne hätten. Mit dem richtigen Test-Setup kann dieser Prozess sogar automatisiert sein und somit eine Menge Zeit sparen. Das Ausführen von Tests nach dem Schreiben von neuem Code stellt sicher, dass neue Änderungen keine bereits vorhandenen Funktionen brechen. Das gibt dem Entwickler Vertrauen in seine Code-Basis, insbesondere dann, wenn der Code produktiv eingesetzt wird, damit die Benutzer mit ihm interagieren können.

      Ein Test-Framework strukturiert die Art, wie wir Testfälle erstellen. Mocha ist ein beliebtes JavaScript-Framework, das unsere Testfälle organisiert und für uns ausführt. Mocha verifiziert jedoch nicht das Verhalten unseres Codes. Um Werte in einem Test zu vergleichen, können wir das Node.js-assert-Modul verwenden.

      In diesem Artikel schreiben Sie Tests für ein Node.js-TODO-Listenmodul. Sie richten das Testframework von Mocha ein und nutzen es, um Ihre Tests zu strukturieren. Dann verwenden Sie das Node.js-assert-Modul, um die Tests selbst zu erstellen. In diesem Sinne verwenden Sie Mocha als Planersteller und assert zur Umsetzung des Plans.

      Voraussetzungen

      Schritt 1 — Schreiben eines Node-Moduls

      Beginnen wir diesen Artikel mit dem Schreiben des Node.js-Moduls, das wir testen möchten. Dieses Modul verwaltet eine Liste von TODO-Elementen. Mithilfe dieses Moduls können wir alle TODOs auflisten, die wir verfolgen, sowie neue Elemente hinzufügen und einige als abgeschlossen markieren. Zusätzlich können wir eine Liste von TODO-Elementen in eine CSV-Datei exportieren. Wenn Sie eine Auffrischung über das Schreiben von Node.js-Modulen wünschen, können Sie unseren Artikel Erstellen eines Node.js-Moduls lesen.

      Zuerst müssen wir die Codierungsumgebung einrichten. Erstellen Sie einen Ordner mit dem Namen Ihres Projekts in Ihrem Terminal. Dieses Tutorial verwendet den Namen todos:

      Gehen Sie dann in diesen Ordner hinein:

      Initialisieren Sie nun npm, da wir später zum Ausführen der Tests seine CLI-Funktionalität verwenden:

      Wir haben nur eine Abhängigkeit, Mocha, das wir zur Organisation und Durchführung unserer Tests verwenden. Zum Herunterladen und Installieren von Mocha verwenden Sie Folgendes:

      • npm i request --save-dev mocha

      Wir installieren Mocha als dev-Abhängigkeit, da es für das Modul in einer Produktionsumgebung nicht erforderlich ist. Wenn Sie mehr über Node.js-Packages oder npm erfahren möchten, lesen Sie den Leitfaden Verwenden von Node.js-Modulen mit npm und package.json.

      Abschließend erstellen wir unsere Datei, die den Code unseres Moduls enthalten wird:

      Damit können wir nun unser Modul erstellen. Öffnen Sie index.js in einem Texteditor wie nano:

      Zuerst definieren wir die Todos-Klasse. Diese Klasse enthält alle Funktionen, die wir zur Verwaltung unserer TODO-Liste benötigen. Fügen Sie index.js die folgenden Zeilen von Code hinzu:

      todos/index.js

      class Todos {
          constructor() {
              this.todos = [];
          }
      }
      
      module.exports = Todos;
      

      Wir beginnen mit der Datei, indem wir eine Todos-Klasse erstellen. Seine Funktion constructor() nimmt keine Argumente an, daher müssen wir keine Werte bereitstellen, um ein Objekt für diese Klasse zu instanziieren. Wenn wir ein Todos-Objekt initialisieren, erstellen wir lediglich eine todos-Funktion, bei der es sich um ein leeres Array handelt.

      Die modules-Zeile ermöglicht es anderen Node.js-Modulen, unsere Todos-Klasse zu verlangen. Wenn wir sie nicht ausdrücklich exportieren, könnte die Testdatei, die wir später erstellen, sie nicht verwenden.

      Wir fügen nun eine Funktion hinzu, um das Array von todos, das wir gespeichert haben, auszugeben. Fügen Sie die folgenden hervorgehobenen Zeilen ein:

      todos/index.js

      class Todos {
          constructor() {
              this.todos = [];
          }
      
          list() {
              return [...this.todos];
          }
      }
      
      module.exports = Todos;
      

      Unsere list()-Funktion gibt eine Kopie des Arrays aus, die von der Klasse verwendet wird. Sie erstellt die Kopie des Arrays mit der destrukturierten Syntax von JavaScript. Wir erstellen eine Kopie des Arrays, damit die Änderungen, die der Benutzer an dem von list() ausgegebenen Array vornimmt, nicht das vom Todos-Objekt verwendete Array beeinträchtigen.

      Anmerkung: JavaScript-Arrays sind Referenztypen. Das bedeutet, dass sich JavaScript bei jeder Variablenzuweisung an ein Array oder bei Funktionsaufrufen mit einem Array als Parameter auf das ursprüngliche Array bezieht, das erstellt wurde. Wenn wir zum Beispiel ein Array mit drei Elementen namens x haben und eine neue Variable y erstellen, sodass y = x, y und x sich beide auf dieselbe Sache beziehen. Alle Änderungen, die wir im Array an y vornehmen, wirken sich auf die Variable x aus und umgekehrt.

      Schreiben wir nun die add()-Funktion, die ein neues TODO-Element hinzufügt:

      todos/index.js

      class Todos {
          constructor() {
              this.todos = [];
          }
      
          list() {
              return [...this.todos];
          }
      
          add(title) {
              let todo = {
                  title: title,
                  completed: false,
              }
      
              this.todos.push(todo);
          }
      }
      
      module.exports = Todos;
      

      Unsere add()-Funktion nimmt eine Zeichenfolge und platziert sie bei einem neuen JavaScript-Object in die title-Funktion. Das neue Objekt hat auch eine completed-Funktion, die standardmäßig auf false gesetzt ist. Dann fügen wir unser neues Objekt unserem Array von TODOs hinzu.

      Eine wichtige Funktionalität in einem TODO-Manager ist die Markierung von Elementen als abgeschlossen. Um dies umzusetzen, durchlaufen wir unser todos-Array, um das TODO-Element zu finden, nach dem der Benutzer sucht. Wenn eines gefunden wird, markieren wir es als abgeschlossen. Wenn keines gefunden wird, geben wir einen Fehler aus.

      Fügen Sie die complete()-Funktion wie hier gezeigt hinzu:

      todos/index.js

      class Todos {
          constructor() {
              this.todos = [];
          }
      
          list() {
              return [...this.todos];
          }
      
          add(title) {
              let todo = {
                  title: title,
                  completed: false,
              }
      
              this.todos.push(todo);
          }
      
          complete(title) {
              let todoFound = false;
              this.todos.forEach((todo) => {
                  if (todo.title === title) {
                      todo.completed = true;
                      todoFound = true;
                      return;
                  }
              });
      
              if (!todoFound) {
                  throw new Error(`No TODO was found with the title: "${title}"`);
              }
          }
      }
      
      module.exports = Todos;
      

      Speichern Sie die Datei und beenden Sie den Texteditor.

      Wir verfügen nun über einen grundlegenden TODO-Manager, mit dem wir experimentieren können. Als Nächstes testen wir den Code manuell, um zu sehen, ob die Anwendung funktioniert.

      Schritt 2 — Manuelles Testen des Codes

      In diesem Schritt führen wir die Funktionen unseres Codes aus und beobachten die Ausgabe, um sicherzustellen, dass sie unseren Erwartungen entspricht. Das nennt sich manuelles Testen. Es ist wahrscheinlich die gebräuchlichste Testmethodik, die Programmierer anwenden. Obwohl wir unsere Tests später mit Mocha automatisieren, testen wir unseren Code zunächst manuell, um ein besseres Gefühl dafür zu bekommen, wie sich manuelles Testen von Test-Frameworks unterscheidet.

      Wir fügen unserer Anwendung zwei TODO-Elemente hinzu und markieren eines als abgeschlossen. Starten Sie die Node.js REPL in dem gleichen Ordner wie die Datei index.js:

      Sie sehen die Eingabeaufforderung > in der REPL, die uns anzeigt, dass wir JavaScript-Code eingeben können. Geben Sie Folgendes an der Eingabeaufforderung ein:

      • const Todos = require('./index');

      Mit require() laden wir das TODOs-Modul in eine Todos-Variable. Erinnern Sie sich, dass unser Modul die Todos-Klasse standardmäßig ausgibt.

      Lassen Sie uns nun ein Objekt für diese Klasse instanziieren. Fügen Sie diese Zeile von Code in der REPL hinzu:

      • const todos = new Todos();

      Wir können das todos-Objekt verwenden, um unsere Umsetzungsarbeiten zu verifizieren. Fügen wir unser erstes TODO-Element hinzu:

      Bisher haben wir noch keine Ausgabe in unserem Terminal gesehen. Verifizieren wir, dass wir unser "run code"-TODO-Element gespeichert haben, indem wir eine Liste aller unserer TODOs abrufen:

      Sie sehen diese Ausgabe in Ihrer REPL:

      Output

      [ { title: 'run code', completed: false } ]

      Das ist das erwartete Ergebnis: Wir haben ein TODO-Element in unserem Array von TODOs und es ist nicht standardmäßig abgeschlossen.

      Wir fügen ein weiteres TODO-Element hinzu:

      • todos.add("test everything");

      Markieren Sie das erste TODO-Element als abgeschlossen:

      • todos.complete("run code");

      Unser todos-Objekt verwaltet nun zwei Elemente: "run code" und "test everything". Das "run code"-TODO wird ebenfalls abgeschlossen sein. Bestätigen wir dies, indem wir list() erneut aufrufen:

      Die REPL wird Folgendes ausgeben:

      Output

      [ { title: 'run code', completed: true }, { title: 'test everything', completed: false } ]

      Beenden Sie nun die REPL wie folgt:

      Wir haben bestätigt, dass sich unser Modul erwartungsgemäß verhält. Wir haben unseren Code nicht in eine Testdatei gestellt oder eine Testbibliothek verwendet, sondern manuell getestet. Leider ist diese Testform zeitaufwendig, wenn wir sie bei jeder Änderung, die wir vornehmen, durchführen. Als Nächstes verwenden wir automatisiertes Testen in Node.js und sehen, ob wir dieses Problem mit dem Mocha lösen können.

      Schritt 3 — Schreiben des ersten Tests mit Mocha und Assert

      Im letzten Schritt haben wir unsere Anwendung manuell getestet. Das funktioniert für individuelle Anwendungsfälle, aber wenn unser Modul skaliert, wird diese Methode weniger praktikabel. Wenn wir neue Eigenschaften testen, müssen wir sicher sein, dass die hinzugefügte Funktionalität keine Probleme in der alten Funktionalität verursacht. Wir möchten jede Eigenschaft bei jeder Änderung des Codes erneut testen, aber dies von Hand zu tun wäre sehr aufwendig und fehleranfällig.

      Eine effizientere Praxis wäre die Einrichtung automatisierter Tests. Das sind skriptbasierte Tests, die wie jeder andere Codeblock geschrieben sind. Wir führen unsere Funktionen mit definierten Eingaben aus und inspizieren ihre Effekte, um sicherzustellen, dass sie sich wie erwartet verhalten. Mit dem Anwachsen unserer Codebasis wächst auch der Umfang der automatisierten Tests. Wenn wir neue Tests zusammen mit den Eigenschaften schreiben, können wir überprüfen, ob das gesamte Modul noch funktioniert – ohne sich jedesmal daran erinnern zu müssen, wie jede einzelne Funktion genutzt wird.

      In diesem Tutorial verwenden wir das Testframework von Mocha mit dem Node.js-assert-Modul. Wir sammeln ein paar praktische Erfahrungen, um zu sehen, wie sie zusammenarbeiten.

      Erstellen Sie zunächst eine neue Datei, um unseren Testcode zu speichern:

      Verwenden Sie nun Ihren bevorzugten Texteditor, um die Testdatei zu öffnen. Sie können wie zuvor nano verwenden:

      In der ersten Zeile der Textdatei laden wir das TODOs-Modul, so wie wir es mit der Node.js-Shell getan haben. Dann laden wir das assert-Modul für das Schreiben unserer Tests. Fügen Sie die folgenden Zeilen hinzu:

      todos/index.test.js

      const Todos = require('./index');
      const assert = require('assert').strict;
      

      Die strict-Funktion des assert-Moduls erlaubt uns, spezielle Gleichheitstests anzuwenden, die von Node.js empfohlen werden und gut für das zukünftige Prüfen geeignet sind, da sie mehr Anwendungsfälle berücksichtigen.

      Bevor wir mit dem Schreiben von Tests beginnen, behandeln wir, wie Mocha unseren Code organisiert. Die in Mocha strukturierten Tests folgen normalerweise dieser Vorlage:

      describe([String with Test Group Name], function() {
          it([String with Test Name], function() {
              [Test Code]
          });
      });
      

      Beachten Sie zwei Schlüsselfunktionen: describe() und it(). Die describe()-Funktion wird zur Gruppierung ähnlicher Tests genutzt. Es ist nicht erforderlich, für Mocha Tests auszuführen, aber die Gruppierung von Tests erleichtert die Pflege unseres Testcodes. Es wird empfohlen, Ihre Tests so zu gruppieren, dass Sie ähnliche Tests leicht zusammen aktualisieren können.

      Die Funktion it() enthält unseren Testcode. Hier würden wir mit den Funktionen unseres Moduls interagieren und die assert-Bibliothek verwenden. Viele it()-Funktionen können als describe()-Funktion definiert werden.

      Unser Ziel in diesem Abschnitt ist die Verwendung von Mocha und assert zur Automatisierung unserer manuellen Tests. Wir führen dies Schritt für Schritt aus und beginnen mit unserem Beschreibungsblock. Fügen Sie Folgendes nach den Modulzeilen in Ihre Datei ein:

      todos/index.test.js

      ...
      describe("integration test", function() {
      });
      

      Mit diesem Codeblock haben wir eine Gruppierung für unsere integrierten Tests erstellt. Komponententests würden jeweils nur eine Funktion testen. Integrationstests verifizieren, wie gut Funktionen innerhalb oder über Module zusammenarbeiten. Wenn Mocha unseren Test ausführt, laufen alle Tests innerhalb des Beschreibungsblocks unter der "integration test"-Gruppe.

      Wir fügen nun eine it()-Funktion hinzu, damit wir mit dem Testen des Codes unseres Moduls beginnen können:

      todos/index.test.js

      ...
      describe("integration test", function() {
          it("should be able to add and complete TODOs", function() {
          });
      });
      

      Beachten Sie, wie deskriptiv wir den Namen des Tests gemacht haben. Wenn jemand unseren Test ausführt, wird sofort klar, was passiert oder fehlgeschlagen ist. Eine gut getestete Anwendung ist typischerweise eine gut dokumentierte Anwendung und Tests können manchmal eine effektive Art der Dokumentation sein.

      Für unseren ersten Test erstellen wir ein neues Todos-Objekt und verifizieren, dass es keine Elemente enthält:

      todos/index.test.js

      ...
      describe("integration test", function() {
          it("should be able to add and complete TODOs", function() {
              let todos = new Todos();
              assert.notStrictEqual(todos.list().length, 1);
          });
      });
      

      Die erste neue Zeile des Codes instanziierte ein neues Todos-Objekt, wie wir es in der Node.js-REPL oder einem anderen Modul tun würden. In der zweiten neuen Zeile verwenden wir das assert-Modul.

      Aus dem assert-Modul verwenden wir die notStrictEqual()-Methode. Diese Funktion nimmt zwei Parameter: den Wert, den wir testen möchten (genannt actual-Wert) und den Wert, den wir erhalten möchten (genannt expected-Wert). Wenn beide Argumente gleich sind, gibt notStrictEqual() einen Fehler aus, damit der Test fehlschlägt.

      Speichern und beenden Sie index.test.js.

      Der Basisfall wird wahr sein, da die Länge 0 sein sollte, was nicht 1 ist. Bestätigen wir das, indem wir Mocha ausführen. Dazu müssen wir unsere package.json-Datei ändern. Öffnen Sie Ihre package.json-Datein mit Ihrem Texteditor:

      Ändern Sie diese nun in der scripts-Funktion so wie hier gezeigt:

      todos/package.json

      ...
      "scripts": {
          "test": "mocha index.test.js"
      },
      ...
      

      Wir haben nun das Verhalten des npm-CLI-Befehls test geändert. Wenn wir npm test ausführen, überprüft npm den gerade eingegebenen Befehl in package.json. Die Ausführung sucht nach der Mocha-Bibliothek in unserem node_modules-Ordner und führt den mocha-Befehl mit unserer Testdatei aus.

      Speichern und beenden Sie package.json.

      Nun sehen wir uns an, was passiert, wenn wir unseren Test ausführen. Geben Sie Folgendes in Ihr Terminal ein:

      Der Befehl erzeugt die folgende Ausgabe:

      Output

      > todos@1.0.0 test your_file_path/todos > mocha index.test.js integrated test ✓ should be able to add and complete TODOs 1 passing (16ms)

      Diese Ausgabe zeigt uns zunächst, welche Testgruppe sie nun ausführen wird. Für jeden einzelnen Test innerhalb einer Gruppe ist der Testfall einbezogen. Wir sehen unseren Testnamen wie in der Funktion it() beschrieben. Das Häkchen auf der linken Seite des Testfalls zeigt an, dass der Test bestanden ist.

      Am Ende erhalten wir eine Zusammenfassung aller Tests. In unserem Fall ist unser einzelner Test bestanden und wurde in 16 ms abgeschlossen (die Zeit variiert von Computer zu Computer).

      Unsere Testung hat erfolgreich begonnen. Der aktuelle Testfall kann jedoch falsch-positive Meldungen liefern. Ein falsch-positiver Testfall ist ein Testfall, der bestanden wird, wenn er fehlschlagen sollte.

      Wir überprüfen gerade, dass die Länge des Arrays nicht gleich 1 ist. Wir werden den Test nun so ändern, dass dieser Zustand auch dann zutrifft, wenn er es nicht sollte. Fügen Sie der index.test.js folgende Zeilen hinzu:

      todos/index.test.js

      ...
      describe("integration test", function() {
          it("should be able to add and complete TODOs", function() {
              let todos = new Todos();
              todos.add("get up from bed");
              todos.add("make up bed");
              assert.notStrictEqual(todos.list().length, 1);
          });
      });
      

      Speichern und schließen Sie die Datei.

      Wir haben zwei TODO-Elemente hinzugefügt. Wir führen nun den Test aus, um zu sehen, was passiert:

      Dadurch ergibt sich Folgendes:

      Output

      ... integrated test ✓ should be able to add and complete TODOs 1 passing (8ms)

      Wie ewartet besteht der Test, da die Länge größer als 1 ist. Jedoch wird der ursprüngliche Zweck des ersten Tests verfehlt. Der erste Test ist dazu gedacht, zu bestätigen, dass wir mit einem Leerzustand beginnen. Ein besserer Test bestätigt dies in allen Fällen.

      Wir ändern nun den Test, damit er nur dann bestanden wird, wenn wir absolut keine TODOs im Speicher haben. Führen Sie die folgenden Änderungen in der index.test.js aus:

      todos/index.test.js

      ...
      describe("integration test", function() {
          it("should be able to add and complete TODOs", function() {
              let todos = new Todos();
              todos.add("get up from bed");
              todos.add("make up bed");
              assert.strictEqual(todos.list().length, 0);
          });
      });
      

      Sie haben notStrictEqual() auf strictEqual() geändert, eine Funktion, die die Gleichheit zwischen dem tatsächlichen und erwarteten Argument überprüft. Die Strict-Equal-Funktion schlägt fehl, wenn unsere Argumente nicht genau gleich sind.

      Speichern und beenden Sie und führen Sie dann den Test aus, damit wir sehen können, was passiert:

      Dieses Mal zeigt die Ausgabe einen Fehler:

      Output

      ... integration test 1) should be able to add and complete TODOs 0 passing (16ms) 1 failing 1) integration test should be able to add and complete TODOs: AssertionError [ERR_ASSERTION]: Input A expected to strictly equal input B: + expected - actual - 2 + 0 + expected - actual -2 +0 at Context.<anonymous> (index.test.js:9:10) npm ERR! Test failed. See above for more details.

      Dieser Text wird uns helfen, herauszufinden, warum der Test fehlgeschlagen ist. Beachten Sie, dass zu Beginn des Testfalls kein Häkchen vorhanden ist, da der Test fehlgeschlagen ist.

      Unsere Testzusammenfassung befindet sich nicht mehr am Ende der Ausgabe, sondern direkt nach der Anzeige unserer Liste von Testfällen:

      ...
      0 passing (29ms)
        1 failing
      ...
      

      Die verbleibende Ausgabe gibt uns Daten über unsere fehlgeschlagenen Tests. Zuerst sehen wir, welcher Testfall fehlgeschlagen ist:

      ...
      1) integrated test
             should be able to add and complete TODOs:
      ...
      

      Dann sehen wir, warum unser Test fehlgeschlagen ist:

      ...
            AssertionError [ERR_ASSERTION]: Input A expected to strictly equal input B:
      + expected - actual
      
      - 2
      + 0
            + expected - actual
      
            -2
            +0
      
            at Context.<anonymous> (index.test.js:9:10)
      ...
      

      Es wird ein AssertionError gemeldet, wenn strictEqual() fehlschlägt. Wir sehen, dass der expected-Wert, 0, vom actual-Wert, 2, abweicht.

      Dann sehen wir die Zeile in unserer Testdatei, in der der Code fehlschlägt. In diesem Fall ist es Zeile 10.

      Nun haben wir selbst gesehen, dass unser Test fehlschlägt, wenn wir fehlerhafte Werte erwarten. Wir ändern unseren Testfall wieder auf seinen richtigen Wert. Öffnen Sie die Datei:

      Dann nehmen Sie die todos.add-Zeilen heraus, sodass Ihr Code wie folgt aussieht:

      todos/index.test.js

      ...
      describe("integration test", function () {
          it("should be able to add and complete TODOs", function () {
              let todos = new Todos();
              assert.strictEqual(todos.list().length, 0);
          });
      });
      

      Speichern und schließen Sie die Datei.

      Führen Sie ihn erneut aus, um zu bestätigen, dass er ohne potenzielle falsch-positive Meldungen besteht:

      Sie erhalten folgende Ausgabe:

      Output

      ... integration test ✓ should be able to add and complete TODOs 1 passing (15ms)

      Wir haben nun die Belastbarkeit unseres Tests deutlich verbessert. Fahren wir mit unserem Integrationstest fort. Der nächste Schritt ist das Hinzufügen eines neuen TODO-Elements in index.test.js:

      todos/index.test.js

      ...
      describe("integration test", function() {
          it("should be able to add and complete TODOs", function() {
              let todos = new Todos();
              assert.strictEqual(todos.list().length, 0);
      
              todos.add("run code");
              assert.strictEqual(todos.list().length, 1);
              assert.deepStrictEqual(todos.list(), [{title: "run code", completed: false}]);
          });
      });
      

      Nach der Verwendung der add()-Funktion bestätigen wir, dass wir nun ein TODO haben, das von unserem todos-Objekt mit strictEqual() verwaltet wird. Unser nächster Test bestätigt die Daten in den todos mit deepStrictEqual(). Die Funktion deepStrictEqual() prüft rekursiv, ob unsere erwarteten und tatsächlichen Objekte die gleichen Eigenschaften haben. In diesem Fall testet sie, dass die von uns erwarteten Arrays beide ein JavaScript-Objekt beinhalten. Dann überprüft sie, dass ihre JavaScript-Objekte die gleichen Eigenschaften haben, d. h., ihre beiden title-Eigenschaften "run code" und ihre beiden completed-Eigenschaften false sind.

      Dann schließen wir die restlichen Tests unter Verwendung dieser beiden Gleichheitsprüfungen nach Bedarf durch Hinzufügen der folgenden hervorgehobenen Zeilen ab:

      todos/index.test.js

      ...
      describe("integration test", function() {
          it("should be able to add and complete TODOs", function() {
              let todos = new Todos();
              assert.strictEqual(todos.list().length, 0);
      
              todos.add("run code");
              assert.strictEqual(todos.list().length, 1);
              assert.deepStrictEqual(todos.list(), [{title: "run code", completed: false}]);
      
              todos.add("test everything");
              assert.strictEqual(todos.list().length, 2);
              assert.deepStrictEqual(todos.list(),
                  [
                      { title: "run code", completed: false },
                      { title: "test everything", completed: false }
                  ]
              );
      
              todos.complete("run code");
              assert.deepStrictEqual(todos.list(),
                  [
                      { title: "run code", completed: true },
                      { title: "test everything", completed: false }
                  ]
          );
        });
      });
      

      Speichern und schließen Sie die Datei.

      Unser Test imitiert nun unseren manuellen Test. Mit diesen programmatischen Tests müssen wir die Ausgabe nicht kontinuierlich überprüfen, um zu sehen, ob unsere Tests bei der Ausführung bestehen. Üblicherweise möchte man jeden Aspekt der Verwendung testen, um sicherzustellen, dass der Code ordnungsgemäß getestet wird.

      Wir führen unseren Test erneut mit npm test aus, um diese bekannte Ausgabe zu erhalten:

      Output

      ... integrated test ✓ should be able to add and complete TODOs 1 passing (9ms)

      Sie haben nun einen integrierten Test mit dem Mocha-Framework und der assert-Bibliothek eingerichtet.

      Gehen wir nun von einer Situation aus, in der wir unser Modul mit einigen anderen Entwicklern geteilt haben und diese uns jetzt Feedback geben. Viele unserer Benutzer würden sich wünschen, dass die Funktion complete() einen Fehler meldet, wenn bisher noch keine TODOs hinzugefügt wurden. Wir fügen diese Funktionalität in unserer Funktion complete() ein.

      Öffnen Sie index.js in Ihrem Texteditor:

      Fügen Sie der Funktion Folgendes hinzu:

      todos/index.js

      ...
      complete(title) {
          if (this.todos.length === 0) {
              throw new Error("You have no TODOs stored. Why don't you add one first?");
          }
      
          let todoFound = false
          this.todos.forEach((todo) => {
              if (todo.title === title) {
                  todo.completed = true;
                  todoFound = true;
                  return;
              }
          });
      
          if (!todoFound) {
              throw new Error(`No TODO was found with the title: "${title}"`);
          }
      }
      ...
      

      Speichern und schließen Sie die Datei.

      Nun fügen wir einen neuen Test für diese neue Eigenschaft hinzu. Wir wollen verifizieren, ob ein Todos-Objekt, das keine Elemente enthält, unseren speziellen Fehler ausgibt, wenn wir es mit complete aufrufen.

      Gehen Sie in die index.test.js zurück:

      Fügen Sie am Ende der Datei den folgenden Code hinzu:

      todos/index.test.js

      ...
      describe("complete()", function() {
          it("should fail if there are no TODOs", function() {
              let todos = new Todos();
              const expectedError = new Error("You have no TODOs stored. Why don't you add one first?");
      
              assert.throws(() => {
                  todos.complete("doesn't exist");
              }, expectedError);
          });
      });
      

      Wie zuvor verwenden wir describe() und it(). Wir beginnen unseren Test mit der Erstellung eines neuen todos-Objekts. Dann definieren wir den Fehler, dessen Meldung wir erwarten, wenn wir die Funktion complete() aufrufen.

      Als Nächstes verwenden wir die Funktion throws() des assert-Moduls. Diese Funktion wurde erstellt, damit wir die Fehler, die unser Code ausgibt, verifizieren können. Sein erstes Argument ist eine Funktion, die den Code enthält, der den Fehler ausgibt. Das zweite Argument ist der Fehler, dessen Meldung wir erwarten.

      Führen Sie in Ihrem Terminal erneut die Tests mit npm test aus und Sie sehen die folgende Ausgabe:

      Output

      ... integrated test ✓ should be able to add and complete TODOs complete() ✓ should fail if there are no TODOs 2 passing (25ms)

      Diese Ausgabe zeigt den Nutzen, warum wir automatisiertes Testen mit Mocha und assert durchführen. Da unsere Tests schriftlich ausgearbeitet sind, verifizieren wir bei jeder Ausführung von npm test, dass alle unsere Tests bestehen. Wir mussten nicht manuell überprüfen, ob der andere Code noch funktioniert – wir wissen, dass es so ist, da der Test, den wir haben, bestand.

      Bisher haben unsere Tests die Ergebnisse von synchronem Code verifiziert. Wir behandeln nun, wie wir unsere neu gewonnenen Testgewohnheiten anpassen müssten, um mit asynchronem Code arbeiten zu können.

      Schritt 4 – Testen von asynchronem Code

      Eine der Eigenschaften, die wir in unserem TODO-Modul benötigen, ist eine CSV-Exportfunktion. Damit werden alle gespeicherten TODOs zusammen mit dem abgeschlossenen Status in einer Datei ausgegeben. Das erfordert die Verwendung des Moduls fs – eines integrierten Node.js-Moduls für die Arbeit mit dem Dateisystem.

      Das Schreiben in eine Datei ist eine asynchrone Operation. Es gibt viele Möglichkeiten, in eine Datei in Node.js zu schreiben. Wir können Callbacks, Promises oder die Schlüsselworte async/await verwenden. In diesem Abschnitt behandeln wir, wie wir Tests für diese verschiedenen Methoden schreiben.

      Callbacks

      Eine callback-Funktion ist eine Funktion, die als Argument in einer asynchronen Funktion verwendet wird. Sie wird aufgerufen, wenn die asynchrone Operation abgeschlossen ist.

      Wir fügen unserer Todos-Klasse eine Funktion namens saveToFile() hinzu. Diese Funktion erstellt eine Zeichenfolge, indem Sie alle unsere TODO-Elemente durchläuft, und schreibt diese Zeichenfolge in eine Datei.

      Öffnen Sie Ihre index.js-Datei:

      Fügen Sie den folgenden hervorgehobenen Code in die Datei ein:

      todos/index.js

      const fs = require('fs');
      
      class Todos {
          constructor() {
              this.todos = [];
          }
      
          list() {
              return [...this.todos];
          }
      
          add(title) {
              let todo = {
                  title: title,
                  completed: false,
              }
              this.todos.push(todo);
          }
      
          complete(title) {
              if (this.todos.length === 0) {
                  throw new Error("You have no TODOs stored. Why don't you add one first?");
              }
      
              let todoFound = false
              this.todos.forEach((todo) => {
                  if (todo.title === title) {
                      todo.completed = true;
                      todoFound = true;
                      return;
                  }
              });
      
              if (!todoFound) {
                  throw new Error(`No TODO was found with the title: "${title}"`);
              }
          }
      
          saveToFile(callback) {
              let fileContents = 'Title,Completedn';
              this.todos.forEach((todo) => {
                  fileContents += `${todo.title},${todo.completed}n`
              });
      
              fs.writeFile('todos.csv', fileContents, callback);
          }
      }
      
      module.exports = Todos;
      

      Zunächst müssen wir das fs-Modul in unsere Datei importieren. Dann haben wir unsere neue Funktion saveToFile() hinzugefügt. Unsere Funktion übernimmt eine Callback-Funktion, die genutzt wird, sobald die Schreiboperation der Datei abgeschlossen ist. In dieser Funktion erstellen wir eine fileContents-Variable, die die gesamte zu speichernde Zeichenfolge als Datei speichert. Sie wird mit den CSV-Titeln initialisiert. Dann durchlaufen wir jedes TODO-Element mit der forEach()-Methode des internen Arrays. Beim Durchlaufen fügen wir die title– und completed-Eigenschaften der einzelnen todos hinzu.

      Zum Schluss verwenden wir das fs-Modul zum Schreiben der Datei mit der writeFile()-Funktion. Unser erstes Argument ist der Dateiname: todos.csv. Das zweite ist der Inhalt der Datei, in diesem Fall unsere fileContents-Variable. Das letzte Argument ist unsere Callback-Funktion, die alle Schreibfehler der Datei behandelt.

      Speichern und schließen Sie die Datei.

      Wir schreiben nun einen Test für unsere Funktion saveToFile. Unser Test führt zwei Dinge aus: Er überprüft die Existenz der Datei und verifiziert, dass sie den richtigen Inhalt hat.

      Öffnen Sie die Datei index.test.js:

      Beginnen wir damit, das fs-Modul am Anfang der Datei zu laden, da wir es zum Testen unserer Ergebnisse verwenden werden:

      todos/index.test.js

      const Todos = require('./index');
      const assert = require('assert').strict;
      const fs = require('fs');
      ...
      

      Am Ende der Datei fügen wir unseren neuen Testfall hinzu:

      todos/index.test.js

      ...
      describe("saveToFile()", function() {
          it("should save a single TODO", function(done) {
              let todos = new Todos();
              todos.add("save a CSV");
              todos.saveToFile((err) => {
                  assert.strictEqual(fs.existsSync('todos.csv'), true);
                  let expectedFileContents = "Title,Completednsave a CSV,falsen";
                  let content = fs.readFileSync("todos.csv").toString();
                  assert.strictEqual(content, expectedFileContents);
                  done(err);
              });
          });
      });
      

      Wie zuvor verwenden wir describe(), um unseren Test getrennt von den anderen zu gruppieren, da er eine neue Funktionalität enthält. Die it()-Funktion unterscheidet sich leicht von unseren anderen. Normalerweise hat die von uns verwendete Callback-Funktion keine Argumente. Dieses Mal haben wir done als Argument. Wir benötigen dieses Argument, wenn wir Funktionen mit Callbacks testen. Die Callback-Funktion done() wird von Mocha verwendet, um ihr anzugeben, wenn eine asynchrone Funktion abgeschlossen ist.

      Alle in Mocha getesteten Callback-Funktionen müssen den Callback done() aufrufen. Wäre dies nicht der Fall, würde Mocha nie wissen, ob die Funktion abgeschlossen ist und würde festgefahren auf ein Signal warten.

      Wir erstellen nun unsere Todos-Instanz und fügen ihr ein einzelnes Element hinzu. Wir rufen die Funktion saveToFile() mit einem Callback auf, der einen Dateischreibfehler findet. Beachten Sie, wie unser Test für diese Funktion im Callback enthalten ist. Wenn unser Testcode außerhalb des Callbacks wäre, würde er fehlschlagen, solange der Code aufgerufen würde, bevor das Schreiben der Datei abgeschlossen wäre.

      In unserer Callback-Funktion überprüfen wir zunächst, dass unsere Datei existiert:

      todos/index.test.js

      ...
      assert.strictEqual(fs.existsSync('todos.csv'), true);
      ...
      

      Die Funktion fs.existsSync() gibt true aus, wenn der Dateipfad in ihrem Argument existiert, und andernfalls false.

      Anmerkung: Die Funktionen des fs-Moduls sind standardmäßig asynchron. Sie bildeten jedoch für Schlüsselfunktionen synchrone Gegenstücke. Dieser Test ist einfacher, wenn synchrone Funktionen verwendet werden, da wir den asynchronen Code nicht schachteln müssen, um sicherzustellen, dass er funktioniert. Im fs-Modul enden synchrone Funktionen normalerweise mit "Sync" am Ende ihrer Namen.

      Dann erstellen wir eine Variable, um unseren erwarteten Wert zu speichern:

      todos/index.test.js

      ...
      let expectedFileContents = "Title,Completednsave a CSV,falsen";
      ...
      

      Wir verwenden readFileSync() des fs-Moduls zum synchronen Lesen der Datei:

      todos/index.test.js

      ...
      let content = fs.readFileSync("todos.csv").toString();
      ...
      

      Wir geben readFileSync() den richtigen Pfad für die Datei: todos.csv. Da readFileSync() ein Buffer-Objekt ausgibt, das Binärdaten speichert, verwenden wir seine toString()-Methode, damit wir seinen Wert mit der Zeichenfolge vergleichen können, die wir voraussichtlich gespeichert haben.

      Wie zuvor verwenden wir das strictEqual des assert-Moduls, um einen Vergleich durchzuführen:

      todos/index.test.js

      ...
      assert.strictEqual(content, expectedFileContents);
      ...
      

      Wir beenden unseren Test durch das Aufrufen des done()-Callbacks, sodass Mocha weiß, dass der Test dieses Falls gestoppt wird:

      todos/index.test.js

      ...
      done(err);
      ...
      

      Wir geben das err-Objekt zu done(), sodass der Test mit Mocha fehlschlägt, falls ein Fehler vorhanden ist.

      Speichern und beenden Sie index.test.js.

      Wie zuvor führen wir diesen Test mit npm test durch. Ihre Konsole zeigt dann diese Ausgabe:

      Output

      ... integrated test ✓ should be able to add and complete TODOs complete() ✓ should fail if there are no TODOs saveToFile() ✓ should save a single TODO 3 passing (15ms)

      Sie haben nun Ihre erste asynchrone Funktion mit Mocha unter der Verwendung von Callbacks getestet. Zum Zeitpunkt des Schreibens dieses Tutorials sind Promises jedoch verbreiteter als Callbacks in neuem Node.js-Code, wie auch in unserem Artikel Schreiben von asynchronem Code in Node.js beschrieben. Als Nächstes lernen wir, wie wir auch diese mit Mocha testen können.

      Promises

      Ein Promise ist ein JavaScript-Objekt, das letztendlich einen Wert ausgibt. Wenn ein Promise erfolgreich ist, ist es gelöst. Wenn es auf einen Fehler trifft, wird es verworfen.

      Wir ändern die saveToFile()-Funktion, damit sie Promises anstelle von Callbacks verwendet. Öffnen Sie index.js:

      Zuerst müssen wir ändern, wie das fs-Modul geladen ist. Ändern Sie in Ihrer index.js-Datei die require()-Aussage am Anfang der Datei, sodass sie aussieht wie folgt:

      todos/index.js

      ...
      const fs = require('fs').promises;
      ...
      

      Wir haben nun das fs-Modul importiert, das Promises anstelle von Callbacks verwendet. Nun müssen wir einige Änderungen an saveToFile() vornehmen, damit es stattdessen mit Promises arbeitet.

      Führen Sie in Ihrem Texteditor die folgenden Änderungen an der Funktion saveToFile() aus, um die Callbacks zu entfernen:

      todos/index.js

      ...
      saveToFile() {
          let fileContents = 'Title,Completedn';
          this.todos.forEach((todo) => {
              fileContents += `${todo.title},${todo.completed}n`
          });
      
          return fs.writeFile('todos.csv', fileContents);
      }
      ...
      

      Der erste Unterschied besteht darin, dass unsere Funktion keine Argumente mehr akzeptiert. Mit Promises benötigen wir keine Callback-Funktion. Die zweite Änderung betrifft die Weise, wie die Datei geschrieben ist. Wir geben nun das Ergebnis des writeFile()-Promises aus.

      Speichern und schließen Sie index.js.

      Wir passen unseren Test so an, dass er mit Promises funktioniert. Öffnen Sie index.test.js:

      Ändern Sie den saveToFile()-Test auf Folgendes:

      todos/index.js

      ...
      describe("saveToFile()", function() {
          it("should save a single TODO", function() {
              let todos = new Todos();
              todos.add("save a CSV");
              return todos.saveToFile().then(() => {
                  assert.strictEqual(fs.existsSync('todos.csv'), true);
                  let expectedFileContents = "Title,Completednsave a CSV,falsen";
                  let content = fs.readFileSync("todos.csv").toString();
                  assert.strictEqual(content, expectedFileContents);
              });
          });
      });
      

      Als erste Änderung müssen wir den done()-Callback aus den Argumenten entfernen. Wenn Mocha das done()-Argument durchläuft, muss es aufgerufen werden oder es gibt einen Fehler wie folgt aus:

      1) saveToFile()
             should save a single TODO:
           Error: Timeout of 2000ms exceeded. For async tests and hooks, ensure "done()" is called; if returning a Promise, ensure it resolves. (/home/ubuntu/todos/index.test.js)
            at listOnTimeout (internal/timers.js:536:17)
            at processTimers (internal/timers.js:480:7)
      

      Schließen Sie beim Testen von Promises den done()-Callback nicht in it() ein.

      Um unser Promise zu testen, müssen wir unseren Assertionscode in die then()-Funktion einfügen. Beachten Sie, dass wir dieses Promise im Test ausgeben. Wir haben keine catch()-Funktion, um das Promise auszufangen, wenn es verworfen wird.

      Wir geben das Promise aus, sodass alle Fehler, die in der then()-Funktion ausgegeben werden, in der it()-Funktion heraustreten. Wenn die Fehler nicht heraustreten, wird der Testfall mit Mocha nicht fehlschlagen. Beim Testen von Promises müssen Sie return auf das getestete Promise verwenden. Andernfalls besteht das Risiko, ein falsch-positives Ergebnis zu erhalten.

      Wir lassen auch die catch()-Klausel aus, da Mocha erkennen kann, wenn ein Promise verworfen wird. Im Fall einer Verwerfung schlägt der Test automatisch fehl.

      Da unser Test nun fertig ist, speichern und beenden Sie die Datei und führen Sie anschließend Mocha mit npm test aus. Als Bestätigung erhalten wir ein erfolgreiches Ergebnis:

      Output

      ... integrated test ✓ should be able to add and complete TODOs complete() ✓ should fail if there are no TODOs saveToFile() ✓ should save a single TODO 3 passing (18ms)

      Wir haben unseren Code und Test zur Verwendung von Promises geändert und wissen nun sicher, dass es funktioniert. Die neuesten asynchronen Muster verwenden jedoch async/await-Schlüsselwörter, damit wir nicht mehrere then()-Funktionen erstellen müssen, um erfolgreiche Ergebnisse zu bearbeiten. Sehen wir als Nächstes, wie wir mit async/await testen können.

      async/await

      Die Schlüsselwörter async/await erleichtern die Arbeit mit Promises, da sie nicht so ausführlich sind. Sobald wir eine Funktion als asynchron mit dem Schlüsselwort async definieren, können wir alle zukünftigen Ergebnisse in dieser Funktion mit dem Schlüsselwort await erhalten. Auf diese Weise können wir Promises verwenden, ohne die Funktionen then() oder catch() verwenden zu müssen.

      Wir können unseren auf Promises basierenden saveToFile()-Test mit async/await vereinfachen. Führen Sie in Ihrem Texteditor diese kleineren Änderungen im saveToFile()-Test in der index.test.js aus:

      todos/index.test.js

      ...
      describe("saveToFile()", function() {
          it("should save a single TODO", async function() {
              let todos = new Todos();
              todos.add("save a CSV");
              await todos.saveToFile();
      
              assert.strictEqual(fs.existsSync('todos.csv'), true);
              let expectedFileContents = "Title,Completednsave a CSV,falsen";
              let content = fs.readFileSync("todos.csv").toString();
              assert.strictEqual(content, expectedFileContents);
          });
      });
      

      Die erste Änderung besteht darin, dass die von der it()-Funktion verwendete Funktion jetzt das Schlüsselwort async zur Definierung verwendet. Dadurch können wir das Schlüsselwort await in ihrem Körper verwenden.

      Die zweite Änderung tritt auf, wenn wir saveToFile() aufrufen. Bevor es aufgerufen wird, wird das Schlüsselwort await verwendet. Node.js wird nun warten, bis diese Funktion gelöst ist, bevor es den Test fortsetzt.

      Da wir den Code aus der then()-Funktion in den it()-Funktionskörper verschoben haben, ist unser Funktionscode leichter zu lesen. Die Ausführung dieses Codes mit npm test erzeugt diese Ausgabe:

      Output

      ... integrated test ✓ should be able to add and complete TODOs complete() ✓ should fail if there are no TODOs saveToFile() ✓ should save a single TODO 3 passing (30ms)

      Wir können jetzt asynchrone Funktionen testen, indem wir ein beliebiges von drei asynchronen Paradigmen entsprechend verwenden.

      Wir haben mit dem Testen von synchronem und asynchronem Code mit Mocha schon einen breiten Bereich abgedeckt. Als Nächstes tauchen wir tiefer ein in einige andere Funktionalitäten, die Mocha bietet, um unsere Testerfahrung zu verbessern. Besonders interessant ist hierbei auch, wie Hooks die Testumgebungen verändern können.

      Schritt 5 – Verwenden von Hooks zur Verbesserung von Testfällen

      Hooks sind ein nützlicher Bestandteil von Mocha, der es uns ermöglicht, die Umgebung vor und nach einem Test zu konfigurieren. Wir fügen Hooks typischerweise in einen describe()-Funktionsblock, da diese eine für einige Testfälle spezifische Auf- und Abbaulogik enthalten.

      Mocha bietet vier Hooks, die wir in unseren Tests verwenden können:

      • before: Dieser Hook wird einmal ausgeführt, bevor der erste Test beginnt.
      • beforeEach: Dieser Hook wird vor jedem Testfall ausgeführt.
      • after: Dieser Hook wird einmal ausgeführt, nachdem der letzte Testfall abgeschlossen ist.
      • afterEach: Dieser Hook wird nach jedem Testfall ausgeführt.

      Hooks sind sehr nützlich, wenn wir eine Funktion oder Eigenschaft mehrmals testen, da sie uns erlauben, den Einrichtungscode des Tests (wie das Erstellen des todos-Objekts) vom Assertionscode des Tests zu trennen.

      Um den Wert von Hooks zu sehen, fügen wir dem Testblock saveToFile() weitere Tests hinzu.

      Obwohl wir bestätigt haben, dass wir unsere TODO-Elemente in eine Datei speichern können, haben wir nur ein Element gespeichert. Außerdem wurde das Element nicht als abgeschlossen markiert. Wir fügen weitere Tests hinzu, um sicherzustellen, dass die verschiedenen Aspekte unseres Moduls funktionieren.

      Wir fügen zunächst einen zweiten Test hinzu, um zu bestätigen, dass unsere Datei korrekt gespeichert wird, wenn wir ein abgeschlossenes TODO-Element haben. Öffnen Sie Ihre Datei index.test.js mit Ihrem Texteditor:

      Ändern Sie den letzten Test folgendermaßen:

      todos/index.test.js

      ...
      describe("saveToFile()", function () {
          it("should save a single TODO", async function () {
              let todos = new Todos();
              todos.add("save a CSV");
              await todos.saveToFile();
      
              assert.strictEqual(fs.existsSync('todos.csv'), true);
              let expectedFileContents = "Title,Completednsave a CSV,falsen";
              let content = fs.readFileSync("todos.csv").toString();
              assert.strictEqual(content, expectedFileContents);
          });
      
          it("should save a single TODO that's completed", async function () {
              let todos = new Todos();
              todos.add("save a CSV");
              todos.complete("save a CSV");
              await todos.saveToFile();
      
              assert.strictEqual(fs.existsSync('todos.csv'), true);
              let expectedFileContents = "Title,Completednsave a CSV,truen";
              let content = fs.readFileSync("todos.csv").toString();
              assert.strictEqual(content, expectedFileContents);
          });
      });
      

      Der Test ist ähnlich wie zuvor. Die wichtigsten Unterschiede sind, dass wir die complete()-Funktion vor saveToFile() aufrufen, und unsere expectedFileContents nun true anstatt false für den Wert der completed-Kolumne haben.

      Speichern und schließen Sie die Datei.

      Wir führen unseren neuen Test und alle anderen mit npm test aus:

      Dadurch ergibt sich Folgendes:

      Output

      ... integrated test ✓ should be able to add and complete TODOs complete() ✓ should fail if there are no TODOs saveToFile() ✓ should save a single TODO ✓ should save a single TODO that's completed 4 passing (26ms)

      Es funktioniert wie erwartet. Es gibt jedoch Raum für Verbesserungen. Sie müssen ein Todos-Objekt zu Beginn des Tests instanziieren. Beim Hinzufügen von mehr Testfällen wird dies schnell repetitiv und vergeudet Speicherplatz. Außerdem erstellt der Test bei jeder Ausführung eine Datei. Das kann von jemandem, der sich mit dem Modul nicht so gut auskennt, mit einer tatsächlichen Ausgabe verwechselt werden. Es wäre schön, wenn wir unsere Ausgabedateien nach dem Testen bereinigen würden.

      Führen wir nun diese Verbesserungen mit Test-Hooks aus. Wir verwenden den Hook beforeEach(), um unsere Testvorrichtung von TODO-Elementen einzurichten. Eine Testvorrichtung ist jeder konsistente Zustand, der in einem Test verwendet wird. In unserem Fall ist unsere Testvorrichtung ein neues todos-Objekt, dem bereits ein TODO-Element hinzugefügt wurde. Wir verwenden afterEach(), um die vom Test erstellte Datei zu entfernen.

      Führen Sie in index.test.js die folgenden Änderungen an Ihrem letzten Test für saveToFile() aus:

      todos/index.test.js

      ...
      describe("saveToFile()", function () {
          beforeEach(function () {
              this.todos = new Todos();
              this.todos.add("save a CSV");
          });
      
          afterEach(function () {
              if (fs.existsSync("todos.csv")) {
                  fs.unlinkSync("todos.csv");
              }
          });
      
          it("should save a single TODO without error", async function () {
              await this.todos.saveToFile();
      
              assert.strictEqual(fs.existsSync("todos.csv"), true);
              let expectedFileContents = "Title,Completednsave a CSV,falsen";
              let content = fs.readFileSync("todos.csv").toString();
              assert.strictEqual(content, expectedFileContents);
          });
      
          it("should save a single TODO that's completed", async function () {
              this.todos.complete("save a CSV");
              await this.todos.saveToFile();
      
              assert.strictEqual(fs.existsSync('todos.csv'), true);
              let expectedFileContents = "Title,Completednsave a CSV,truen";
              let content = fs.readFileSync("todos.csv").toString();
              assert.strictEqual(content, expectedFileContents);
          });
      });
      

      Entschlüsseln wir alle vorgenommenen Änderungen. Wir haben dem Testblock ein beforeEach() hinzugefügt:

      todos/index.test.js

      ...
      beforeEach(function () {
          this.todos = new Todos();
          this.todos.add("save a CSV");
      });
      ...
      

      Diese beiden Zeilen Code erstellen ein neues Todos-Objekt, das in jedem unserer Tests verfügbar ist. Mit Mocha verweist das this-Objekt in beforeEach() auf dasselbe this-Objekt in it(). this ist für jeden Codeblock im describe()-Block gleich. Weitere Informationen über this finden Sie in unserem Tutorial Verstehen Sie This, Bind, Call und Apply in JavaScript.

      Diese leistungsstarke gemeinsame Nutzung des Kontexts ist der Grund, warum wir schnell Testvorrichtungen erstellen können, die für unsere beiden Tests funktionieren.

      Dann bereinigen wir unsere CSV-Datei in der afterEach()-Funktion:

      todos/index.test.js

      ...
      afterEach(function () {
          if (fs.existsSync("todos.csv")) {
              fs.unlinkSync("todos.csv");
          }
      });
      ...
      

      Wenn unser Test fehlgeschlagen ist, hat der Test möglicherweise keine Datei erstellt. Aus diesem Grund überprüfen wir, ob die Datei vorhanden ist, bevor wir die Funktion unlinkSync() verwenden, um diese zu löschen.

      Die verbleibenden Änderungen wechseln die Referenz von todos, die zuvor in der Funktion it() erstellt wurden, zu this.todos, das im Mocha-Kontext verfügbar ist. Wir haben auch die Zeilen gelöscht, die zuvor todos in den einzelnen Testfällen instanziierten.

      Führen wir nun diese Datei aus, um zu bestätigen, dass unsere Tests noch funktionieren. Geben Sie npm test in Ihr Terminal ein, um Folgendes zu erhalten:

      Output

      ... integrated test ✓ should be able to add and complete TODOs complete() ✓ should fail if there are no TODOs saveToFile() ✓ should save a single TODO without error ✓ should save a single TODO that's completed 4 passing (20ms)

      Die Ergebnisse sind gleich, und als zusätzlichen Vorteil haben wir die Einrichtungszeit für neue Tests für die Funktion saveToFile() leicht reduziert sowie eine Lösung für die zurückbleibende CSV-Datei gefunden.

      Zusammenfassung

      In diesem Tutorial haben Sie ein Node.js-Modul geschrieben, um TODO-Elemente zu verwalten und den Code manuell mit der Node.js-REPL getestet. Dann haben Sie eine Testdatei erstellt und das Mocha-Framework zur Ausführung automatisierter Tests verwendet. Mit dem assert-Modul konnten Sie verifizieren, ob Ihr Code funktioniert. Sie haben mit Mocha auch synchrone und asynchrone Funktionen getestet. Schließlich haben Sie Hooks mit Mocha erstellt, die das Schreiben mehrerer verwandter Testfälle wesentlich lesbarer und wartungsfreundlicher machen.

      Mit diesen neuen Kenntnissen können Sie nun versuchen, Tests für neue Node.js-Module zu schreiben, die Sie gerade erstellen. Können Sie über die Ein- und Ausgaben Ihrer Funktion nachdenken und Ihren Test schreiben, bevor Sie Ihren Code erstellen?

      Wenn Sie weitere Informationen über das Mocha-Framework erhalten möchten, besuchen Sie unsere offizielle Mocha-Dokumentation. Wenn Sie gerne noch mehr über Node.js lernen möchten, können Sie zu der Serienseite Codieren in Node.js zurückkehren.



      Source link

      Erstellen einer Anwendung für ein inspirierendes Zitat unter Verwendung von AdonisJs und MySQL


      Der Autor wählte den Tech Education Fund, um eine Spende im Rahmen des Programms Write for DOnations zu erhalten.

      Einführung

      AdonisJs ist ein in einfachem JavaScript geschriebenes Node.js Web-Framework, das auf allen gängigen Betriebssystemen läuft. Es verwendet das beliebte Designmuster MVC (Model – View – Controller) und bietet ein stabiles Ökosystem für das Schreiben serverseitiger Webanwendungen. Das Framework bietet nahtlose Authentifizierung, SQL ORM (objekt-relationales Mapping), Migrationen und Datenbank-Seeding. AdonisJs weist eine ähnliche Architektur wie das PHP Webanwendungs-Framework Laravel auf, einschließlich der gleichen Ordnerstruktur und mehrere gemeinsam genutzter Einrichtungskonzepte.

      Standardmäßig verwendet AdonisJs die Edge Template Engine, die für eine intuitive Verwendung konzipiert ist. Genau wie Laravel wird AdonisJs mit einem ORM namens Lucid bereitgestellt, das als Schnittstelle für die Kommunikation zwischen den Modellen einer Anwendung und der Datenbank dient. Mit AdonisJs können Entwickler eine Full-Stack-Anwendung erstellen, bei der der Backend-Server für die Anwendung der Geschäftslogik, das Routing und das Rendering aller Seiten der Anwendung verantwortlich ist. Es ist auch möglich, eine Webdienst-API zu erstellen, um JSON-Antworten von einem Controller zurückzugeben; diese Webdienste können dann über Frontend-Frameworks wie Vue.js, React und Angular konsumiert werden.

      In diesem Tutorial erstellen Sie eine Anwendung mit AdonisJs unter Verwendung seiner CLI. Sie erstellen Routen, Controller, Modelle und Ansichten innerhalb Ihrer Anwendung und führen Formularvalidierungen durch. Das Beispiel in diesem Tutorial wird eine Anwendung für ein inspirierendes Zitat sein, bei der sich ein Benutzer registrieren und anmelden kann, um ein inspirierendes Zitat zu erstellen. Diese Demo-Anwendung bietet Ihnen die Möglichkeit zur Ausführung von CRUD (Create, Read, Update und Delete)-Operationen.

      Voraussetzungen

      Bevor Sie mit diesem Leitfaden beginnen, benötigen Sie Folgendes:

      Anmerkung: Dieses Tutorial verwendet für die Entwicklung einen macOS-Rechner. Wenn Sie ein anderes Betriebssystem verwenden, müssen Sie in den ersten Schritten möglicherweise sudo für npm-Befehle verwenden.

      Schritt 1 – Installieren der Adonis CLI

      In diesem Abschnitt werden Sie Adonis CLI und alle erforderlichen Pakete auf Ihrem lokalen Rechner installieren. Die CLI ermöglicht es Ihnen, ein neues AdonisJs-Projekt aufzubauen, sowie Boilerplates für Controller, Middlewares und Modelle in Ihrer Anwendung zu erstellen und zu generieren. Außerdem erstellen Sie Ihre Datenbank für das Projekt.

      Führen Sie den folgenden Befehl aus, um die AdonisJs CLI global auf Ihrem Rechner über npm zu installieren:

      Geben Sie nach Abschluss der Installation den folgenden Befehl im Terminal ein, um die Installation von AdonisJs zu bestätigen und die aktuelle Version anzuzeigen:

      Sie sehen eine Ausgabe, die die aktuelle Version von AdonisJs zeigt:

      Output

      4.1.0

      Mit der erfolgreichen Installation der AdonisJs CLI haben Sie nun Zugriff auf den Befehl adonis und können ihn zum Erstellen neuer Installationen eines AdonisJs-Projekts, zur Verwaltung Ihres Projekts und zum Erzeugen relevanter Dateien wie die Controller, Modelle usw. verwenden.

      Nun können Sie mit der Erstellung eines neuen AdonisJs-Projekts fortfahren, indem Sie den Befehl adonis wie hier gezeigt verwenden:

      • adonis new adonis-quotes-app

      Der vorhergehende Befehl erstellt eine Anwendung namens adonis-quotes-app​​​ in einem neuen Verzeichnis mit demselben Namen in Ihrem lokalen Projektverzeichnis mit der entsprechenden AdonisJs MVC-Struktur.

      Verschieben Sie sie in den neuen Anwendungsordner:

      Starten Sie dann Ihre Anwendung durch Ausführen von:

      Dadurch wird der Entwicklungsserver auf dem Standardport 3333 gestartet, wie in der Root-Datei .env für Ihre Anwendung angegeben. Navigieren Sie zu http://localhost:3333, um die Begrüßungsseite von AdonisJs anzuzeigen.

      Begrüßungsseite von AdonisJs

      Jetzt schließen Sie die Einrichtung Ihrer Datenbank ab. Hier installieren Sie den Treiber mysql, um sich über npm von Ihrer Node.js-Anwendung mit Ihrem MySQL-Server zu verbinden. Gehen Sie zunächst wieder zu Ihrem Terminal zurück, auf dem die Anwendung derzeit läuft, beenden Sie den Prozess mit STRG+C und führen Sie den folgenden Befehl aus:

      Nachdem Sie nun den MySQL Node.js-Treiber für diese Anwendung erfolgreich installiert haben, müssen Sie die Anwendungsdatenbank erstellen und die entsprechende Verbindung zu ihr aufbauen.

      Die neueste Version von MySQL, die Sie aus dem vorbereitendem Tutorial installiert haben, verwendet ein standardmäßiges Authentifizierungs-Plugin namens caching_ha2_password. Dieses wird derzeit von den Node.js-Treibern für MySQL nicht unterstützt. Um Probleme mit der Datenbankverbindung in Ihrer Anwendung zu vermeiden, müssen Sie einen neuen MySQL-Benutzer anlegen und das derzeit unterstützte Authentifizierungs-Plugin mysql_native_password verwenden.

      Um zu beginnen, greifen Sie mit dem Account root auf den MySQL-Client zu:

      Sie werden aufgefordert, das während der MySQL-Installation eingerichtete Passwort für den Account root einzugeben.

      Erstellen Sie als Nächstes den Benutzer und das Passwort unter Verwendung des Plugins mysql_native_password:

      • CREATE USER 'sammy'@'localhost' IDENTIFIED WITH mysql_native_password BY 'password';

      Sie sehen den folgenden Output:

      Output

      Query OK, 0 rows affected (0.02 sec)

      Erstellen Sie nachfolgend eine Datenbank für die Anwendung mit:

      Sie sehen den folgenden Output:

      Output

      Query OK, 1 row affected (0.03 sec)

      Damit haben Sie die Datenbank für diese Anwendung erfolgreich erstellt.

      Aktivieren Sie jetzt den Zugriff auf die erstellte Datenbank für den neuen MySQL-Benutzer. Führen Sie den folgenden Befehl aus, um dem Benutzer alle Berechtigungen in der Datenbank zu erteilen:

      • GRANT ALL PRIVILEGES ON adonis.* TO 'sammy'@'localhost';

      Laden Sie die Berechtigungstabelle neu, indem Sie den folgenden Befehl ausführen, um die soeben vorgenommenen Änderungen zu übernehmen:

      Sie sehen den folgenden Output:

      Output

      Query OK, 0 rows affected (0.00 sec)

      Beenden Sie den MySQL-Client mit:

      Sie haben die AdonisJs CLI erfolgreich installiert, ein neues AdonisJs-Projekt erstellt und mysql über npm installiert.  Außerdem haben Sie die Datenbank für diese Anwendung erstellt und einen MySQL-Benutzer mit den entsprechenden Berechtigungen eingerichtet. Dies ist die Grundeinrichtung für Ihre Anwendung. Im nächsten Abschnitt beginnen Sie mit der Erstellung der erforderlichen Ansichten für Ihre Anwendung.

      Schritt 2 – Verwenden der Edge Templating Engine

      AdonisJs wird mit einer eigenen Template-Engine namens Edge bereitgestellt. Sie ermöglicht Ihnen die Erstellung einer wiederverwendbaren HTML-Vorlage und die Einführung von Front-End-Logik in Ihre Anwendung mit minimalem Code. Edge stellt JavaScript-Entwickler während der Entwicklung einer Anwendung die Werkzeuge zur Verfügung, um ein komponentenbasiertes Layout zu erstellen, Bedingungen zu schreiben, Iterationen zu verwenden und Anzeigeebenen zur Aufnahme der Logik zu erstellen. Alle Template-Dateien enden mit der Erweiterung .edge und werden im Verzeichnis resources/views gespeichert.

      Nachfolgend sind die Ansichten aufgeführt, die Ihre Anwendung für die ordnungsgemäße Funktionsweise benötigt:

      • Master Layout: Mit Edge können Sie eine Seite erstellen, die das CSS, gängige JavaScript-Dateien, jQuery und gängige Bestandteile der Benutzeroberfläche enthält, die in der gesamten Anwendung gleich bleiben, wie zum Beispiel die Navigationsleiste, das Logo, der Header usw. Sobald Sie die Seite Master Layout erstellt haben, übernehmen andere Ansichten (Seiten) in Ihrer Anwendung diese.
      • Index View: Diese Seite verwendet das Master Layout, um gängige Dateien zu erben und Inhalte für die Homepage der Anwendung darzustellen.
      • Login-Seite: Diese Seite verwendet ebenfalls das Master Layout und stellt das Formular mit den Eingabefeldern für Benutzername und Passwort für die Anmeldung der Benutzer dar.
      • Register-Seite: Hier sehen Benutzer ein Formular zur Registrierung und zur dauerhaften Speicherung der Informationen in der Datenbank.
      • Create Quote-Seite: Benutzer werden diese Seite zum Erstellen eines inspirierenden Zitats verwenden.
      • Edit Quote-Seite: Benutzer werden diese Seite zum Bearbeiten eines Zitats verwenden.
      • View Quote-Seite: Benutzer werden diese Seite zum Anzeigen eines bestimmten Zitats verwenden.

      Verwenden Sie zunächst den Befehl adonis, um die Seite Master Layout zu erstellen, indem Sie den folgenden Befehl ausführen:

      • adonis make:view layouts/master

      Sie werden eine Ausgabe ähnlich der folgenden sehen:

      Output

      ✔ create resources/views/layouts/master.edge

      Dieser Befehl erstellt automatisch eine Datei master.edge in Ihrem Ordner resources/views/layouts. Öffnen Sie die neue Datei:

      • nano resources/views/layouts/master.edge

      Fügen Sie den folgenden Code hinzu:

      /resources/views/layouts/master.edge

      <!DOCTYPE html>
      <html lang="en">
      <head>
          <meta charset="UTF-8">
          <meta name="viewport" content="width=device-width, initial-scale=1.0">
          <meta http-equiv="X-UA-Compatible" content="ie=edge">
          <title>adonis-quotes-app</title>
          {{ style('https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css') }}
          {{ style('style') }}
          {{ script('https://code.jquery.com/jquery-3.3.1.slim.min.js') }}
      </head>
      <body>
          <div class="container-fliud">
              @include('navbar')
              @!section('content')
          </div>
          {{ script('https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js') }}
      
      </body>
      </html>
      

      In dieser Datei schließen Sie die CDN-Dateien für Bootstrap CSS, Bootstrap JavaScript und jQuery ein. Sie fügen einen globalen CSS-Dateinamen style.css hinzu und schließen innerhalb der div eine Partiale-Datei namens navbar ein. Um Fragmente des HTML-Codes, die Sie auf mehreren Seiten Ihrer Anwendung benötigen, wie nav oder footer, wiederzuverwenden, können Sie Partiale einfügen. Dabei handelt es sich um kleinere Dateien, die den sich wiederholenden Code enthalten, sodass der Code für diese Elemente schneller an einer Stelle und nicht bei jedem Auftreten aktualisiert werden kann. Die navbar enthält Markierungen für die Schaltflächen Login und Register, ein Logo und einen Home-Link.

      Auf diese Weise können alle nachfolgenden für diese Anwendung erstellten Seiten das Master-Layout erweitern und die navbar rendern, ohne dass der Inhalt neu geschrieben werden muss. Sie erstellen diese Datei navbar später im Tutorial.

      Abschließend definieren Sie einen Abschnitt-Tag @! section(), um Inhalte anderer Seiten einzuschließen und sie vom Master-Layout rendern zu lassen. Damit dies wie erwartet funktioniert, müssen alle neuen Seiten, die das Master-Layout erweitern, auch einen Abschnitt-Tag mit demselben Namen (d. h. @section('content')) definieren.

      Speichern und schließen Sie die Datei, sobald Sie mit der Bearbeitung fertig sind.

      Als Nächstes verwenden Sie den Befehl adonis, um die Navigationsleiste zu erstellen:

      Sie sehen einen Output, der so ähnlich wie der nachfolgende aussieht:

      Output

      ✔ create resources/views/navbar.edge

      Öffnen Sie die neu erstellte Datei:

      • nano resources/views/navbar.edge

      Fügen Sie dann den folgenden Code hinzu:

      /resources/views/navbar.edge

      <nav class="navbar navbar-expand-lg navbar-dark text-white">
          <a class="navbar-brand" >LOGO</a>
          <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
              <span class="navbar-toggler-icon"></span>
          </button>
      
          <div class="collapse navbar-collapse" id="navbarNav">
              <ul class="navbar-nav">
                  <li class="nav-item active ">
                      <a class="btn text-white" href="https://www.digitalocean.com/">Home</a>
                  </li>
              </ul>
          </div>
          <div class="navbar-right" id="navbarNav">
              @loggedIn
                  <ul class="navbar-nav">
                          <li>
                              <div class="text-right">
                                   <a href="{{route('create.quote')}}" class="btn btn-outline-primary">Create Quote</a>
                              </div>
                          </li>
      
                      <li class="nav-item dropdown">
                          <a class="nav-link dropdown-toggle" href="https://www.digitalocean.com/#" id="navbarDropdownMenuLink" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
                             {{ auth.user.username}}
                          </a>
                          <div class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink">
                              <form method="POST" action="{{route('logout')}}">
                                  {{ csrfField() }}
                                    <button  type="submit" class="dropdown-item" href="">logout</button>
                              </form>
                          </div>
                      </li>
                  </ul>
              @else
                  <ul class="navbar-nav">
                      <li class="nav-item active pr-2">
                          <a href="{{route('login.create')}}" class="btn btn-outline-danger">
                            login
                          </a>
                      </li>
                      <li class="nav-item active pr-2">
                          <a href="{{route('register.create')}}" class="btn btn-outline-primary">
                              Register
                          </a>
                      </li>
                  </ul>
              @endloggedIn
          </div>
      </nav>
      

      Zusätzlich zur Definition der Links zu der Homepage und einer Schaltfläche zum Registrieren und Anmelden fügen Sie ein Tag @loggedIn hinzu. Damit können Sie eine bedingte Anweisung um den authentifizierten Benutzer herum schreiben und bei Bedarf entsprechende Inhalte anzeigen. Für einen authentifizierten Benutzer zeigt die Anwendung den Benutzernamen und eine Schaltfläche zum Erstellen eines neuen Zitats an. Wenn ein Benutzer nicht angemeldet ist, zeigt Ihre Anwendung eine Schaltfläche für die Anmeldung oder die Registrierung an. Diese Seite wird als Partiale auf jeder Seite eingefügt, so wie es zuvor im Master-Layout für diese Anwendung enthalten war.

      Speichern und schließen Sie die Datei.

      Nun erstellen Sie die Indexseite, die Sie für die Homepage der Anwendung verwenden. Sie wird die Liste aller inspirierenden Zitate, die die Benutzer schreiben, rendern und anzeigen:

      Sie werden eine Ausgabe sehen, die der folgenden ähnelt:

      Output

      ✔ create resources/views/index.edge

      Die hier erstellte Datei wird sich in resources/views/index.edge befinden. Öffnen Sie die Datei:

      • nano resources/views/index.edge

      Fügen Sie dann den folgenden Code hinzu:

      /resources/views/index.edge

      @layout('layouts/master')
      @section('content')
      
      <div class="container">
          <div class="text-center">
              @if(flashMessage('successmessage'))
                  <span class="alert alert-success p-1">{{ flashMessage('successmessage') }}</span>
              @endif
          </div>
          <div class="row">
              @each(quote in quotes)
                  <div class="col-md-4 mb-4 quote-wrapper">
                      <a href="https://www.digitalocean.com/view-quote/{{quote.id}}" class="w-100">
                          <div class="card shadow-lg bg-dark text-white">
                              <div class="card-body">
                                  <blockquote class="blockquote mb-0">
                                      <p>{{quote.body}}</p>
                                      <footer class="blockquote-footer">
                                          <cite title="Source Title"> {{quote.username}}</cite>
                                      </footer>
                                  </blockquote>
                                  @if(auth.user.id == quote.user_id)
                                    <div>
                                      <a  href="http://www.digitalocean.com/edit-quote/{{quote.id}}" class="btn btn-primary">edit</a>
                                      <a href="http://www.digitalocean.com/delete-quote/{{quote.id}}" class="btn btn-danger">delete</a>
                                    </div>
                                  @endif
                              </div>
                          </div>
                      </a>
                  </div>
              @else
               <div class="col-md-12 empty-quote text-center">
                      <p>No inspirational quote has been created</p>
               </div>
              @endeach
          </div>
      </div>
      @endsection
      

      Hier geben Sie an, dass diese Ansicht das Layout master durch Erweiterung verwenden wird. Diese Seite hat nun Zugriff auf alle Bibliotheken, Stylesheets und die navbar, die im Layout master enthalten sind. Als Nächstes iterieren Sie über ein Array von quotes (Zitaten) unter Verwendung des integrierten Tags @each. Das Array quotes wird von dem QuoteController, den Sie später in diesem Tutorial erstellen werden, an diese Ansicht übergeben. Wenn keine Zitate vorhanden sind, wird eine entsprechende Meldung angezeigt.

      Speichern und schließen Sie diese Datei.

      Um die Anmeldeseite zu erstellen, führen Sie nun den folgenden Befehl aus:

      • adonis make:view auth/login

      Sie werden eine Ausgabe sehen, die der folgenden ähnelt:

      Output

      ✔ create resources/views/auth/login.edge

      Dadurch wird automatisch ein Ordner auth innerhalb von resources/views erstellt und darin ebenfalls eine Datei login.edge angelegt. Öffnen Sie die Datei login.edge:

      • nano resources/views/auth/login.edge

      Fügen Sie den folgenden Inhalt hinzu:

      /resources/views/auth/login.edge

      @layout('layouts/master')
      @section('content')
        <div class="container">
          <div class="row">
            <div class="col-md-4 shadow bg-white mt-5 rounded offset-md-4">
              <form method="POST" action="{{route('login.store')}}">
                {{ csrfField() }}
                  <div>
                    @if(flashMessage('successmessage'))
                      <span class="alert alert-success p-1">{{ flashMessage('successmessage') }}</span>
                    @endif
                  </div>
                  <div class="form-group">
                    <label for="email">Email address</label>
                    <input type="email" class="form-control" id="email" name="email" value="{{old('email','')}}"  placeholder="Enter email">
                    {{ elIf('<span class=text-danger>$self</span>', getErrorFor('email'), hasErrorFor('email')) }}
                  </div>
                  <div class="form-group">
                    <label for="pasword">Password</label>
                    <input type="password" class="form-control" id="password" name="password" value="{{old('password','')}}" placeholder="Password">
                    {{ elIf('<span class=text-danger>$self</span>', getErrorFor('password'), hasErrorFor('password')) }}
                  </div>
      
                  <div class="text-center">
                    <button type="submit" class="btn btn-primary">Submit</button>
                  </div>
              </form>
            </div>
          </div>
        </div>
      @endsection
      

      Diese Datei enthält ein Formular, das Eingabeelemente enthält,mit denen Sie den Benutzernamen und das Passwort eines registrierten Benutzers erfassen, bevor dieser sich erfolgreich authentifizieren und mit der Erstellung von Zitaten beginnen kann. Ein weiteres wichtiges Element, das Sie auf dieser Seite beachten sollten, ist das {{ csrfField() }}​​​. Es handelt sich dabei um eine globale Variable, die AdonisJs verwendet, um das CSRF-Zugriffstoken zu übergeben, wenn eine POST-, PUT– und DELETE-Anfrage aus Ihrer Anwendung gesendet wird.

      Dies wurde eingerichtet, um Ihre Anwendung vor Cross-Site Request Forgery (CSRF)-Angriffen zu schützen. Es funktioniert, indem es für jeden Benutzer, der Ihre Website besucht, ein eindeutiges CSRF-Geheimnis erzeugt. Sobald Ihre Benutzer eine HTTP-Anfrage vom Frontend aus senden, wird ein entsprechendes Token für dieses Geheimnis generiert und mit der Anfrage weitergegeben. Auf diese Weise kann die für diese Anfrage innerhalb von AdonisJs erstellte Middleware überprüfen, ob sowohl das Token als auch das CSRF-Geheimnis gültig sind und dem aktuell authentifizierten Benutzer gehören.

      Speichern und schließen Sie die Datei, sobald Sie fertig sind.

      Als Nächstes erstellen Sie die Registrierungsseite mit diesem Befehl:

      • adonis make:view auth/register

      Sie werden eine ähnliche Ausgabe wie diese sehen:

      Output

      ✔ create resources/views/auth/register.edge

      Suchen und öffnen Sie die neu erstellte Datei in resources/views/auth/register.edge:

      • nano resources/views/auth/register.edge

      Fügen Sie folgenden Code hinzu:

      resources/views/auth/register.edge

      @layout('layouts/master')
      @section('content')
        <div class="container ">
          <div class="row">
              <div class="col-md-4  bg-white p-3 mt-5 shadow no-border rounded offset-md-4">
                <form method="POST" action="{{route('register.store')}}">
                  {{ csrfField() }}
                    <div class="form-group">
                      <label for="name">Fullname</label>
                      <input type="text" class="form-control" id="name" name="name"  value="{{old('name','')}}" placeholder="Enter Fullname">
                      {{ elIf('<span class=text-danger>$self</span>', getErrorFor('name'), hasErrorFor('name')) }}
                    </div>
                    <div class="form-group">
                      <label for="email">Email address</label>
                      <input type="email" class="form-control" id="email"  name="email" value="{{old('email','')}}" placeholder="Enter email">
                      {{ elIf('<span class=text-danger>$self</span>', getErrorFor('email'), hasErrorFor('email')) }}
                    </div>
                    <div class="form-group">
                      <label for="pasword">Password</label>
                      <input type="password" class="form-control" id="password" name="password" placeholder="Password">
                      {{ elIf('<span class=text-danger>$self</span>', getErrorFor('password'), hasErrorFor('password')) }}
                    </div>
                    <div class="text-center">
                        <button type="submit" class="btn btn-primary">Submit</button>
                    </div>
                </form>
              </div>
          </div>
        </div>
      @endsection
      

      Ähnlich wie die Anmeldeseite enthält diese Datei ein HTML-Formular mit Eingabefeldern zur Erfassung von Name, E-Mail und Passwort eines Benutzers während des Registrierungsvorgangs. Ebenfalls enthalten ist das {{ csrfField() }}, da es für jede Post-Anfrage für eine AdonisJs-Anwendung erforderlich ist.

      Speichern und schließen Sie die Datei.

      Nun erstellen Sie eine neue Datei, um ein inspirierendes Zitat zu erstellen, indem Sie den folgenden Befehl vom Terminal aus ausführen:

      • adonis make:view quotes/create-quote

      Sie sehen eine Ausgabe wie:

      Output

      ✔ create resources/views/quotes/create-quote.edge

      Öffnen Sie resources/views/quotes/create-quote.edge:

      • nano resources/views/quotes/create-quote.edge

      Und fügen Sie den folgenden Inhalt hinzu:

      /resources/views/quotes/create-quote.edge

      @layout('layouts/master')
      @section('content')
      <div class="container">
          <div class="row">
              <div class="col-md-3"></div>
              <div class="col-md-6 shadow bg-white mt-5 rounded p-3">
                  <div class="float-right">
                      <a href="https://www.digitalocean.com/" class="btn btn-outline-dark ">back</a>
                  </div>
                      <br>
      
                  <div class="clear-fix"></div>
                      <form method="POST" action="{{route('store.quote')}}">
                          {{ csrfField() }}
                          <div class="form-group">
                              <label for="quote">Create Quote</label>
                              <textarea type="text" rows="5"  name='body' id="body" class="form-control" id="quote" placeholder="Write an inspirational quote"></textarea>
                          </div>
      
                          <div class="text-center">
                              <button type="submit" class="btn btn-primary">Submit</button>
                          </div>
                      </form>
                  </div>
              </div>
              <div class="col-md-3"></div>
          </div>
      </div>
      @endsection
      

      Diese Seite erweitert das Master-Layout und enthält ein HTML-Formular mit einem Textbereichselement, das es dem Benutzer ermöglicht, Text über mehrere Zeilen einzugeben, bevor er über die entsprechende Route veröffentlicht und gehandhabt wird.

      Speichern und schließen Sie die Datei, sobald Sie fertig sind.

      Erstellen Sie als Nächstes eine Seite zur Bearbeitung eines bestimmten Zitats. Führen Sie den folgenden Befehl vom Terminal aus:

      • adonis make:view quotes/edit-quote

      Sie sehen den folgenden Output:

      Output

      ✔ create resources/views/quotes/edit-quote.edge

      Öffnen Sie die Datei mit:

      • nano resources/views/quotes/edit-quote.edge

      Fügen Sie den folgenden Inhalt zu resources/views/quotes/edit-quote hinzu:

      /resources/views/quotes/edit-quote.edge

      @layout('layouts/master')
      @section('content')
      <div class="container">
          <div class="row">
              <div class="col-md-6 shadow bg-white rounded p-3 offset-md-3">
                  <div class="float-right">
                      <a href="https://www.digitalocean.com/" class="btn btn-outline-dark ">back</a>
                  </div>
                  <br>
      
                  <div class="clear-fix"></div>
                  <form method="POST" action="/update-quote/{{quote.id}}">
                      {{ csrfField() }}
                      <div class="form-group">
                          <label for="pasword">Edit Quote</label>
                          <textarea type="text" rows="5"  name='body' id="body" class="form-control" id="quote" placeholder="write the inspirational quote">{{quote.body}}</textarea>
                      </div>
                      <div class="text-center">
                          <button type="submit" class="btn btn-primary">Update</button>
                      </div>
      
                  </form>
              </div>
          </div>
      </div>
      @endsection
      

      Diese Seite enthält ähnliche Inhalte wie die Datei create-quote.edge – der Unterschied besteht darin, dass sie die Details eines bestimmten Zitats enthält, die zu bearbeiten sind, <form method="POST" action="/update-quote/{{quote.id}}">.

      Speichern und schließen Sie die Datei.

      Erstellen Sie abschließend eine Seite, um ein einzelnes inspirierendes Zitat anzuzeigen:

      • adonis make:view quotes/quote

      Sie sehen eine Ausgabe, die dieser ähnelt:

      Output

      ✔ create resources/views/quotes/quote.edge

      Öffnen Sie die Datei mit:

      • nano resources/views/quotes/quote.edge

      Fügen Sie folgenden Code hinzu:

      /resources/views/quotes/quote.edge

      @layout('layouts/master')
      @section('content')
      <div class="container">
          <div class="row">
              <div class="col-md-6 offset-md-3">
                  <div class="card shadow-lg bg-dark text-white">
                      <div class="card-body">
                          <div class="float-right">
                              <a href="https://www.digitalocean.com/" class="btn btn-outline-primary ">back</a>
                          </div>
                              <br>
                          <div class="clear-fix"></div>
                          <blockquote class="blockquote mb-0">
                              <p>{{quote.body}}</p>
                              <footer class="blockquote-footer">
                                  <cite title="Source Title">{{quote.username}}</cite>
                              </footer>
                          </blockquote>
                      </div>
                  </div>
              </div>
          </div>
      </div>
      @endsection
      

      Diese Seite gibt die Details eines bestimmten Zitats wieder, einschließlich des Zitatkörpers quote.body, und des Autors, der es erstellt hat, quote.username.

      Wenn Sie mit der Datei fertig sind, speichern und schließen Sie sie.

      Sie haben alle erforderlichen Seiten für Ihre Anwendung mit Hilfe der Edge Templating Engine erstellt. Als Nächstes konfigurieren und stellen Sie eine Verbindung mit der Datenbank Ihrer Anwendung her.

      Schritt 3 – Erstellen eines Datenbankschemas

      Wenn Sie Ihre Anwendung jetzt bereitstellen, wird ein Fehler ausgegeben, da Sie die Anwendung noch mit einer Datenbank verbinden müssen. In diesem Abschnitt richten Sie eine Verbindung mit der Datenbank ein und verwenden dann den Befehl adonis zur Erzeugung einer Migrationsdatei, die zur Erstellung der Tabellen für die Datenbank verwendet wird.

      AdonisJs enthält ein ORM namens Lucid ORM, das eine aktive Datensatz-Implementierung für die Arbeit mit Ihrer Datenbank bereitstellt. Damit entfällt das mühsame Schreiben von SQL-Abfragen, die Daten in Echtzeit aus der Datenbank abrufen. Dies ist besonders hilfreich bei der Arbeit an einer komplexen Anwendung, die viele Abfragen erfordert. So können Sie beispielsweise alle Zitate aus Ihrer Anwendung abrufen, indem Sie eingeben:

      const quotes = await Quote.all()
      

      Um mit der entsprechenden Konfiguration für Ihre Anwendungsdatenbank fortzufahren, stellen Sie sicher, dass Sie sich immer noch im Stammverzeichnis Ihrer Anwendung befinden und erstellen Sie eine .env-Datei:

      Öffnen Sie die neu erstellte Datei und fügen Sie den folgenden Inhalt hinzu:

      .env

      HOST=127.0.0.1
      PORT=3333
      NODE_ENV=development
      APP_URL=http://${HOST}:${PORT}
      CACHE_VIEWS=false
      APP_KEY=bTVOEgUvmTCfkvgrK8gEBC3Qxt1xSYr0
      DB_CONNECTION=mysql
      DB_HOST=127.0.0.1
      DB_PORT=3306
      DB_USER=sammy
      DB_PASSWORD=password
      DB_DATABASE=adonis
      SESSION_DRIVER=cookie
      HASH_DRIVER=bcrypt
      

      Standardmäßig ist die Datenbankverbindung für eine AdonisJs-Anwendung SQLite, die Sie hier auf MySQL aktualisieren werden. Des Weiteren geben Sie den PORT für die Anwendung, die Anwendungsumgebung und Berechtigungsnachweise für die Datenbank an. Stellen Sie sicher, dass Sie die Platzhalter DB_USER, DB_PASSWORD und DB_DATABASE mit Ihren Berechtigungsnachweisen ersetzen.

      Als Nächstes erstellen Sie das Modell und eine Migrationsdatei für Quote unter Verwendung der Adonis CLI. Führen Sie dazu den folgenden Befehl aus:

      • adonis make:model Quote --migration

      Sie sehen eine Ausgabe, die der folgenden ähnelt:

      Output

      ✔ create app/Models/Quote.js ✔ create database/migrations/1568209992854_quote_schema.js

      Dieser Befehl erstellt ein Modell für Quote im Ordner app/Models und eine Schemadatei in dem Ordner database/migrations. Der neu erstellten Schemadatei wird der aktuelle Zeitstempel vorangestellt. Öffnen Sie die Schemadatei mit:

      • nano database/migrations/1568209992854_quote_schema.js

      Aktualisieren Sie den Inhalt mit dem folgenden Code:

      database/migrations/…quote_schema.js

      'use strict'
      /** @type {import('@adonisjs/lucid/src/Schema')} */
      const Schema = use('Schema')
      class QuoteSchema extends Schema {
        up () {
          this.create('quotes', (table) => {
            table.increments()
            table.integer('user_id').notNullable()
            table.string('username', 80).notNullable()
            table.string('body').notNullable()
            table.timestamps()
          })
        }
        down () {
          this.drop('quotes')
        }
      }
      module.exports = QuoteSchema
      

      Eine Schemadatei in AdonisJs erfordert zwei verschiedene Methoden:

      • up: Wird verwendet, um eine neue Tabelle zu erstellen oder eine bestehende Tabelle zu ändern.
      • down: Wird verwendet, um die bei der Methode up vorgenommene Änderung rückgängig zu machen.

      Zusätzlich zu den Feldern timestamps() und increments() aktualisieren Sie den Inhalt der Schemadatei mit den Feldattributen user_id, username und dem body eines zu erstellenden Zitats. Die Felder user_id und username verweisen auf die Details des Benutzers, der ein bestimmtes Zitat erstellt. Dies definiert eine 1:n-Beziehung und bedeutet, dass ein Benutzer eine unendliche Anzahl von Zitaten besitzen kann, während ein einzelnes Zitat nur einem Benutzer gehören kann.

      Speichern und schließen Sie die Datei.

      AdonisJs wird standardmäßig mit Modell User und der zugehörigen Migrationsdatei installiert. Dies erfordert nur eine kleine Änderung, um die Beziehung zwischen dem Modell User und Quote herzustellen.

      Öffnen Sie das Modell User in app/Models/User.js:

      Fügen Sie diese Methode unmittelbar nach der Methode tokens() hinzu:

      app/Models/User.js

      ...
      class User extends Model {
        ...
        tokens () {
          return this.hasMany('App/Models/Token')
        }
      
        quote () {
          return this.hasMany('App/Models/Quote')
        }
      }
      
      module.exports = User
      

      Dadurch wird eine 1:n-Beziehung mit der Tabelle Quote unter Verwendung von user_id als Fremdschlüssel festgelegt.

      Speichern und schließen Sie die Datei.

      Um diesen Abschnitt abzuschließen, verwenden Sie den folgenden Befehl zur Ausführung von Migrationen, wodurch die Methode up() aller Migrationsdateien ausgeführt wird:

      Sie werden eine Ausgabe sehen, die der folgenden ähnelt:

      Output

      migrate: 1503248427885_user.js migrate: 1503248427886_token.js migrate: 1568209992854_quote_schema.js Database migrated successfully in 3.42 s

      Sie haben eine Verbindung mit Ihrer Datenbank konfiguriert und gesichert. Des Weiteren haben Sie ein Modell Quote und die dazugehörige Schemadatei erstellt und eine 1:n-Beziehung zwischen User und Quote hergestellt. Als Nächstes generieren Sie die Routen und Controller zur Bearbeitung von HTTP-Anfragen und die Geschäftslogik zum Erstellen, Bearbeiten und Löschen eines inspirierenden Zitats.

      Schritt 4 – Erstellen von Controllern und Einrichten von Routen

      In diesem Abschnitt beginnen Sie mit der Erstellung von Controllern, die die gesamte Logik für die Anwendung verarbeiten und diese später an eine bestimmte Route anhängen, damit die Benutzer über eine URL darauf zugreifen können.

      Zu Beginn werden Sie mit der Adonis CLI einen neuen HTTP-Request-Controller erstellen, der alle Authentifizierungsprozesse für Ihre Anwendung verarbeiten kann, indem Sie den folgenden Befehl ausführen:

      • adonis make:controller Auth --type http

      Dieser Befehl erstellt eine Datei AuthController.js und speichert sie innerhalb des Ordners app/Controllers/Http. Sie verwenden das Flag --type, um anzugeben, dass dieser Controller ein HTTP-Controller sein soll.

      Sie werden eine Ausgabe sehen, die der folgenden ähnelt:

      Output

      ✔ create app/Controllers/Http/AuthController.js

      Öffnen Sie als Nächstes die neu erstellte Controller-Datei:

      • nano app/Controllers/Http/AuthController.js

      Aktualisieren Sie sie mit dem folgenden Inhalt:

      app/Controllers/Http/AuthController.js

      'use strict'
      const User = use('App/Models/User')
      class AuthController {
      
          loginView({ view }) {
              return view.render('auth.login')
          }
          registrationView({ view }) {
              return view.render('auth.register')
          }
      
          async postLogin({ request, auth, response}) {
              await auth.attempt(request.input('email'), request.input('password'))
              return response.route('index')
          }
      
          async postRegister({ request, session, response }) {
              const user = await User.create({
                  username: request.input('name'),
                  email: request.input('email'),
                  password: request.input('password')
              })
              session.flash({ successmessage: 'User have been created successfully'})
              return response.route('login.create');
          }
      
          async logout ({ auth, response }) {
              await auth.logout()
              return response.route("https://www.digitalocean.com/")
          }
      }
      module.exports = AuthController
      

      In dieser Datei importieren Sie das Modell User und erstellen dann zwei Methoden namens loginView() und registerView(), um die Anmelde- bzw. Registrierungsseite zu rendern. Abschließend erstellen Sie die folgenden asynchronen Methoden:

      • postLogin(): Mit dieser Methode erhalten Sie den Wert von email und password, die mit Hilfe der in AdonisJs eingebauten Methode request gepostet wurden, und validieren dann diesen Benutzer anhand der Details in der Datenbank. Wenn ein solcher Benutzer in der Datenbank existiert und den korrekten Berechtigungsnachweis eingegeben hat, wird er zurück auf die Homepage umgeleitet und authentifiziert, bevor er ein neues Zitat erstellen kann. Andernfalls wird eine Meldung angezeigt, die auf den falschen Berechtigungsnachweis hinweist.
      • postRegister(): Dies erhält den Wert von username, email und password für einen Benutzer, um ein Konto für diesen Benutzer in der Datenbank zu erstellen. Eine Nachricht mit der Angabe, dass dieser Benutzer erfolgreich erstellt wurde, wird an die Sitzung weitergeleitet, und der Benutzer wird auf die Anmeldeseite umgeleitet, um authentifiziert zu werden und mit der Erstellung eines Zitats zu beginnen.
      • logout(): Diese Methode verarbeitet die Logout-Funktionalität und leitet den Benutzer zurück auf die Homepage.

      Speichern und schließen Sie die Datei.

      Nachdem Sie nun den Controller für die Registrierung und Authentifizierung von Benutzern eingerichtet haben, fahren Sie mit der Erstellung eines HTTP-Request-Controllers für die Verwaltung aller Operationen in Bezug auf Zitate fort.

      Führen Sie, zurück im Terminal, den folgenden Befehl aus, um den QuoteController zu erstellen:

      • adonis make:controller Quote --type http --resource

      Die Verwendung des Flags --resource erstellt einen Controller mit vordefinierten kreativen Methoden zur Durchführung von CRUD (Create, Read, Update und Delete)-Operationen.

      Sie sehen:

      Output

      ✔ create app/Controllers/Http/QuoteController.js

      Finden Sie diese Datei innerhalb von app/Controllers/Http/QuoteControllers.js:

      • nano app/Controllers/Http/QuoteController.js

      Aktualisieren Sie sie mit dem folgenden Inhalt:

      app/Controllers/Http/QuoteController.js

      'use strict'
      const Quote = use('App/Models/Quote')
      
      class QuoteController {
      
        async index ({ view }) {
          const quote = await Quote.all()
          return view.render('index', {
            quotes: quote.toJSON()
          })
        }
      
        async create ({ view }) {
          return view.render('quotes.create-quote')
        }
      
        async store ({ request,auth,session, response }) {
          const quote = await Quote.create({
            user_id: auth.user.id,
            username: auth.user.username,
            body: request.input('body')
          })
          session.flash({ 'successmessage': 'Quote has been created'})
          return response.redirect("https://www.digitalocean.com/")
        }
      
        async show ({ params, view }) {
          const quote = await Quote.find(params.id)
          return view.render('quotes.view-quote', {
            quote: quote.toJSON()
          })
        }
      
        async edit ({ params, view }) {
          const quote = await Quote.find(params.id)
          return view.render('quotes.edit-quote', {
            quote: quote.toJSON()
          })
        }
      
        async update ({ params, request, response, session }) {
          const quote = await Quote.find(params.id)
          quote.body = request.input('body')
          await quote.save()
          session.flash({'successmessage': 'Quote has been updated'})
          return response.redirect("https://www.digitalocean.com/")
        }
      
        async destroy ({ params, response, session }) {
          const quote = await Quote.find(params.id)
          await quote.delete()
          session.flash({'successmessage': 'Quote has been deleted'})
          return response.redirect("https://www.digitalocean.com/")
        }
      }
      module.exports = QuoteController
      

      In diesem Controller haben Sie das Modell Quote importiert und die folgenden Methoden aktualisiert, die automatisch mit AdonisJs CLI erstellt wurden:

      • index(): Um alle Zitate aus der Datenbank zu holen und sie auf der Homepage der Anwendung darzustellen.
      • create(): Um eine Seite zum Erstellen von Zitaten darzustellen.
      • store(): Um ein neu erstelltes Zitat dauerhaft in der Datenbank zu speichern und eine entsprechende Antwort zurückzugeben.
      • show(): Um die id eines bestimmten Zitats zu erhalten, es aus der Datenbank abzurufen und auf der Seite zur Bearbeitung von Zitaten anzuzeigen.
      • edit(): Um Details eines bestimmten Zitats aus der Datenbank abzurufen und zur Bearbeitung darzustellen.
      • update(): Um jede Aktualisierung eines Zitats zu verarbeiten und den Benutzer zurück auf die Homepage zu leiten.
      • destroy(): Um ein bestimmtes Zitat zu löschen und vollständig aus der Datenbank zu entfernen.

      Speichern und schließen Sie die Datei.

      Nachdem Sie alle erforderlichen Controller für diese Anwendung erstellt haben, können Sie nun die Routen so einrichten, dass die Benutzer problemlos mit Ihrer Anwendung interagieren können. Navigieren Sie zu Beginn zu der Datei start/routes.js:

      Ersetzen Sie den Inhalt mit dem folgenden:

      start/routes.js

      'use strict'
      const Route = use('Route')
      
      Route.get("https://www.digitalocean.com/",'QuoteController.index').as('index')
      Route.get('/register','AuthController.registrationView').as('register.create')
      Route.post('/register-store','AuthController.postRegister').as('register.store').validator('Register')
      Route.get('/login','AuthController.loginView').as('login.create')
      Route.post('/login-store','AuthController.postLogin').as('login.store')
      Route.get('/view-quote/:id','QuoteController.show').as('view.quote')
      
      Route.group(() => {
          Route.get('/create-quote','QuoteController.create').as('create.quote')
          Route.post('/store-quote','QuoteController.store').as('store.quote')
          Route.get('/edit-quote/:id','QuoteController.edit').as('edit.quote')
          Route.post('/update-quote/:id','QuoteController.update').as('update.quote')
          Route.get('/delete-quote/:id','QuoteController.destroy').as('delete.quote')
          Route.post('/logout','AuthController.logout').as('logout')
      }).middleware(['auth'])
      

      Hier definieren Sie den Pfad für jede Route in Ihrer Anwendung, geben die HTTP-Verben für jede Aktion an und binden die Route an eine bestimmte Methode in jedem Controller. Außerdem benennen Sie jede dieser Routen so, wie sie in den Controllern und Ansichten referenziert wurden.

      Um sicherzustellen, dass nur authentifizierte Benutzer auf alle Zitatrouten zugreifen können, ordnen Sie eine Gruppe namens Middleware zu. Schließlich hängen Sie eine Validierer-Methode an die Route register.store an, um die Benutzereingaben zu validieren.

      Speichern und schließen Sie die Datei.

      Sie haben Ihre Controller erstellt und die Routen für Ihre Anwendung eingerichtet. Als Nächstes erstellen Sie die in diesem Schritt definierte Validierer-Methode.

      Schritt 5 – Validieren der Benutzereingabe

      Standardmäßig verfügt AdonisJs nicht über integrierte Validierer. Daher müssen Sie den Validierer für Ihre Anwendung manuell installieren und registrieren.

      Führen Sie den folgenden Befehl aus, um ihn zu installieren:

      Öffnen Sie die folgende Datei, um den Anbieter des Validieres (Validator Provider) zu registrieren:

      Registrieren Sie dann den Anbieter des Validieres, indem Sie ihn an die Liste der Anbieter anhängen, wie nachfolgend gezeigt:

      start/app.js

      ...
      const providers = [
         ...
         '@adonisjs/cors/providers/CorsProvider',
         '@adonisjs/shield/providers/ShieldProvider',
         '@adonisjs/session/providers/SessionProvider',
         '@adonisjs/auth/providers/AuthProvider',
         '@adonisjs/validator/providers/ValidatorProvider'
      ]
      

      Nachdem Sie den Anbieter des Validieres innerhalb Ihrer Anwendung installiert und registriert haben, erstellen Sie nun mit dem folgenden Befehl einen benutzerdefinierten Validierer, um die Eingabe des Benutzers während der Registrierung zu validieren:

      • adonis make:validator Register

      Dadurch wird eine Datei Register.js im Verzeichnis App/Validators erstellt. Öffnen Sie die Datei mit:

      • nano app/Validators/Register.js

      Fügen Sie den folgenden Code zur Datei hinzu:

      app/Validators/Register.js

      'use strict'
      class Register {
        get rules () {
          return {
            name:'required',
            email:'required|email|unique:users',
            password:'required|min:8'
          }
        }
      
        get messages(){
          return{
            'name.required':'Full name is required',
            'email.required':'email is required',
            'email.unique':'email already exists',
            'password.required':'password is required',
            'password.min':'password should be at least 8 characters'
          }
        }
      }
      module.exports = Register
      

      In Ihrer Anwendung definieren Sie Regeln für bestimmte Felder. Wenn Validierungen zu einem beliebigen Zeitpunkt fehlschlagen, setzt der Validierer den Fehler automatisch als Flash-Meldung und der Benutzer wird zurück zum Formular geleitet.

      Speichern und schließen Sie die Datei, sobald Sie die Bearbeitung abgeschlossen haben.

      Um Ihrer Anwendung ein Design hinzuzufügen, öffnen Sie schließlich die folgende Datei:

      Ersetzen Sie die Inhalte mit dem folgenden:

      /public/style.css

      @import url('https://fonts.googleapis.com/css?family=Montserrat:300');
      
      html, body {
        height: 100%;
        width: 100%;
      }
      
      body {
        font-family: 'Montserrat', sans-serif;
        font-weight: 300;
        background-image: url("/splash.png");
        background-color: #220052;
      }
      
      * {
        margin: 0;
        padding: 0;
      }
      
      a {
        color: inherit;
        text-decoration: underline;
      }
      
      p {
        margin: 0.83rem 0;
      }
      
      .quote-wrapper {
        margin-top: 20px;
      }
      
      .quote-wrapper a {
        text-decoration: none;
      }
      
      .quote-wrapper a:hover {
        color: #ffffff;
      }
      
      .empty-quote {
        color: #ffffff;
      }
      
      form {
        padding: 20px;
      }
      

      In dieser Datei aktualisieren Sie das CSS-Design Ihrer Anwendung in der Datei style.css.

      Sie haben einen Anbieter eines Validieres installiert und registriert, um die Eingabe von Benutzern während des Registrierungsvorgangs zu überprüfen. Des Weiteren haben Sie den Inhalt Ihres Stylesheets aktualisiert, um der Anwendung mehr Design hinzuzufügen. Im letzten Schritt testen Sie Ihre Anwendung.

      Schritt 6 – Bereitstellen der Anwendung

      In diesem Schritt stellen Sie Ihre Anwendung bereit und erstellen einen Benutzer und ein Passwort zum Testen der Authentifizierung. Außerdem fügen Sie Ihrer Anwendung ein Zitate hinzu und zeigen es auf der Homepage an.

      Um Ihre Anwendung zu testen, starten Sie den Entwicklungsserver mit dem folgenden Befehl aus dem Stammverzeichnis Ihrer Anwendung:

      Dadurch wird die Anwendung auf dem innerhalb der Stammdatei .env definierten Port, 3333, gestartet. Navigieren Sie in Ihrem Browser zu http://localhost:3333.

      Homepage der Zitat-Anwendung

      Die Homepage ist zur Zeit leer, da Sie keine Zitate erstellt haben. Klicken Sie auf die Schaltfläche Register.

      Anmeldeseite

      Geben Sie Ihre Informationen ein und klicken Sie auf die Schaltfläche Submit, um den Registrierungsvorgang abzuschließen. Sie werden auf die Anmeldeseite umgeleitet. Geben Sie Ihre E-Mail-Adresse und Ihr Passwort für die Authentifizierung ein.

      Anmeldeseite

      Nachdem Sie authentifiziert sind, klicken Sie auf die Schaltfläche Create Quote.

      Seite „Zitat erstellen“

      Geben Sie ein Zitat ein und navigieren Sie zu der Seite View all, um Ihr Zitat anzuzeigen.

      Seite „Alle Zitate anzeigen“

      Sie haben Ihre Anwendung getestet, indem Sie einen Benutzer erstellt und authentifiziert und anschließend ein Zitat verfasst haben.

      Zusammenfassung

      In diesem Tutorial haben Sie eine Webanwendung mit AdonisJs erstellt. Sie haben die Anwendung unter Verwendung der AdonisJs CLI eingerichtet und die CLI zum Erstellen anderer relevanter Dateien wie Controller, Modelle und Ansichten verwendet.

      Sie können Webanwendungen mit diesem Framework unabhängig von Ihrer Größe und Komplexität erstellen. Sie können den Quellcode für dieses Projekt hier auf GitHub herunterladen. Um weitere Funktionen zu erkunden, können Sie auch die offizielle Dokumentation besuchen.

      Wenn Sie einige unserer anderen JavaScript-Framework-Tutorials erkunden möchten, sehen Sie sich Folgendes an:



      Source link