Portieren von APIs nach TypeScript als Problemlöser

Das React-Frontend des Execute-Programms wurde von JavaScript in TypeScript konvertiert . Aber das in Ruby geschriebene Backend hat sich nicht berührt. Die mit diesem Backend verbundenen Probleme ließen die Projektentwickler jedoch über einen Wechsel von Ruby zu TypeScript nachdenken. Die Übersetzung des Materials, das wir heute veröffentlichen, widmet sich der Geschichte der Portierung des Execute Program-Backends von Ruby auf TypeScript und den damit verbundenen Problemen.



Bei Verwendung des Ruby-Backends vergessen wir manchmal, dass eine Eigenschaft der API ein Array von Zeichenfolgen speichert, keine einfache Zeichenfolge. Manchmal haben wir ein API-Fragment geändert, auf das an verschiedenen Stellen zugegriffen wurde, aber vergessen, den Code an einer dieser Stellen zu aktualisieren. Dies sind die üblichen Probleme einer dynamischen Sprache, die für jedes System charakteristisch sind, dessen Code nicht zu 100% von Tests abgedeckt wird. (Dies ist zwar seltener der Fall, wenn der Code vollständig durch Tests abgedeckt ist.)

Gleichzeitig sind diese Probleme aus dem Frontend verschwunden, seit wir ihn auf TypeScript umgestellt haben. Ich habe mehr Erfahrung in der Serverprogrammierung als im Client, aber trotzdem habe ich mehr Fehler gemacht, wenn ich mit dem Backend und nicht mit dem Frontend gearbeitet habe. All dies deutete darauf hin, dass das Backend auch in TypeScript konvertiert werden sollte.

Ich habe das Backend im März 2019 in ungefähr 2 Wochen von Ruby auf TypeScript portiert. Und alles hat so funktioniert, wie es sollte! Wir haben am 14. April 2019 einen neuen Code in der Produktion bereitgestellt. Es war eine Beta-Version, die nur einer begrenzten Anzahl von Benutzern zur Verfügung stand. Danach brach nichts mehr. Die Benutzer haben nicht einmal etwas bemerkt. Hier ist eine Grafik, die den Zustand unserer Codebasis vor und unmittelbar nach dem Übergang zeigt. Die x-Achse repräsentiert die Zeit (in Tagen), die y-Achse repräsentiert die Anzahl der Codezeilen.


Übersetzen des Frontends von JavaScript nach TypeScript und Übersetzen des Backends von Ruby nach TypeScript

Während des Portierungsprozesses habe ich eine große Menge an Hilfscode geschrieben. Wir haben also ein eigenes Tool zum Ausführen von Tests mit einem Volumen von 200 Zeilen. Wir haben eine 120-Zeilen-Bibliothek für die Arbeit mit der Datenbank sowie eine größere Routing-Bibliothek für die API, die den Front-End- und Back-End-Code verbindet.

In unserer eigenen Infrastruktur ist der Router das Interessanteste. Es ist ein Wrapper für Express, der die korrekte Anwendung der Typen sicherstellt, die sowohl im Client- als auch im Servercode verwendet werden. Dies bedeutet, dass wenn sich ein Teil der API ändert, der andere nicht einmal kompiliert wird, ohne Änderungen daran vorzunehmen, um die Unterschiede zu beseitigen.

Hier ist ein Backend-Handler, der eine Liste von Blog-Posts zurückgibt. Dies ist eines der einfachsten ähnlichen Codefragmente im System:

router.handleGet(api.blog, async () => {
  return {
    posts: blog.posts,
  }
})

Wenn wir den Schlüsselnamen postsin ändern blogPosts, wird ein Kompilierungsfehler angezeigt, dessen Text unten angezeigt wird (der Kürze halber werden hier Informationen zu den Objekttypen weggelassen.)

Property 'posts' is missing in type '...' but required in type '...'.

Jeder Endpunkt wird durch ein Ansichtsobjekt definiert api.someNameHere. Dieses Objekt wird von Client und Server gemeinsam genutzt. Beachten Sie, dass Typen in der Handlerdeklaration nicht direkt erwähnt werden. Sie werden alle aus dem Argument abgeleitet api.blog.

Dieser Ansatz funktioniert für einfache Endpunkte, wie den oben beschriebenen Endpunkt blog. Es eignet sich jedoch für komplexere Endpunkte. Beispielsweise verfügt eine Endpunkt-API für die Arbeit mit Lektionen über einen tief verschachtelten Schlüssel eines logischen Typs .lesson.steps[index].isInteractive. Dank alledem ist es jetzt unmöglich, folgende Fehler zu machen:

  • Wenn wir versuchen, auf isinteractiveden Client zuzugreifen oder einen solchen Schlüssel vom Server zurückzugeben, wird der Code nicht kompiliert. Der Schlüsselname sollte isInteractivemit einem Großbuchstaben aussehen I.
  • isInteractive — .
  • isInteractive number, , , .
  • API, , isInteractive — , , , , , , , .

Beachten Sie, dass dies alles die Codegenerierung umfasst. Dies geschieht mit io-ts und ein paar hundert Codezeilen von unserem eigenen Router.

Das Deklarieren von API-Typen erfordert zusätzliche Arbeit, die Arbeit ist jedoch einfach. Wenn wir die Struktur der API ändern, müssen wir wissen, wie sich die Struktur des Codes ändert. Wir nehmen Änderungen an den API-Deklarationen vor und der Compiler verweist uns dann auf alle Stellen, an denen der Code repariert werden muss.

Es ist schwierig, die Bedeutung dieser Mechanismen zu erkennen, bis Sie sie für eine Weile verwenden. Wir können große Objekte von einer Stelle in der API an eine andere verschieben, die Schlüssel umbenennen, große Objekte in Teile aufteilen, kleine Objekte in ein Objekt zusammenführen, ganze Endpunkte aufteilen oder zusammenführen. Und wir können dies alles tun, ohne uns Gedanken darüber zu machen, dass wir vergessen haben, die entsprechenden Änderungen am Client- oder Servercode vorzunehmen.

Hier ist ein echtes Beispiel. Ich habe vor kurzem ungefähr 20 Stunden an vier freien Tagen damit verbracht, das API Execute-Programm neu zu gestalten . Die gesamte Struktur der API hat sich geändert. Beim Vergleich des neuen Client- und Servercodes mit dem alten wurden Zehntausende von Zeilenänderungen aufgezeichnet. Ich habe den serverseitigen Routing-Code neu gestaltet (wie oben)handleGet) Ich habe alle Typdeklarationen für die API neu geschrieben und dabei viele strukturelle Änderungen vorgenommen. Außerdem habe ich alle Teile des Clients neu geschrieben, in denen die geänderten APIs aufgerufen wurden. Während dieser Arbeit wurden 246 der 292 Quelldateien geändert.

Bei den meisten Arbeiten habe ich mich nur auf ein Typsystem verlassen. In der letzten Stunde dieses 20-Stunden-Falls begann ich mit der Durchführung von Tests, die größtenteils erfolgreich endeten. Ganz am Ende haben wir eine ganze Reihe von Tests durchgeführt und drei kleine Fehler gefunden.

Dies waren alles logische Fehler: Bedingungen, die das Programm versehentlich an den falschen Ort führten. In der Regel hilft ein Typsystem nicht beim Auffinden solcher Fehler. Es dauerte einige Minuten, um diese Fehler zu beheben. Diese neu gestaltete API wurde vor einigen Monaten bereitgestellt. Wenn Sie etwas weiterlesenUnsere Website - Diese API gibt relevante Materialien heraus.

Dies bedeutet nicht, dass das statische Typsystem garantiert, dass der Code immer korrekt ist. Dieses System erlaubt es nicht, auf Tests zu verzichten. Das Refactoring wird jedoch erheblich vereinfacht.

Ich erzähle Ihnen von der automatischen Codegenerierung. Wir verwenden nämlich Schemata , um Typdefinitionen aus der Struktur unserer Datenbank zu generieren. Das System stellt eine Verbindung zur Postgres-Datenbank her, analysiert die Spaltentypen und schreibt die entsprechenden TypeScript-Typdefinitionen in die .d.tsvon der Anwendung verwendete reguläre Datei .

Eine Datei mit Datenbankschematypen wird bei jedem Start von unserem Migrationsskript auf dem neuesten Stand gehalten. Aus diesem Grund müssen wir diese Typen nicht manuell unterstützen. Modelle verwenden Definitionen von Datenbanktypen, um sicherzustellen, dass der Anwendungscode korrekt auf alle in der Datenbank gespeicherten Daten zugreift. Es gibt keine fehlenden Tabellen, keine fehlenden Spalten oder Einträge nullin nicht unterstützenden Spalten null. Wir erinnern uns, nullin unterstützenden Spalten korrekt zu verarbeiten null. Und all dies wird beim Kompilieren statisch überprüft.

All dies zusammen schafft eine zuverlässige statisch typisierte Kette der Informationsübertragung, die sich von der Datenbank bis zu den Eigenschaften der React-Komponenten im Frontend erstreckt:

  • , ( API) , .
  • API , API, ( ) .
  • React- , API, .

Während ich an diesem Material arbeitete, konnte ich mich nicht an einen einzigen Fall von Inkonsistenz im Code erinnern, der der API zugeordnet war, die die Kompilierung bestanden hat. Wir hatten keine Produktionsfehler, die auftraten, weil der mit der API verbundene Client- und Servercode unterschiedliche Vorstellungen über das Datenformular hatte. Und das alles ist nicht das Ergebnis automatisierter Tests. Wir schreiben für die API selbst keine Tests.

Dies bringt uns in eine äußerst angenehme Position: Wir können uns auf die wichtigsten Teile der Anwendung konzentrieren. Ich verbringe sehr wenig Zeit mit Typkonvertierungen. Viel weniger als ich damit verbracht habe, die Ursachen für verwirrende Fehler zu identifizieren, die durch in Ruby oder JavaScript geschriebene Codeebenen drangen und dann seltsame Ausnahmen verursachten, die weit von der Fehlerquelle entfernt waren.

So sieht das Projekt aus, nachdem das Backend in TypeScript übersetzt wurde. Wie Sie sehen können, wurde seit dem Übergang viel Code geschrieben. Wir hatten genug Zeit, um die Konsequenzen der Entscheidung zu bewerten.


TypeScript wird im Frontend und Backend des Projekts verwendet.

Hier haben wir nicht die übliche Frage für solche Veröffentlichungen aufgeworfen, nämlich die gleichen Ergebnisse nicht durch Tippen, sondern durch Verwendung von Tests zu erzielen. Solche Ergebnisse können nicht nur mit Tests erzielt werden. Wir werden wahrscheinlich mehr darüber sprechen.

Liebe Leser! Haben Sie Projekte, die in anderen Sprachen geschrieben wurden, in TypeScript übersetzt?


All Articles