hmm: NodeJS server.js Datei aufräumen

Hi Leute,

ich habe in meinem Node JS Projekt eine einzige server.js Datei die das gesamte Routing regelt. Weil diese Datei immer unsauberer aussieht möchte ich diese jetzt aufräumen. Sollte ich die einzelnen "sendFile"-Funktionen in unterschiedliche Datein packen? Habt ihr eine Idee wie ich den folgenen Code Step bei Step übersichtlicher gestalten kann? Welche Regeln sollte ich umsetzen, wenn ich den code übersichtlicher haben möchte? Ich dachte auch daran mir ein JavaScript Objekt für die Datenbank zu schreiben, leider weiß ich nicht wie man ein solches Objekt sinnvoll anlegt, bzw. ob ein solches Objekt SQL Querys als String kriegen sollte oder ob es nur parameter kriegen sollte.

var express = require('express');
var session = require('express-session');
var app = express();
var myParser = require("body-parser");

app.use(myParser.urlencoded({extended : true}));
app.use(session({
    secret: '2C44-4D44-WppQ38S',
    resave: true,
    saveUninitialized: true
}));

//Specify a port
var port = process.env.port || 8080;

//Serve up files
app.use("/ressourcen",  express.static(__dirname + '/ressourcen'));
app.use("/js", express.static(__dirname + '/js'));
app.use("/pivottable", express.static(__dirname + '/pivottable'));

app.get('/', function(req, res){
    res.sendfile('main.html', { root: __dirname + "/html" } );
});

app.get('/registrieren', function(req, res) {
    res.sendfile('registrieren.html', { root: __dirname + "/html" } );
});

app.get('/datenschutz', function(req, res) {
    res.sendfile('datenschutz.html', { root: __dirname + "/html" } );
});

app.get('/einstellungen', function(req, res) {
    if(req.session.email) {
        res.sendfile('einstellungen.html', { root: __dirname + "/html" } );
    } else {
         res.sendfile('main.html', { root: __dirname + "/html" } );
    }
});

app.get('/team', function(req, res) {
    if(req.session.email) {
        res.sendfile('team.html', { root: __dirname + "/html" } );
    } else {
         res.sendfile('main.html', { root: __dirname + "/html" } );
    }
});

app.get('/mitarbeiter', function(req, res) {
    if(req.session.email) {
        res.sendfile('mitarbeiter.html', { root: __dirname + "/html" } );
    } else {
         res.sendfile('main.html', { root: __dirname + "/html" } );
    }
});

app.get('/kontakt', function(req, res) {
    res.sendfile('kontakt.html', { root: __dirname + "/html" } );
});

app.get('/dashboard', function(req, res) {
    if(req.session.email) {
        res.sendfile('/pivottable/examples/d3.html', { root: __dirname + "/" } );
    } else {
         res.sendfile('main.html', { root: __dirname + "/html" } );
    }
});

app.post('/login', function(req, res){
    var sqlite3 = require('sqlite3').verbose();
    var db = new sqlite3.Database('skills.db');

    db.each("select count(*) as count from user_info where email='" + req.body.email + "' and passwort='" + 
        req.body.password + "'", function(err, row) {
        if(row.count == 1) {
            req.session.email = req.body.email;
            res.sendfile('/pivottable/examples/d3.html', { root: __dirname + "/" } );
        } else {
            res.sendfile('mainFail.html', { root: __dirname + "/html" } );
        }
    });
    
    db.close();
});

app.get('/logout', function (req, res) {
  req.session.destroy();
  res.sendfile('main.html', { root: __dirname + "/html" } );
});

app.get('/getSkillData', function (req, res) {
    if(req.session.email) {
        var sqlite3 = require('sqlite3').verbose();
        var db = new sqlite3.Database('skills.db');

        var data = db.all("SELECT skill as skill,subskill as subskill, name as name, nivau as nivau FROM skill_data where email='" 
            + req.session.email + "' order by name, skill, subskill", function(err, rows) {
            res.type('json');
            res.send(rows);
        });
        db.close();
    }
});

app.post('/saveSkillData', function(req, res) {
    if(req.session.email) {
        var sqlite3 = require('sqlite3').verbose();
        var db = new sqlite3.Database('skills.db');
    
        db.serialize(function() {
            db.run("delete from skill_data where email = '" + req.session.email + "'");
        });
        
        for(var i = 0; i < req.body.arr.length; i++) {
            if(req.body.arr[i].skill.trim() != "" && req.body.arr[i].subskill.trim() != "" && req.body.arr[i].name.trim() != "" && req.body.arr[i].nivau.trim() != "") {
                db.serialize(function() {
                    db.run("INSERT into skill_data(email, skill, subskill, NAME, nivau) VALUES ('"
                    + req.session.email + "', '" 
                    + req.body.arr[i].skill + "', '"
                    + req.body.arr[i].subskill + "', '"
                    + req.body.arr[i].name + "', '"
                    + req.body.arr[i].nivau
                    + "')");
                });
            }
        }
        db.close();
    }
});

app.post('/registrieren', function(req, res) {
    var sqlite3 = require('sqlite3').verbose();
    var db = new sqlite3.Database('skills.db');

    db.serialize(function() {
        db.run("INSERT into user_info(anrede, vorname, nachname, email, passwort) VALUES ('"
        + req.body.Anrede + "', '" 
        + req.body.Vorname + "', '"
        + req.body.Nachname + "', '"
        + req.body.Email + "', '"
        + req.body.Pass
        + "')");
    });
    
    db.close();
    res.sendfile('registrierenRueckmeldung.html', { root: __dirname + "/html" } );
});

app.post('/regDatenAendern', function(req, res) {
    if(req.session.email) {
        var sqlite3 = require('sqlite3').verbose();
        var db = new sqlite3.Database('skills.db');
    
         db.serialize(function() {
            db.run("delete from user_info where email = '" + req.session.email + "'");
        });
    
        db.serialize(function() {
            db.run("INSERT into user_info(anrede, vorname, nachname, email, passwort) VALUES ('"
            + req.body.Anrede + "', '" 
            + req.body.Vorname + "', '"
            + req.body.Nachname + "', '"
            + req.body.Email + "', '"
            + req.body.Pass
            + "')");
        });
        
        db.close();
        res.send("Das ändern Ihrer Daten war erfolgreich.");
     } else {
          res.sendfile('main.html', { root: __dirname + "/html" } );
     }
});

app.post('/regLoeschen', function(req, res) {
    if(req.session.email) {
        var sqlite3 = require('sqlite3').verbose();
        var db = new sqlite3.Database('skills.db');
    
         db.serialize(function() {
            db.run("delete from user_info where email = '" + req.session.email + "'");
        });
    
        db.close();
        res.send("Ihr Account wurde gelöscht..");
    } else {
         res.sendfile('main.html', { root: __dirname + "/html" } );
    }
});

app.post('/mail', function (req, res) {
	// hier verschicken wir die email
	var nodemailer = require('nodemailer');
    
    var transporterObjectConfig = nodemailer.createTransport({
    host: 'egergrgrgrgr',
    port: '587',
    secure: false,
    requireTLS: true,
    auth: {
    user: 'egrergregrgr',
    pass: 'gergergergergr'
    }
    });
    
	var mailOptions = {
        from: req.body.email, // sender address
        to: 'xxx', // list of receivers
        subject: 'Kontaktanfrage', // Subject line
        text: req.body.message, 
        html: req.body.message 
    };

    transporterObjectConfig.sendMail(mailOptions, function(error, info){
        if(error){
            return console.log(error);
        }
        res.sendfile('kontaktRueckmeldung.html', { root: __dirname + "/html" } );
    });
});


//Start up the website
app.listen(port);
console.log('Listening on port: ', port);
  1. Nur eine Idee: Routingtable vom Code trennen, könnte z.B. so aussehen:

    [/mail]
    http_method = post
    app_method  = Name der Funktion die hierfür zuständig ist
    host = mail.example.com
    port = 587
    wsw = ... 
    

    Was darauf hinausläuft, URLs an Methoden zu binden, da sich ohnehin alles in einer Klasse abspielt. Damit kannst Du auch sämtliche Funktionsargumente vom Code getrennt konfigurierbar machen.

    1. Danke, das klingt sinnvoll!

      Dann würde ich weiterhin alle Routings in meiner server.js machen und die dazugehörigen Funktionen einfach in andere Dateien auslagern?

      macht es Sinn für die Anzahl an Routen zu verringern, z.b statt

      /saveData

      /getData

      nur: /data mit Parameter "get" oder "set" ?

      Sollte ich Funktionen an die Routen binden oder wäre es besser ein Objekt an die Routen zu binden?

      1. macht es Sinn für die Anzahl an Routen zu verringern, z.b statt [..]

        Unbedingt! Du hast einen Statischen Teil, z.B. /foo. Und eine dynamische Komponente, das sind mögliche RequestParameter, custom request header und cookies. Auch die RequestMethod ist eine dynamische Komponente. So muss eine Routingtable nur statische Routen also den statischen Teil halten.

        Sollte ich Funktionen an die Routen binden oder wäre es besser ein Objekt an die Routen zu binden?

        Aus der statischen Routingtable ergibt sich doch ein Objekt: Du bekommst eine Reihe Schlüssel=Wert Paare für sämtliche Eigenschaften die damit konfigurierbar sind. Die an den URL gebundene Methode kriegt wie bisher ein request und ein responseobjekt und zusätzlich übergibst Du die Konfiguration als Objekt. Die Methode kümmert sich dann um die dynamischen Komponenten wie Benutzereingaben, sendet Mails und kommuniziert mit Datenbanken und baut die Response zusammen.

        Viel Erfolg!

        1. Ok, danke. Dann mache ich den dynamischen Teil über eine einzige Route.

          Frage:

          Wie sollte ein Objekt das die Datenbankabfragen regelt gebaut sein? Das Objekt bekommt seine teilweise statische Datenbankverbindung im Konstruktor, dass ist schonmal klar. Aber wie mach ich das mit den SQL Statments, übergebe ich die an das Datenbank Objekt oder bekommt das Datenbank Objekt besser nur Parameter übergeben?

          Also würde ich dem Objekt zb sowas geben:

          Select * from wurstTable where abgelaufen=nicht and hergestellt<>vonVegetariern;

          oder sollte ich lieber eine Funktion im Objekt schreiben, welche die Parameter der where Klausel als Json oder Array entgegen nimmt?

          1. Es gibt verschiedene Möglichkeiten zur Organisation. Ein Data Access Layer (kurz DAL) beschreibt eine dieser Möglichkeiten was darauf hinausläuft dass die App.Methode nur noch reine Datenobjekte zur Schnittstelle (DALClassname.Methodname) bringt oder dort abholt. Auch den Namen der DAL Class könntest Du in einer eigenen Methode deiner App kapseln, so liefert Dir dann z.b. eine eigene Methode

            staled = App.instock({
              Artikelname: 'Frankfurter Würstchen',
              Status:      'stale', // oder Haltbarkeitsdatum
              Hersteller:  'HiPo Tofu Arts'
            });
            

            alle Würste die weg müssen aber noch am Lager sind. Die Funktion selbst definiert ein prepared Statement oder bedient sich eines bereits in der DALKlasse zentral definierten Statements wo nur noch die Argumente einzusetzen sind.

            Und wenn es sich abzeichnet, dass es zuviele Methoden werden, organisiere die in Subklassen.

            1. ok, danke! D.h. ich schreibe eine Route in meiner server.js Datei welche ein Json entgegennimmt und an das Datenbankobjekt weiterleitet. Das Datenbankobjekt schreibe ich dann so, dass es die SQL Statments passend zum JSON zusammenbaut. Wenn ich irgendwann zuviele Methoden brauche zerteile ich das DB Objekt in Subklassen und Nutze gegebenenfalls Vererbung.

              Ich habe vor den Code meiner Webseite step by step aufzuräumen. Ich habe dafür folgende Idee:

              1. Start der Webseite wie gehabt per node server.js

              2. In der Datei server.js wird ein Objekt MainServer initialisiert. Dieses Objekt initialisiert ein Objekt Datenbankmanager.

              3. Jede Statische HTML Seite bekommt eine eigene Route. Die Routen aller Statischen HTML Seiten verweisen auf eine Methode des MainServer objekts, nämlich auf die Methode "statischesHtml(pfadZurHtmlDatei)". Diese Methode schmeißt dann die Entsprechenden HTML Seiten mit sendfile.

              4. Es gibt eine Route für alle Datenbank Zugriffe, diese Route verweist auf eine Methode des MainServer Objekts. Diese methode bekommt ein passend Konfiguriertes Json und reagiet entsprechend darauf.

              Damit stelle ich dann auch sicher, dass nur eine Referenz eines Objekts alle Datenbank Sachen macht, d.h. ich habe immernur eine Connection offen.

              Frage:

              Meine Seite erstellt SVG's anhand bestimmter Parameter. D.h. die Url

              webseite/svg?daten=2&farbe=weiss

              führt stets zum gleichen SVG. Geht jetzt ein User mit dieser URl auf meine Unterseite /svg, so hole ich mir seine Parameter derzeit Clientseitig um Clientseitig das SVG zu erstellen. D.h. der Workflow wäre

              Route wird angesprochen und aktiviert die MainServer Methode

              -> die MainServer Methode schmeißt die Html Seite svg.html

              -> Clientseitig startet jetzt ein JavaScript Code auf der svg.html. Dieser holt sich zuerst die Daten indem er meine Daten-Route per ajax anspricht, dann holt er sich die parameter aus der URl und fügt diese zusammen mit den Daten aus der Datenbank in den SVG Generator ein welcher daraufhin dass SVG Generiert und anzeigt.

              Macht das so sinn oder sollte ich etwas ändern?

              1. Wenn Du statische Routen und dynamischen Anteil trennst noch ein Hinweis: Stelle sicher dass in jedem Fall, also mit oder ohne dynamischen Anteil im Request, in fakt bei einem Request auf /svg ohne jegliche Parameter, eine Antortseite geliefert wird. Das heißt, dass erst in der an den URL /svg gebundenen Methode festgestellt wird, ob Parameter im Request sind oder nicht oder eine bestimmte Request-Methode verwendet wurde usw. Dafür hast Du ja in jeder Methode ein request Objekt was Du hierzu befragen kannst.

                Das ist einer der Nachteile einer URL <=> Method Bindung der erst mit einer URL <=> Class Bindung überwunden werden kann. So können nämlich sämtliche Methoden denselben Namen haben was den Bau eines Interfaces ermöglicht, womit der Ablauf gleichnamiger Methoden (nur der Classname ist ein Anderer) ja bei jedem Request derselbe ist und lediglich der dynamische Anteil einer Route (Parameter) zu einer Verzweigung führt womit eine andere Methode aufgerufen wird. Ob Node.js jedoch eine URL-Klassenbindung unterstützt, kann ich nicht beurteilen.

                Evntl. ist es mit Node.js gar möglich die Bindung als URL => ClassName.MethodName also full qualified vorzunehmen was die Übersicht verbessert. Die Wiederverwendbarkeit bestimmter Methoden kannst Du auf jeden Fall auch mit einer reinen Methodenbindung erreichen, bspw. so dass alle URLs die nicht interaktiv sind, also keine Parameter erwarten, allesamt namentlich einunddieselbe Methode aufrufen wobei nur ein bestimmtes Attribut darüber bestimmt wo der Inhalt herkommt.

                MfG

  2. Schau dir mal express' Middlewares an.

    Du hast sehr viel redundanten Code. Zum Beispiel dürften sich alle deine res.sendfile-Aufrufe eliminieren lassen, indem du die dafür vorgesehene express.static()-Middleware benutzt, sowie du es am Anfang ja auch ein paar mal gemacht hast.

    Ein anderes Beispiel: du prüfst wieder und wieder ob eine Email-Adresse in der Session hinterlegt ist if(req.session.email), lass das einmal von einer Middleware erledigen.

    Datenbank-Abfragen und das Zusasammenstricken einer HTTP-Anwort sind ebenfalls Dinge, die sich gut durch Middlewares separieren lassen.

    Dann gliedere alle deine Middlewares in eigene Node.js-Module, was bleibt ist eine sehr ürbersichtliche server.js, die nur deine verschiedenen Middlewares miteinander verwebt. Das ist der defakto-Standard, nach dem man express-Apps baut.