OK, es ist eine Weile her, und dies ist eine beliebte Frage, so habe ich vorausgegangen und erstellt ein Gerüst github Repository mit JavaScript-Code und eine lange README, wie ich eine mittelgroße express.js-Anwendung zu strukturieren mag.
focusaurus/express_code_structure ist das Repo mit dem neuesten Code dafür. Pull-Anfragen sind willkommen.
Hier ist ein Schnappschuss der README, da Stackoverflow keine Just-a-Link-Antworten mag. Ich werde einige Aktualisierungen vornehmen, da dies ein neues Projekt ist, das ich weiterhin aktualisieren werde, aber letztendlich wird das Github-Repository der aktuelle Ort für diese Informationen sein.
Express-Code-Struktur
Dieses Projekt ist ein Beispiel dafür, wie man eine mittelgroße express.js-Webanwendung organisiert.
Aktuell bis mindestens express v4.14 Dezember 2016
Wie groß ist Ihre Anwendung?
Webanwendungen sind nicht alle gleich, und es gibt meiner Meinung nach keine einheitliche Codestruktur, die auf alle express.js-Anwendungen angewendet werden sollte.
Wenn Ihre Anwendung klein ist, brauchen Sie keine so tiefe Verzeichnisstruktur, wie sie hier gezeigt wird. Halten Sie es einfach und legen Sie eine Handvoll von .js
Dateien im Stammverzeichnis Ihres Repositorys und Sie sind fertig. Voilà.
Wenn Ihre Anwendung sehr groß ist, müssen Sie sie irgendwann in verschiedene npm-Pakete aufteilen. Im Allgemeinen scheint der node.js-Ansatz viele kleine Pakete zu bevorzugen, zumindest für Bibliotheken, und Sie sollten Ihre Anwendung aufbauen, indem Sie mehrere npm-Pakete verwenden, sobald dies Sinn macht und den Overhead rechtfertigt. Wenn Ihre Anwendung wächst und ein Teil des Codes eindeutig außerhalb Ihrer Anwendung wiederverwendbar wird oder ein eindeutiges Subsystem darstellt, verschieben Sie ihn in ein eigenes Git-Repository und machen Sie daraus ein eigenständiges npm-Paket.
Also Der Schwerpunkt dieses Projekts liegt auf der Darstellung einer praktikablen Struktur für eine mittelgroße Anwendung.
Wie sieht Ihre Gesamtarchitektur aus?
Es gibt viele Ansätze für die Erstellung einer Webanwendung, z. B.
- Serverseitiges MVC a la Ruby on Rails
- Einseitige Anwendungen im Stil von MongoDB/Express/Angular/Node (MEAN)
- Einfache Website mit einigen Formularen
- Modelle/Operationen/Anzeigen/Ereignisse a la MVC ist tot, es ist an der Zeit, weiterzumachen
- und viele andere sowohl aktuelle als auch historische
Jedes dieser Verzeichnisse passt gut in eine andere Verzeichnisstruktur. Für die Zwecke dieses Beispiels handelt es sich nur um ein Gerüst und nicht um eine voll funktionsfähige Anwendung, aber ich gehe von den folgenden zentralen Architekturpunkten aus:
- Die Website hat einige traditionelle statische Seiten/Templates
- Der "Anwendungsteil" der Website wurde im Stil einer Single Page Application entwickelt
- Die Anwendung stellt dem Browser eine API im REST/JSON-Stil zur Verfügung
- Die Anwendung modelliert eine einfache Geschäftsdomäne, in diesem Fall eine Autohausanwendung
Und was ist mit Ruby on Rails?
Es wird ein Thema in diesem Projekt sein, dass viele der Ideen, die in Ruby on Rails verkörpert werden, und die "Convention over Configuration"-Entscheidungen, die sie angenommen haben, obwohl sie weithin akzeptiert und verwendet werden, eigentlich nicht sehr hilfreich sind und manchmal das Gegenteil von dem sind, was dieses Repository empfiehlt.
Mein Hauptpunkt ist, dass es grundlegende Prinzipien für die Organisation von Code gibt, und basierend auf diesen Prinzipien machen die Ruby on Rails-Konventionen (meistens) für die Ruby on Rails-Community Sinn. Diese Konventionen einfach gedankenlos zu übernehmen, geht jedoch am Thema vorbei. Sobald Sie die Grundprinzipien verstanden haben, werden ALLE Ihre Projekte gut organisiert und übersichtlich sein: Shell-Skripte, Spiele, mobile Anwendungen, Unternehmensprojekte, sogar Ihr Home-Verzeichnis.
Für die Rails-Community ist es wichtig, dass ein einzelner Rails-Entwickler von einer Anwendung zur nächsten wechseln kann und jedes Mal mit der Anwendung vertraut ist. Dies ist sehr sinnvoll, wenn Sie 37 Signale oder Pivotal Labs sind, und hat Vorteile. In der Welt des serverseitigen JavaScript ist der allgemeine Ethos einfach viel wilder, alles ist möglich, und wir haben kein Problem damit. So sind wir nun mal. Wir sind daran gewöhnt. Selbst innerhalb von express.js ist es ein enger Verwandter von Sinatra, nicht von Rails, und Konventionen von Rails zu übernehmen, ist normalerweise nicht hilfreich. Ich würde sogar sagen Prinzipien vor Konvention vor Konfiguration .
Grundlegende Prinzipien und Motivationen
-
Geistig handhabbar sein
- Das Gehirn kann nur eine kleine Anzahl von zusammenhängenden Dingen auf einmal verarbeiten und darüber nachdenken. Deshalb benutzen wir Verzeichnisse. Sie helfen uns, mit Komplexität umzugehen, indem wir uns auf kleine Teile konzentrieren.
-
Angemessene Größe
- Erstellen Sie keine "Herrenhaus-Verzeichnisse", in denen es nur eine Datei gibt, die 3 Verzeichnisse weiter unten liegt. Sie können dies in der Ansible Bewährte Praktiken die kleine Projekte dazu zwingt, mehr als 10 Verzeichnisse für mehr als 10 Dateien anzulegen, obwohl ein Verzeichnis mit 3 Dateien viel besser geeignet wäre. Sie fahren nicht mit dem Bus zur Arbeit (es sei denn, Sie sind ein Busfahrer, aber selbst dann fahren Sie mit dem Bus zur Arbeit und nicht zur Arbeit), also erstellen Sie keine Dateisystemstrukturen, die nicht durch die tatsächlichen Dateien darin gerechtfertigt sind.
-
Modular, aber pragmatisch sein
- Die Node-Community bevorzugt insgesamt kleine Module. Alles, was sich sauber aus Ihrer Anwendung herauslösen lässt, sollte in ein Modul extrahiert werden, entweder für den internen Gebrauch oder zur Veröffentlichung auf npm. Für die mittelgroßen Anwendungen, um die es hier geht, kann der Overhead jedoch den Arbeitsablauf ermüden, ohne dass dies einen entsprechenden Nutzen bringt. Wenn Sie also etwas Code haben, der herausgefiltert werden soll, aber nicht genug, um ein komplett separates npm-Modul zu rechtfertigen, betrachten Sie es einfach als ein " Proto-Baustein "mit der Erwartung, dass sie bei Überschreiten einer bestimmten Größenschwelle herausgezogen wird.
- Einige Leute wie @hij1nx sogar eine
app/node_modules
Verzeichnis und haben package.json
Dateien im Proto-Baustein Verzeichnisse, um diesen Übergang zu erleichtern und als Erinnerung zu dienen.
-
Code leicht auffindbar sein
- Unser Ziel ist es, dass ein Entwickler bei einer zu erstellenden Funktion oder einem zu behebenden Fehler keine Schwierigkeiten hat, die betreffenden Quelldateien zu finden.
- Die Namen sind aussagekräftig und genau
- lästiger Code vollständig entfernt wird und nicht in einer verwaisten Datei verbleibt oder nur auskommentiert wird
-
Suchfreundlich sein
- Der gesamte Quellcode der Erstanbieter befindet sich in der
app
Verzeichnis, damit Sie cd
dort find/grep/xargs/ag/ack/etc ausführen und sich nicht von den Übereinstimmungen Dritter ablenken lassen
-
Verwenden Sie einfache und eindeutige Bezeichnungen
- npm scheint jetzt alle Paketnamen in Kleinbuchstaben zu verlangen. Ich finde das meist schrecklich, aber ich muss der Herde folgen, also sollten Dateinamen mit
kebab-case
obwohl der Variablenname dafür in JavaScript lauten muss camelCase
denn -
ist ein Minuszeichen in JavaScript.
- der Name der Variablen entspricht dem Basisnamen des Modulpfads, aber mit
kebab-case
umgewandelt in camelCase
-
Gruppierung nach Kopplung, nicht nach Funktion
- Dies ist eine große Abweichung von der Ruby on Rails-Konvention von
app/views
, app/controllers
, app/models
usw.
- Funktionen werden zu einem vollständigen Stapel hinzugefügt, daher möchte ich mich auf einen vollständigen Stapel von Dateien konzentrieren, die für meine Funktion relevant sind. Wenn ich dem Benutzermodell ein Telefonnummernfeld hinzufüge, interessiert mich kein anderer Controller als der Benutzer-Controller, und ich interessiere mich für kein anderes Modell als das Benutzermodell.
- Anstatt also 6 Dateien zu bearbeiten, die sich jeweils in einem eigenen Verzeichnis befinden und tonnenweise andere Dateien in diesen Verzeichnissen zu ignorieren, ist dieses Repository so organisiert, dass alle Dateien, die ich für die Erstellung eines Features benötige, an einem Ort zusammengefasst sind
- Es liegt in der Natur von MVC, dass die Benutzeransicht mit dem Benutzer-Controller gekoppelt ist, der wiederum mit dem Benutzermodell gekoppelt ist. Wenn ich also das Benutzermodell ändere, ändern sich diese 3 Dateien oft gemeinsam, aber der Deals-Controller oder der Customer-Controller sind entkoppelt und daher nicht beteiligt. Dasselbe gilt normalerweise auch für Nicht-MVC-Designs.
- MVC oder MOVE-Stil Entkopplung in Bezug auf die Code geht in welchem Modul wird immer noch gefördert, aber die Verbreitung der MVC-Dateien in Geschwister Verzeichnisse ist nur ärgerlich.
- So hat jede meiner Routendateien den Teil der Routen, der ihr gehört. Ein Schienen-Stil
routes.rb
Datei ist praktisch, wenn man einen Überblick über alle Routen in der App haben möchte, aber wenn man tatsächlich Funktionen entwickelt und Fehler behebt, interessiert man sich nur für die Routen, die für das Stück, das man ändert, relevant sind.
-
Tests neben dem Code speichern
- Dies ist nur ein Beispiel für "Gruppierung durch Kopplung", aber ich wollte es besonders hervorheben. Ich habe viele Projekte geschrieben, bei denen die Tests in einem parallelen Dateisystem mit dem Namen "tests" liegen, und jetzt, wo ich angefangen habe, meine Tests in dasselbe Verzeichnis wie den zugehörigen Code zu legen, werde ich nie wieder zurückgehen. Das ist modularer und in Texteditoren viel einfacher zu handhaben und erleichtert den "../../ "-Pfadunsinn erheblich. Wenn Sie Zweifel haben, probieren Sie es an ein paar Projekten aus und entscheiden Sie selbst. Ich werde nichts weiter tun, um Sie davon zu überzeugen, dass es besser ist.
-
Reduzierung der Querschnittskopplung mit Events
- Es ist einfach zu denken: "OK, immer wenn ein neues Geschäft erstellt wird, möchte ich eine E-Mail an alle Vertriebsmitarbeiter senden", und dann einfach den Code zum Senden dieser E-Mails in die Route zum Erstellen von Geschäften einfügen.
- Diese Kopplung verwandelt Ihre App jedoch letztendlich in einen riesigen Schlammball.
- Stattdessen sollte das DealModel nur ein "create"-Ereignis auslösen und sich nicht bewusst sein, was das System sonst noch als Reaktion darauf tun könnte.
- Wenn Sie auf diese Weise programmieren, ist es viel einfacher, den gesamten benutzerbezogenen Code in
app/users
weil es kein Rattennest von gekoppelter Geschäftslogik gibt, das die Reinheit der Benutzercodebasis verunreinigt.
-
Der Codefluss ist nachvollziehbar
- Machen Sie keine magischen Dinge. Laden Sie keine Dateien aus magischen Verzeichnissen im Dateisystem automatisch. Sei nicht Rails. Die Anwendung beginnt bei
app/server.js:1
und Sie können alles sehen, was geladen und ausgeführt wird, indem Sie dem Code folgen.
- Machen Sie keine DSLs für Ihre Routen. Führen Sie keine alberne Metaprogrammierung durch, wenn sie nicht erforderlich ist.
- Wenn Ihre Anwendung so groß ist, dass sie
magicRESTRouter.route(somecontroller, {except: 'POST'})
ist ein großer Gewinn für Sie über 3 grundlegende app.get
, app.put
, app.del
Wenn Sie mit einer monolithischen Anwendung arbeiten, die zu groß ist, um sie effektiv zu bearbeiten, werden Sie wahrscheinlich eine solche Anwendung bauen. Machen Sie sich schick für große Erfolge, nicht für die Umwandlung von 3 einfachen Zeilen in 1 komplexe Zeile.
-
Dateinamen in Kleinbuchstaben verwenden
-
Verwenden Sie nicht app.configure
. Es ist fast völlig nutzlos, und man braucht es einfach nicht. Er ist in vielen Standardtexten enthalten, weil er sinnlos kopiert wurde.
-
DIE REIHENFOLGE VON MIDDLEWARE UND ROUTEN IN EXPRESS IST WICHTIG!!!
- Fast jedes Routing-Problem, das ich auf Stackoverflow sehe, ist ein Problem mit der Express-Middleware
- Im Allgemeinen möchten Sie, dass Ihre Routen entkoppelt sind und sich nicht so sehr auf die Reihenfolge verlassen
- Verwenden Sie nicht
app.use
für Ihre gesamte Anwendung, wenn Sie diese Middleware wirklich nur für 2 Routen benötigen (ich schaue Sie an, body-parser
)
- Vergewissern Sie sich, dass Sie am Ende GENAU diese Reihenfolge haben:
- Jede superwichtige anwendungsweite Middleware
- Alle Ihre Routen und verschiedene Routen-Middlewares
- THEN Fehlerbehandler
-
Da express.js von Sinatra inspiriert ist, geht es leider davon aus, dass alle Routen in server.js
und es wird klar sein, wie sie angeordnet sind. Für eine mittelgroße Anwendung ist es schön, die Dinge in separate Routenmodule aufzuteilen, aber es birgt die Gefahr einer ungeordneten Middleware
Der App-Symlink-Trick
Es gibt viele Ansätze, die von der Gemeinschaft im großen Gist beschrieben und ausführlich diskutiert werden Bessere lokale require()-Pfade für Node.js . Vielleicht entscheide ich mich bald dafür, entweder "einfach mit vielen ../../../ " umzugehen oder die requireFrom modlue. Im Moment verwende ich jedoch den unten beschriebenen Trick mit den Symlinks.
Eine Möglichkeit zur Vermeidung von projektinternen Anforderungen mit lästigen relativen Pfaden wie require("../../../config")
ist die Anwendung des folgenden Tricks:
- Erstellen Sie einen Symlink unter node_modules für Ihre Anwendung
- cd node_modules && ln -nsf ../app
- hinzufügen. nur der Symlink node_modules/app selbst , nicht den gesamten Ordner node_modules, an git
- git add -f node_modules/app
- Ja, Sie sollten immer noch "node_modules" in Ihrem
.gitignore
Datei
- Nein, Sie sollten "node_modules" nicht in Ihr Git-Repository aufnehmen. Einige Leute empfehlen Ihnen dies zu tun. Sie sind falsch.
- Jetzt können Sie projektinterne Module mit diesem Präfix anfordern
var config = require("app/config");
var DealModel = require("app/deals/deal-model")
;
- Im Grunde genommen funktionieren die projektinternen Requests damit ähnlich wie die Requests für externe npm-Module.
- Tut mir leid, Windows-Benutzer, Sie müssen sich an die relativen Pfade des übergeordneten Verzeichnisses halten.
Konfiguration
Generell sollten Module und Klassen so kodiert werden, dass sie nur ein einfaches JavaScript erwarten options
Objekt übergeben. Nur app/server.js
sollte die app/config.js
Modul. Von dort aus kann es kleine options
Objekte, um Subsysteme nach Bedarf zu konfigurieren, aber jedes Subsystem an ein großes globales Konfigurationsmodul voller zusätzlicher Informationen zu koppeln, ist eine schlechte Kopplung.
Versuchen Sie, die Erstellung von DB-Verbindungen zu zentralisieren und diese an Subsysteme weiterzugeben, anstatt Verbindungsparameter zu übergeben und die Subsysteme selbst ausgehende Verbindungen herstellen zu lassen.
NODE_ENV
Dies ist eine weitere verlockende, aber schreckliche Idee, die von Rails übernommen wurde. Es sollte genau 1 Platz in Ihrer App sein, app/config.js
die sich mit dem NODE_ENV
Umgebungsvariable. Alles andere sollte eine explizite Option als Klassenkonstruktorargument oder Modulkonfigurationsparameter annehmen.
Wenn das E-Mail-Modul eine Option hat, wie E-Mails zugestellt werden sollen (SMTP, nach stdout protokollieren, in eine Warteschlange stellen usw.), sollte es eine Option wie {deliver: 'stdout'}
aber es sollte auf keinen Fall prüfen NODE_ENV
.
Tests
Ich bewahre meine Testdateien jetzt im selben Verzeichnis auf wie den zugehörigen Code und verwende Namenskonventionen für die Dateierweiterung, um die Tests vom Produktionscode zu unterscheiden.
foo.js
hat den Code des Moduls "foo".
foo.tape.js
hat die knotenbasierten Tests für foo und befindet sich im selben Verzeichnis
foo.btape.js
kann für Tests verwendet werden, die in einer Browserumgebung ausgeführt werden müssen
Ich verwende Dateisystem-Globs und die find . -name '*.tape.js'
um bei Bedarf Zugriff auf alle meine Tests zu erhalten.
Wie man den Code innerhalb der einzelnen .js
Moduldatei
Bei diesem Projekt geht es hauptsächlich darum, wo Dateien und Verzeichnisse abgelegt werden, und ich möchte nicht viel mehr dazu sagen, aber ich möchte nur erwähnen, dass ich meinen Code in drei verschiedene Abschnitte einteile.
- Der Eröffnungsblock von CommonJS erfordert Aufrufe zur Angabe von Abhängigkeiten
- Hauptcodeblock aus reinem JavaScript. Hier gibt es keine CommonJS-Verschmutzung. Verweisen Sie nicht auf exports, module oder require.
- Schließen des CommonJS-Blocks zum Einrichten von Exporten