Arbeiten mit unerwarteten Daten in JavaScript





Eines der Hauptprobleme bei dynamisch typisierten Sprachen besteht darin, dass Sie nicht immer einen korrekten Datenfluss garantieren können, da Sie beispielsweise nicht erzwingen können, dass ein Parameter oder eine Variable auf einen anderen Wert als null gesetzt wird. In solchen Fällen verwenden wir in der Regel einfachen Code:



function foo (mustExist) {
  if (!mustExist) throw new Error('Parameter cannot be null')
  return ...
}


Das Problem bei diesem Ansatz ist die Codeverschmutzung, da Sie Variablen überall testen müssen und nicht garantiert werden kann, dass alle Entwickler diesen Test tatsächlich immer ausführen, insbesondere in Situationen, in denen eine Variable oder ein Parameter nicht null sein kann. Oft wissen wir nicht einmal, dass ein solcher Parameter den Wert undefiniert oder null haben kann - dies geschieht häufig, wenn verschiedene Spezialisten an den Client- und Serverteilen arbeiten, dh in den allermeisten Fällen.



Um dieses Szenario ein wenig zu optimieren, suchte ich nach Möglichkeiten, wie und mit welchen Strategien der Überraschungsfaktor am besten minimiert werden kann. Da bin ich auf einen tollen Artikel von Eric Elliott gestoßen.... Der Zweck dieser Arbeit ist nicht, seinen Artikel vollständig zu widerlegen, sondern interessante Informationen hinzuzufügen, die ich dank meiner Erfahrung auf dem Gebiet der JavaScript-Entwicklung im Laufe der Zeit entdecken konnte.



Bevor ich anfange, möchte ich einige der in diesem Artikel behandelten Punkte durchgehen und meine Meinung als Serverkomponentenentwickler zum Ausdruck bringen, da ein anderer Artikel eher kundenorientiert ist.



Wie alles begann



Das Datenverarbeitungsproblem kann auf mehrere Faktoren zurückzuführen sein. Der Hauptgrund ist natürlich die Benutzereingabe. Zusätzlich zu den in einem anderen Artikel genannten Quellen gibt es jedoch noch andere Quellen für fehlerhafte Daten:



  • Datenbankeinträge
  • Funktionen, die implizit Nulldaten zurückgeben
  • Externe APIs


In allen betrachteten Fällen gelten unterschiedliche Lösungen, und später werden wir jede im Detail analysieren, wobei wir uns daran erinnern, dass keine ein Allheilmittel ist. Die meisten Probleme werden durch menschliches Versagen verursacht: In vielen Fällen sind Sprachen darauf vorbereitet, mit null oder undefinierten Daten (null oder undefiniert) zu arbeiten. Bei der Transformation dieser Daten kann jedoch die Fähigkeit zur Verarbeitung verloren gehen.



Vom Benutzer eingegebene Daten



In diesem Fall haben wir nur sehr wenige Möglichkeiten. Wenn das Problem in der Benutzereingabe liegt, kann es mit der sogenannten Hydratation gelöst werden (mit anderen Worten, wir müssen die Roheingabe, die der Benutzer an uns sendet (zum Beispiel als Teil einer API-Nutzlast), in etwas umwandeln, mit dem wir können fehlerfrei arbeiten).



Auf der Serverseite können wir bei Verwendung eines Webservers wie Express alle Vorgänge mit Benutzereingaben auf der Clientseite mithilfe von Standardtools wie JSON-Schema  oder Joi ausführen .



Ein Beispiel dafür, was mit Express oder AJV gemacht werden kann, finden Sie unten:



const Ajv = require('ajv')
const Express = require('express')
const bodyParser = require('body-parser')
 
const app = Express()
const ajv = new Ajv()
 
app.use(bodyParser.json())
 
app.get('/foo', (req, res) => {
  const schema = {
    type: 'object',
    properties: {
      name: { type: 'string' },
      password: { type: 'string' },
      email: { type: 'string', format: 'email' }
    },
    additionalProperties: false
    required: ['name', 'password', 'email']
  }
 
  const valid = ajv.validate(schema, req.body)
    if (!valid) return res.status(422).json(ajv.errors)
    // ...
})
 
app.listen(3000)


Schauen Sie: Wir überprüfen den Hauptteil der Route. Standardmäßig ist dies das Objekt, das wir vom Body-Parser-Paket als Teil der Nutzlast erhalten. In diesem Fall wird es durch ein JSON-Schema geleitet , sodass überprüft wird, ob eine dieser Eigenschaften einen anderen Typ oder ein anderes Format hat (im Fall von E-Mail).



Wichtig! Beachten Sie, dass wir ein HTTP 422 für ein unverarbeitetes Objekt zurückgeben . Viele Benutzer interpretieren einen Abfragefehler, z. B. einen ungültigen Text oder eine ungültige Abfragezeichenfolge, als Fehler. 400  Ungültige Abfrage - Dies ist teilweise richtig, aber in diesem Fall lag das Problem nicht in der Anfrage selbst, sondern in den Daten, die der Benutzer mit ihm gesendet hat. Die optimale Antwort auf den Benutzer wäre also Fehler 422: Dies bedeutet, dass die Anforderung korrekt ist, aber nicht verarbeitet werden kann, da ihr Inhalt nicht das erwartete Format aufweist.



Eine andere Option (neben der Verwendung von AJV) ist die Verwendung der Bibliothek, die ich mit Roz erstellt habe . Wir haben es Expresso genannt , und es handelt sich um eine Reihe von Bibliotheken, die es etwas einfacher machen, APIs zu entwickeln, die Express verwenden. Ein solches Tool ist  @ expresso / validator , das im Wesentlichen das tut, was wir oben gezeigt haben, aber als Middleware übergeben werden kann.



Zusätzliche Parameter mit Standardwerten



Zusätzlich zu dem, was wir zuvor überprüft haben, haben wir die Möglichkeit entdeckt, einen Nullwert an unsere Anwendung zu übergeben, falls dieser nicht in einem optionalen Feld gesendet wird. Stellen Sie sich zum Beispiel vor, wir haben eine Paginierungsroute, die zwei Parameter, Seite und Größe, als Abfragezeichenfolgen verwendet. Sie sind jedoch optional und sollten standardmäßig auf Standard gesetzt werden, wenn sie nicht empfangen werden.



Idealerweise sollte unser Controller eine Funktion haben, die ungefähr so ​​funktioniert:



function searchSomething (filter, page = 1, size = 10) {
  // ...
}


Hinweis. Genau wie bei dem 422-Fehler, den wir als Antwort auf Paging-Anfragen zurückgegeben haben, ist es wichtig, den korrekten Fehlercode 206 Unvollständiger Inhalt zurückzugeben , wenn wir auf eine Anfrage antworten, für die die zurückgegebene Datenmenge Teil eines Ganzen ist, geben wir zurück 206. Wenn der Benutzer die letzte Seite erreicht hat und keine Daten mehr vorhanden sind, können wir einen Code von 200 zurückgeben. Wenn der Benutzer versucht, eine Seite außerhalb des gesamten Seitenbereichs zu finden, geben wir einen Code von 204 No content zurück .



Dies würde das Problem lösen, wenn wir zwei Nullwerte erhalten, aber dies ist ein sehr kontroverser Aspekt von JavaScript im Allgemeinen. Optionale Parameter nehmen nur dann einen Standardwert an, wenn der Wert leer ist. Diese Regel funktioniert jedoch nicht für den Wert null. Wenn wir also Folgendes tun:



function foo (a = 10) {
  console.log(a)
}
 
foo(undefined) // 10
foo(20) // 20
foo(null) // null


und wir müssen die Informationen als null behandeln, wir können uns hierfür nicht ausschließlich auf optionale Parameter verlassen. Daher haben wir in solchen Fällen zwei Möglichkeiten:



1. Verwenden Sie die If-Anweisungen im Controller



function searchSomething (filter, page = 1, size = 10) {
  if (!page) page = 1
  if (!size) size = 10
  // ...
}


Es sieht nicht sehr gut aus und ist ziemlich unpraktisch.



2. Verwenden Sie JSON-Schemas  direkt auf der Route.



Auch hier können wir AJV oder @ expresso / validator verwenden, um diese Daten zu validieren:



app.get('/foo', (req, res) => {
  const schema = {
    type: 'object',
    properties: {
      page: { type: 'number', default: 1 },
      size: { type: 'number', default: 10 },
    },
    additionalProperties: false
  }
 
<a href=""></a>  const valid = ajv.validate(schema, req.params)
    if (!valid) return res.status(422).json(ajv.errors)
    // ...
})


Arbeiten mit Null- und undefinierten Werten



Ich persönlich bin aus mehreren Gründen nicht zufrieden mit der Idee, sowohl null als auch undefiniert in JavaScript zu verwenden, um zu beweisen, dass der Wert leer ist. Zusätzlich zu den Schwierigkeiten, diese Konzepte auf die abstrakte Ebene zu bringen, sollte man optionale Parameter nicht vergessen. Wenn Sie immer noch Zweifel an diesen Konzepten haben, möchte ich Ihnen ein gutes Beispiel aus der Praxis geben:







Nachdem wir die Definitionen verstanden haben, können wir sagen, dass JavaScript im Jahr 2020 zwei wichtige Funktionen enthalten wird: den Null- Koaleszenz- Operator und optionale Verkettung. Ich werde jetzt nicht auf Details eingehen, da ich  bereits einen Artikel darüber geschrieben habe. (es ist auf Portugiesisch), aber beachten Sie, dass diese beiden Innovationen unsere Aufgabe erheblich vereinfachen werden, da wir uns auf diese beiden Konzepte konzentrieren können, null und undefiniert mit einem geeigneten Operator (??), anstatt logische Negative wie! obj zu verwenden das sind fruchtbare Gründe für Fehler.



Funktionen, die implizit null zurückgeben



Dieses Problem ist aufgrund seiner impliziten Natur viel schwieriger zu lösen. Einige Funktionen verarbeiten Daten unter der Annahme, dass sie immer bereitgestellt werden. In einigen Fällen ist dies jedoch nicht der Fall. Betrachten wir ein Standardbeispiel:



function foo (num) {
  return 23*num
}


Wenn num null ist, ist das Ergebnis dieser Funktion 0, was nicht erwartet wurde. In solchen Fällen haben wir keine andere Wahl, als den Code zu testen. Es gibt zwei Arten von Tests, die durchgeführt werden können. Die erste besteht darin, eine einfache if-Anweisung zu verwenden:



function foo (num) {
  if (!num) throw new Error('Error')
  return 23*num
}


Der zweite Weg ist die Verwendung der Entweder- Monade , die in dem von mir erwähnten Artikel ausführlich behandelt wird. Dies ist eine großartige Möglichkeit, um mehrdeutige Daten zu verarbeiten, dh Daten, die möglicherweise null sind oder nicht. Dies liegt daran, dass JavaScript bereits über eine integrierte Funktion verfügt, die zwei Aktionsströme unterstützt: Versprechen:



function exists (value) {
  return x != null ? Promise.resolve(value) : Promise.reject(`Invalid value: ${value}`)
}
 
async function foo (num) {
  return exists(num).then(v => 23 * v)
}


Auf diese Weise können Sie die catch-Anweisung von exist an die Funktion delegieren, die foo aufgerufen hat:



function init (n) {
  foo(n)
    .then(console.log)
    .catch(console.error)
}
 
init(12) // 276
init(null) // Invalid value: null


Externe APIs und Datenbankeinträge



Dies ist ein sehr häufiger Fall, insbesondere wenn es Systeme gibt, die aus Datenbanken entwickelt wurden, die zuvor erstellt oder gefüllt wurden. Zum Beispiel ein neues Produkt, das dieselbe Datenbank wie sein erfolgreicher Vorgänger verwendet und dabei Benutzer aus verschiedenen Systemen integriert, und so weiter.



Das große Problem dabei ist nicht die Tatsache, dass die Datenbank unbekannt ist - tatsächlich ist dies der Grund, da wir nicht wissen, was auf Datenbankebene getan wurde, und wir können nicht bestätigen, ob wir Daten mit dem Wert null oder undefiniert erhalten oder nicht. ... Wir können nur über eine Dokumentation von schlechter Qualität sprechen, wenn die Datenbank nicht ordnungsgemäß dokumentiert ist und wir das gleiche Problem wie zuvor haben.



Wir können hier fast nichts tun, und ich persönlich bevorzuge es, den Status der Daten zu überprüfen, um sicherzustellen, dass ich damit arbeiten kann. Sie können jedoch nicht alle Daten überprüfen, da viele der zurückgegebenen Objekte möglicherweise einfach zu groß sind. Daher wird empfohlen, vor dem Ausführen von Vorgängen die am Betrieb der Funktion beteiligten Daten, z. B. eine Karte oder einen Filter, zu überprüfen, um sicherzustellen, ob sie undefiniert sind oder nicht.



Fehler generieren



Es wird empfohlen, Assertionsfunktionen  für Datenbanken und externe APIs zu verwenden. Im Wesentlichen geben diese Funktionen gegebenenfalls Daten zurück, und andernfalls wird ein Fehler generiert. Der häufigste Anwendungsfall für diese Art von Funktion ist, wenn wir eine API haben, um beispielsweise nach einem bestimmten Datentyp anhand des Bezeichners zu suchen, der bekannten findById:



async function findById (id) {
  if (!id) throw new InvalidIDError(id)
 
  const result = await entityRepository.findById(id)
  if (!result) throw new EntityNotFoundError(id)
  return result
}


Ersetzen Sie die Entität durch den Namen Ihrer Entität, z. B. UserNotFoundError.



Dies ist gut, da wir eine Funktion innerhalb desselben Controllers haben können, um Benutzer anhand der ID zu finden, und eine andere Funktion, die diesen Benutzer verwendet, um andere Daten zu finden, z. B. die Profile dieses Benutzers in einer anderen Sammlung von Datenbanken. Beim Aufrufen der Profilsuchfunktion verwenden wir Assertion, um sicherzustellen, dass der Benutzer tatsächlich in unserer Datenbank vorhanden ist. Andernfalls wird die Funktion nicht einmal ausgeführt und Sie können den Fehler direkt auf der Route suchen:



async function findUser (id) {
  if (!id) throw new InvalidIDError(id)
 
  const result = await userRepository.findById(id)
  if (!result) throw new UserNotFoundError(id)
  return result
}
 
async function findUserProfiles (userId) {
  const user = await findUser(userId)
 
  const profile = await profileRepository.findById(user.profileId)
  if (!profile) throw new ProfileNotFoundError(user.profileId)
  return profile
}


Beachten Sie, dass wir keinen Datenbankaufruf durchführen, wenn der Benutzer nicht vorhanden ist, da die erste Funktion sicherstellt, dass der Benutzer vorhanden ist. Jetzt können wir so etwas auf der Route machen:



app.get('/users/{id}/profiles', handler)
 
// --- //
 
async function handler (req, res) {
  try {
    const userId = req.params.id
    const profile = await userService.getProfile(userId)
    return res.status(200).json(profile)
  } catch (e) {
    if (e instanceof UserNotFoundError || e instanceof ProfileNotFoundError) return res.status(404).json(e.message)
    if (e instanceof InvalidIDError) return res.status(400).json(e.message)
  }
}


Wir können den zurückgegebenen Fehlertyp ermitteln, indem wir einfach den Instanznamen der vorhandenen Fehlerklasse überprüfen.



Fazit



Es gibt verschiedene Möglichkeiten, Daten zu verarbeiten, um einen kontinuierlichen und vorhersehbaren Informationsfluss sicherzustellen. Kennst du noch andere Tipps ?! Lass sie in den Kommentaren



Gefällt dir das Material ?! Möchten Sie Ratschläge geben, eine Meinung äußern oder einfach nur Hallo sagen? So finden Sie mich in den sozialen Medien:








Dieser Artikel wurde ursprünglich von Lucas Santos auf dev.to veröffentlicht . Wenn Sie Fragen oder Kommentare zum Thema des Artikels haben, posten Sie diese unter dem Originalartikel auf dev.to.



All Articles