German (Deutsch) translation by Władysław Łucyszyn (you can also view the original English article)
In diesem Artikel erstellen wir eine komplette Website mit einer Front-zugewandten Client-Seite sowie einem Control Panel zur Verwaltung des Seiteninhalts. Wie Sie sich denken können, enthält die letzte Arbeitsversion der Anwendung viele verschiedene Dateien. Ich schrieb dieses Tutorial Schritt für Schritt und folgte dem Entwicklungsprozess, aber ich habe nicht jede einzelne Datei hinzugefügt, da dies ein sehr langes und langweiliges Lesen wäre. Der Quellcode ist jedoch auf GitHub verfügbar und ich empfehle dringend, dass Sie einen Blick darauf werfen.
Einführung
Express ist eines der besten Frameworks für Node. Es hat großartige Unterstützung und eine Reihe hilfreicher Funktionen. Es gibt eine Menge toller Artikel, die alle Grundlagen abdecken. Allerdings möchte ich dieses Mal ein wenig tiefer eintauchen und meinen Workflow für die Erstellung einer vollständigen Website teilen. Im Allgemeinen ist dieser Artikel nicht nur für Express, sondern auch für die Verwendung in Kombination mit anderen großartigen Tools, die für Node-Entwickler verfügbar sind.
Ich nehme an, dass Sie mit Nodejs vertraut sind, es auf Ihrem System installiert haben und dass Sie wahrscheinlich bereits einige Anwendungen damit erstellt haben.
Im Herzen von Express ist Connect. Dies ist ein Middleware-Framework, das mit vielen nützlichen Dingen ausgestattet ist. Wenn Sie sich fragen, was genau eine Middleware ist, hier ein kurzes Beispiel:
var connect = require('connect'), http = require('http'); var app = connect() .use(function(req, res, next) { console.log("That's my first middleware"); next(); }) .use(function(req, res, next) { console.log("That's my second middleware"); next(); }) .use(function(req, res, next) { console.log("end"); res.end("hello world"); }); http.createServer(app).listen(3000);
Middleware ist im Grunde eine Funktion, die request
und response
und eine next
Funktion akzeptiert. Jede Middleware kann entscheiden, mit einem response
objekt zu antworten oder den Fluss an die next
Funktion weiterzuleiten, indem sie den nächsten Callback aufruft. Wenn Sie im obigen Beispiel den Aufruf der Methode next()
in der zweiten Middleware entfernen, wird die Zeichenfolge hello world
niemals an den Browser gesendet. Im Allgemeinen funktioniert Express so. Es gibt einige vordefinierte Middlewares, die Ihnen natürlich viel Zeit ersparen. Wie zum Beispiel der Body parser
, der Anfragekörper analysiert und application / json, application / x-www-form-urlencoded und multipart / form-data unterstützt. Oder der Cookie parser
, der Cookie-Header analysiert und req.cookies
mit einem Objekt füllt, das mit dem Namen des Cookies verknüpft ist.
Express umschließt tatsächlich Connect und fügt einige neue Funktionen hinzu. Wie zum Beispiel Routing-Logik, die den Prozess viel reibungsloser macht. Hier ist ein Beispiel für die Behandlung einer GET-Anfrage:
app.get('/hello.txt', function(req, res){ var body = 'Hello World'; res.setHeader('Content-Type', 'text/plain'); res.setHeader('Content-Length', body.length); res.end(body); });
Konfiguration
Es gibt zwei Möglichkeiten, Express einzurichten. Die erste ist, indem Sie es in Ihre package.json
Datei und laufende npm install
(es gibt einen Witz, der npm
bedeutet kein Problem Mann :)).
{ "name": "MyWebSite", "description": "My website", "version": "0.0.1", "dependencies": { "express": "3.x" } }
Der Code des Frameworks wird in node_modules
platziert und Sie können eine Instanz davon erstellen. Ich bevorzuge jedoch eine alternative Option, indem ich das Befehlszeilentool verwende. Installieren Sie Express einfach global mit npm install -g express
. Dadurch haben Sie jetzt ein brandneues CLI-Instrument. Zum Beispiel, wenn Sie Folgendes ausführen:
express --sessions --css less --hogan app
Express erstellt ein Anwendungsskelett mit einigen bereits für Sie konfigurierten Dingen. Hier sind die Verwendungsoptionen für den Befehl express(1)
:
Usage: express [options] Options: -h, --help output usage information -V, --version output the version number -s, --sessions add session support -e, --ejs add ejs engine support (defaults to jade) -J, --jshtml add jshtml engine support (defaults to jade) -H, --hogan add hogan.js engine support -c, --css add stylesheet support (less|stylus) (defaults to plain css) -f, --force force on non-empty directory
Wie Sie sehen können, gibt es nur ein paar Optionen, aber für mich sind sie genug. Normalerweise verwende ich less als CSS-Präprozessor und hogan als Template-Engine. In diesem Beispiel benötigen wir auch Sitzungsunterstützung, sodass das Argument --sessions
dieses Problem löst. Wenn der obige Befehl abgeschlossen ist, sieht unser Projekt wie folgt aus:
/public /images /javascripts /stylesheets /routes /index.js /user.js /views /index.hjs /app.js /package.json
Wenn Sie die Datei package.json
auschecken, werden Sie sehen, dass alle Abhängigkeiten, die wir benötigen, hier hinzugefügt werden. Obwohl sie noch nicht installiert wurden. Um dies zu tun, führen Sie einfach npm install
aus und dann wird der Ordner node_modules
angezeigt.
Mir ist klar, dass der obige Ansatz nicht immer angemessen ist. Möglicherweise möchten Sie Ihre Route-Handler in einem anderen Verzeichnis oder etwas ähnliches platzieren. Aber wie Sie in den nächsten Kapiteln sehen werden, werde ich Änderungen an der bereits generierten Struktur vornehmen, was ziemlich einfach ist. Man sollte sich also den express(1)
-Befehl als Boilerplatten-Generator vorstellen.
Schnelle Lieferung
Für dieses Tutorial entwarf ich eine einfache Website einer gefälschten Firma namens FastDelivery. Hier ist ein Screenshot des kompletten Designs:



Am Ende dieses Tutorials werden wir eine vollständige Webanwendung mit einem funktionierenden Control Panel haben. Die Idee besteht darin, jeden Teil der Website in separaten eingeschränkten Bereichen zu verwalten. Das Layout wurde in Photoshop erstellt und in CSS (weniger) und HTML (Hogan) -Dateien aufgeteilt. Jetzt werde ich den Schneideprozess nicht behandeln, weil es nicht das Thema dieses Artikels ist, aber wenn Sie irgendwelche Fragen diesbezüglich haben, zögern Sie nicht zu fragen. Nach dem Slicing haben wir folgende Dateien und App-Struktur:
/public /images (there are several images exported from Photoshop) /javascripts /stylesheets /home.less /inner.less /style.css /style.less (imports home.less and inner.less) /routes /index.js /views /index.hjs (home page) /inner.hjs (template for every other page of the site) /app.js /package.json
Hier ist eine Liste der Elemente der Site, die wir verwalten werden:
- Home (das Banner in der Mitte - Titel und Text)
- Blog (Hinzufügen, Entfernen und Bearbeiten von Artikeln)
- Seite Dienste
- Karriereseite
- Kontaktseite
Aufbau
Es gibt ein paar Dinge, die wir tun müssen, bevor wir mit der eigentlichen Implementierung beginnen können. Die Konfiguration ist eine davon. Stellen wir uns vor, dass unsere kleine Website an drei verschiedenen Orten bereitgestellt werden sollte - einem lokalen Server, einem Staging-Server und einem Produktionsserver. Natürlich sind die Einstellungen für jede Umgebung anders und wir sollten einen Mechanismus implementieren, der flexibel genug ist. Wie Sie wissen, wird jedes Knotenscript als Konsolenprogramm ausgeführt. So können wir problemlos Befehlszeilenargumente senden, die die aktuelle Umgebung definieren. Ich habe diesen Teil in ein separates Modul eingepackt, um später einen Test dafür zu schreiben. Hier ist die Datei /config/index.js
:
var config = { local: { mode: 'local', port: 3000 }, staging: { mode: 'staging', port: 4000 }, production: { mode: 'production', port: 5000 } } module.exports = function(mode) { return config[mode || process.argv[2] || 'local'] || config.local; }
Es gibt nur zwei Einstellungen (für jetzt) - mode
und port
. Wie Sie vielleicht vermuten, verwendet die Anwendung verschiedene Ports für die verschiedenen Server. Aus diesem Grund müssen wir den Einstiegspunkt der Website in app.js
aktualisieren.
... var config = require('./config')(); ... http.createServer(app).listen(config.port, function(){ console.log('Express server listening on port ' + config.port); });
Um zwischen den Konfigurationen zu wechseln, fügen Sie einfach die Umgebung am Ende hinzu. Beispielsweise:
node app.js staging
Wird herstellen:
Express server listening on port 4000
Jetzt haben wir alle unsere Einstellungen an einem Ort und sie sind leicht zu verwalten.
Tests
Ich bin ein großer TDD-Fan. Ich werde versuchen, alle in diesem Artikel verwendeten Basisklassen abzudecken. Natürlich wird es zu lange dauern, Tests für absolut alles zu haben, aber im Allgemeinen sollten Sie so vorgehen, wenn Sie Ihre eigenen Apps erstellen. Eines meiner Lieblingsgerüste zum Testen ist jasmine. Natürlich ist es in der npm-Registrierung verfügbar:
npm install -g jasmine-node
Lassen Sie uns ein tests
verzeichnis erstellen, das unsere Tests enthält. Das erste, was wir prüfen werden, ist unser Konfigurationssetup. Die Spezifikationsdateien müssen mit .spec.js
enden, daher sollte die Datei config.spec.js
heißen.
describe("Configuration setup", function() { it("should load local configurations", function(next) { var config = require('../config')(); expect(config.mode).toBe('local'); next(); }); it("should load staging configurations", function(next) { var config = require('../config')('staging'); expect(config.mode).toBe('staging'); next(); }); it("should load production configurations", function(next) { var config = require('../config')('production'); expect(config.mode).toBe('production'); next(); }); });
Führen Sie jasmine-node ./tests
aus und du solltest folgendes sehen:
Finished in 0.008 seconds 3 tests, 6 assertions, 0 failures, 0 skipped
Diesmal schrieb ich die Implementierung zuerst und die Testsekunde. Das ist nicht genau der TDD-Weg, aber in den nächsten paar Kapiteln werde ich das Gegenteil tun.
Ich empfehle dringend, eine Menge Zeit damit zu verbringen, Tests zu schreiben. Es gibt nichts besseres als eine vollständig getestete Anwendung.
Vor ein paar Jahren habe ich etwas sehr Wichtiges erkannt, das dir helfen kann, bessere Programme zu produzieren. Jedes Mal, wenn Sie anfangen, eine neue Klasse, ein neues Modul oder nur eine neue Logik zu schreiben, fragen Sie sich:
Wie kann ich das testen?
Die Antwort auf diese Frage wird Ihnen helfen, viel effizienter zu programmieren, bessere APIs zu erstellen und alles in gut getrennte Blöcke zu setzen. Sie können keine Tests für Spaghetti-Code schreiben. Zum Beispiel in der Konfigurationsdatei oben (/config/index.js
) Ich habe die Möglichkeit hinzugefügt, den mode
im Konstruktor des Moduls zu senden. Sie fragen sich vielleicht, warum ich das mache, wenn die Hauptidee darin besteht, den Modus aus den Befehlszeilenargumenten zu bekommen? Es ist einfach ... weil ich es testen musste. Stellen wir uns vor, dass ich einen Monat später etwas in einer production
konfiguration überprüfen muss, aber das Knotenscript wird mit einem staging
-Parameter ausgeführt. Ohne diese kleine Verbesserung werde ich diese Veränderung nicht vornehmen können. Dieser letzte kleine Schritt verhindert nun tatsächlich Probleme in der Zukunft.
Datenbank
Da wir eine dynamische Website erstellen, benötigen wir eine Datenbank, in der wir unsere Daten speichern können. Ich habe mich für dieses Tutorial für mongodb entschieden. Mongo ist eine NoSQL-Dokumentendatenbank. Die Installationsanleitung kann hier gefunden werden und da ich ein Windows-Benutzer bin, habe ich stattdessen die Windows-Installation verfolgt. Wenn Sie mit der Installation fertig sind, führen Sie den MongoDB-Daemon aus, der standardmäßig Port 27017 überwacht. In der Theorie sollten wir in der Lage sein, eine Verbindung zu diesem Port herzustellen und mit dem mongodb-Server zu kommunizieren. Um dies von einem Node-Skript aus zu tun, benötigen wir einen mongodb-Modul / Treiber. Wenn Sie die Quelldateien für dieses Lernprogramm heruntergeladen haben, wurde das Modul bereits in der Datei package.json
hinzugefügt. Wenn nicht, füge einfach "mongodb": "1.3.10"
zu deinen Abhängigkeiten hinzu und führe npm install
aus.
Als nächstes schreiben wir einen Test, der prüft, ob ein mongodb-Server läuft. /tests/mongodb.spec.js
Datei:
describe("MongoDB", function() { it("is there a server running", function(next) { var MongoClient = require('mongodb').MongoClient; MongoClient.connect('mongodb://127.0.0.1:27017/fastdelivery', function(err, db) { expect(err).toBe(null); next(); }); }); });
Der Rückruf in der Methode .connect
des Mongodb-Clients erhält ein db
-Objekt. Wir werden es später verwenden, um unsere Daten zu verwalten, was bedeutet, dass wir innerhalb unserer Modelle darauf zugreifen müssen. Es ist keine gute Idee, jedes Mal ein neues MongoClient
-Objekt zu erstellen, wenn wir eine Anfrage an die Datenbank stellen müssen. Deshalb habe ich den Aufruf des Express-Servers in den Callback der connect
-Funktion verschoben:
MongoClient.connect('mongodb://127.0.0.1:27017/fastdelivery', function(err, db) { if(err) { console.log('Sorry, there is no mongo db server running.'); } else { var attachDB = function(req, res, next) { req.db = db; next(); }; http.createServer(app).listen(config.port, function(){ console.log('Express server listening on port ' + config.port); }); } });
Noch besser, da wir ein Konfigurations-Setup haben, wäre es eine gute Idee, den mongodb-Host und -Port dort zu platzieren und dann die Verbindungs-URL zu ändern auf:
'mongodb://' + config.mongo.host + ':' + config.mongo.port + '/fastdelivery'
Achten Sie genau auf die Middleware: attachDB
, die ich kurz vor dem Aufruf der http.createServer
-Funktion hinzugefügt habe. Dank dieser kleinen Ergänzung werden wir eine .db
-Eigenschaft des Anforderungsobjekts auffüllen. Die gute Nachricht ist, dass wir während der Routendefinition mehrere Funktionen hinzufügen können. Beispielsweise:
app.get('/', attachDB, function(req, res, next) { ... })
Also ruft Express vorher attachDB
an, um unseren Routen-Handler zu erreichen. Sobald dies geschieht, hat das Anfrageobjekt die Eigenschaft .db
und wir können damit auf die Datenbank zugreifen.
MVC
Wir alle kennen das MVC-Muster. Die Frage ist, wie dies für Express gilt. Mehr oder weniger, es ist eine Frage der Interpretation. In den nächsten Kapiteln werde ich Module erstellen, die als Modell, Ansicht und Controller fungieren.
Model
Das Modell wird mit den Daten umgehen, die in unserer Anwendung enthalten sind. Es sollte Zugriff auf ein DB
-Objekt haben, das von MongoClient
zurückgegeben wird. Unser Modell sollte auch eine Methode haben, es zu erweitern, weil wir vielleicht verschiedene Arten von Modellen erstellen möchten. Zum Beispiel möchten wir vielleicht ein BlogModel
oder ein ContactsModel
. Daher müssen wir eine neue Spezifikation schreiben: /tests/base.model.spec.js
, um diese beiden Modellfunktionen zu testen. Und denken Sie daran, dass wir durch die Definition dieser Funktionalitäten, bevor wir mit der Implementierung beginnen, garantieren können, dass unser Modul nur das macht, was wir wollen.
var Model = require("../models/Base"), dbMockup = {}; describe("Models", function() { it("should create a new model", function(next) { var model = new Model(dbMockup); expect(model.db).toBeDefined(); expect(model.extend).toBeDefined(); next(); }); it("should be extendable", function(next) { var model = new Model(dbMockup); var OtherTypeOfModel = model.extend({ myCustomModelMethod: function() { } }); var model2 = new OtherTypeOfModel(dbMockup); expect(model2.db).toBeDefined(); expect(model2.myCustomModelMethod).toBeDefined(); next(); }) });
Anstelle eines realen DB
-Objekts entschied ich, ein Mockup-Objekt zu übergeben. Das liegt daran, dass ich später etwas Spezifisches testen möchte, das von Informationen abhängt, die aus der Datenbank kommen. Es wird viel einfacher sein, diese Daten manuell zu definieren.
Die Implementierung der extend
-Methode ist etwas knifflig, weil wir den Prototyp von module.exports
ändern müssen, aber den ursprünglichen Konstruktor behalten müssen. Zum Glück haben wir schon einen schönen Test geschrieben, der beweist, dass unser Code funktioniert. Eine Version, die das obige übergibt, sieht so aus:
module.exports = function(db) { this.db = db; }; module.exports.prototype = { extend: function(properties) { var Child = module.exports; Child.prototype = module.exports.prototype; for(var key in properties) { Child.prototype[key] = properties[key]; } return Child; }, setDB: function(db) { this.db = db; }, collection: function() { if(this._collection) return this._collection; return this._collection = this.db.collection('fastdelivery-content'); } }
Hier gibt es zwei Hilfsmethoden. Ein Setter für das DB
-Objekt und ein Getter für unsere Datenbank collection
.
Aussicht
Die Ansicht zeigt Informationen auf dem Bildschirm an. Im Wesentlichen ist die Ansicht eine Klasse, die eine Antwort an den Browser sendet. Express bietet hierfür einen kurzen Weg:
res.render('index', { title: 'Express' });
Das Antwortobjekt ist ein Wrapper, der eine nette API hat, die unser Leben einfacher macht. Allerdings würde ich lieber ein Modul erstellen, das diese Funktionalität kapseln wird. Das Verzeichnis der Standardan views
wird in templates
geändert, und es wird ein neues erstellt, in dem die Base
sichtklasse gehostet wird. Diese kleine Änderung erfordert jetzt eine weitere Änderung. Wir sollten Express mitteilen, dass sich unsere Vorlagendateien jetzt in einem anderen Verzeichnis befinden:
app.set('views', __dirname + '/templates');
Zuerst definiere ich, was ich brauche, schreibe den Test und schreibe danach die Implementierung. Wir benötigen ein Modul, das den folgenden Regeln entspricht:
- Sein Konstruktor sollte ein Antwortobjekt und einen Vorlagennamen erhalten.
- Es sollte eine
render
methode haben, die ein Datenobjekt akzeptiert. - Es sollte erweiterbar sein.
Sie wundern sich vielleicht, warum ich die View
-Klasse erweitere. Ist es nicht einfach die Methode response.render
aufzurufen? In der Praxis gibt es Fälle, in denen Sie einen anderen Header senden oder das response
objekt irgendwie manipulieren möchten. Wie zum Beispiel die Bereitstellung von JSON-Daten:
var data = {"developer": "Krasimir Tsonev"}; response.contentType('application/json'); response.send(JSON.stringify(data));
Anstatt dies jedes Mal zu tun, wäre es nett, eine HTMLView
-Klasse und eine JSONView
-Klasse zu haben. Oder sogar eine XMLView
-Klasse zum Senden von XML-Daten an den Browser. Es ist einfach besser, wenn Sie eine große Website erstellen, solche Funktionalitäten zu integrieren, anstatt den gleichen Code immer wieder zu kopieren.
Hier ist die Spezifikation für die /views/Base.js
:
var View = require("../views/Base"); describe("Base view", function() { it("create and render new view", function(next) { var responseMockup = { render: function(template, data) { expect(data.myProperty).toBe('value'); expect(template).toBe('template-file'); next(); } } var v = new View(responseMockup, 'template-file'); v.render({myProperty: 'value'}); }); it("should be extendable", function(next) { var v = new View(); var OtherView = v.extend({ render: function(data) { expect(data.prop).toBe('yes'); next(); } }); var otherViewInstance = new OtherView(); expect(otherViewInstance.render).toBeDefined(); otherViewInstance.render({prop: 'yes'}); }); });
Um das Rendering zu testen, musste ich ein Mockup erstellen. In diesem Fall habe ich ein Objekt erstellt, das das Antwortobjekt des Express nachahmt. Im zweiten Teil des Tests habe ich eine andere View-Klasse erstellt, die die Basisklasse erbt und eine benutzerdefinierte Rendermethode anwendet. Hier ist die Klasse /views/Base.js
.
module.exports = function(response, template) { this.response = response; this.template = template; }; module.exports.prototype = { extend: function(properties) { var Child = module.exports; Child.prototype = module.exports.prototype; for(var key in properties) { Child.prototype[key] = properties[key]; } return Child; }, render: function(data) { if(this.response && this.template) { this.response.render(this.template, data); } } }
Jetzt haben wir drei Spezifikationen in unserem test
verzeichnis und wenn Sie jasmine-node ./tests
ausführen, sollte das Ergebnis sein:
Finished in 0.009 seconds 7 tests, 18 assertions, 0 failures, 0 skipped
Controller
Erinnerst du dich an die Routen und wie sie definiert wurden?
app.get('/', routes.index);
Das '/'
nach der Route, das im obigen Beispiel der Controller ist. Es ist nur eine Middleware-Funktion, die request
, response
und next
akzeptiert.
exports.index = function(req, res, next) { res.render('index', { title: 'Express' }); };
Oben sollte Ihr Controller im Kontext von Express aussehen. Das Befehlszeilentool express(1)
erstellt ein Verzeichnis namens routes
, aber in unserem Fall ist es besser, es als controllers
zu bezeichnen, also habe ich es so geändert, dass es dieses Benennungsschema widerspiegelt.
Da wir nicht nur eine winzig kleine Anwendung erstellen, wäre es sinnvoll, wenn wir eine Basisklasse erstellen, die wir erweitern können. Wenn wir jemals irgendeine Art von Funktionalität an alle unsere Controller weitergeben müssen, wäre diese Basisklasse der perfekte Ort. Auch hier schreibe ich zuerst den Test, also definieren wir, was wir brauchen:
- Es sollte über eine
extend
Methode verfügen, die ein Objekt akzeptiert und eine neue untergeordnete Instanz zurückgibt - Die untergeordnete Instanz sollte über eine
run
-Methode verfügen, bei der es sich um die alte Middleware-Funktion handelt - Es sollte eine
name
-Eigenschaft geben, die den Controller identifiziert - Wir sollten in der Lage sein, basierend auf der Klasse unabhängige Objekte zu erstellen
Also nur ein paar Dinge für den Moment, aber wir können später mehr Funktionalität hinzufügen. Der Test würde ungefähr so aussehen:
var BaseController = require("../controllers/Base"); describe("Base controller", function() { it("should have a method extend which returns a child instance", function(next) { expect(BaseController.extend).toBeDefined(); var child = BaseController.extend({ name: "my child controller" }); expect(child.run).toBeDefined(); expect(child.name).toBe("my child controller"); next(); }); it("should be able to create different childs", function(next) { var childA = BaseController.extend({ name: "child A", customProperty: 'value' }); var childB = BaseController.extend({ name: "child B" }); expect(childA.name).not.toBe(childB.name); expect(childB.customProperty).not.toBeDefined(); next(); }); });
Und hier ist die Implementierung von /controllers/Base.js
:
var _ = require("underscore"); module.exports = { name: "base", extend: function(child) { return _.extend({}, this, child); }, run: function(req, res, next) { } }
Natürlich sollte jede Kindklasse ihre eigene run
methode zusammen mit ihrer eigenen Logik definieren.
FastDelivery Website
Ok, wir haben eine gute Auswahl an Klassen für unsere MVC-Architektur und haben unsere neu erstellten Module mit Tests abgedeckt. Jetzt sind wir bereit, mit unserer gefälschten Firma FastDelivery
fortzufahren. Stellen wir uns vor, dass die Website aus zwei Teilen besteht - einem Frontend und einem Administrationsbereich. Das Front-End wird verwendet, um die Informationen, die in der Datenbank gespeichert sind, unseren Endbenutzern anzuzeigen. Das Admin-Panel wird verwendet, um diese Daten zu verwalten. Beginnen wir mit unserem Admin (Control) Panel.
Schalttafel
Lassen Sie uns zunächst einen einfachen Controller erstellen, der als Verwaltungsseite dient. /controllers/Admin.js
Datei:
var BaseController = require("./Base"), View = require("../views/Base"); module.exports = BaseController.extend({ name: "Admin", run: function(req, res, next) { var v = new View(res, 'admin'); v.render({ title: 'Administration', content: 'Welcome to the control panel' }); } });
Durch Verwendung der vordefinierten Basisklassen für unsere Controller und Ansichten können wir problemlos den Einstiegspunkt für das Control Panel erstellen. Die View
-Klasse akzeptiert den Namen einer Vorlagendatei. Gemäß dem obigen Code sollte die Datei admin.hjs
heißen und in /templates
abgelegt werden. Der Inhalt würde ungefähr so aussehen:
<!DOCTYPE html> <html> <head> <title>{{ title }}</title> <link rel='stylesheet' href='/stylesheets/style.css' /> </head> <body> <div class="container"> <h1>{{ content }}</h1> </div> </body> </html>
(Um dieses Tutorial relativ kurz und in einem leicht lesbaren Format zu halten, werde ich nicht jede einzelne Ansichtsvorlage zeigen. Ich empfehle dringend, dass Sie den Quellcode von GitHub herunterladen.)
Um den Controller sichtbar zu machen, müssen wir in app.js
eine Route hinzufügen:
var Admin = require('./controllers/Admin'); ... var attachDB = function(req, res, next) { req.db = db; next(); }; ... app.all('/admin*', attachDB, function(req, res, next) { Admin.run(req, res, next); });
Beachten Sie, dass wir die Admin.run
-Methode nicht direkt als Middleware senden. Das liegt daran, dass wir den Kontext beibehalten wollen. Wenn wir das tun:
app.all('/admin*', Admin.run);
Das Wort this
im Admin
weist auf etwas anderes hin.
Schutz des Administrationsbereichs
Jede Seite, die mit /admin
beginnt, sollte geschützt sein. Um dies zu erreichen, verwenden wir Express Middleware: Sessions. Es verbindet einfach ein Objekt mit der Anfrage namens session
. Wir sollten jetzt unseren Admin-Controller ändern, um zwei zusätzliche Dinge zu tun:
- Es sollte prüfen, ob eine Sitzung verfügbar ist. Wenn nicht, dann zeigen Sie ein Login-Formular an.
- Es sollte die Daten akzeptieren, die vom Login-Formular gesendet wurden, und den Benutzer autorisieren, wenn der Benutzername und das Passwort übereinstimmen.
Hier ist eine kleine Hilfsfunktion, die wir verwenden können, um dies zu erreichen:
authorize: function(req) { return ( req.session && req.session.fastdelivery && req.session.fastdelivery === true ) || ( req.body && req.body.username === this.username && req.body.password === this.password ); }
Zuerst haben wir eine Anweisung, die versucht, den Benutzer über das Session-Objekt zu erkennen. Zweitens prüfen wir, ob ein Formular eingereicht wurde. Ist dies der Fall, sind die Daten aus dem Formular im request.body
-Objekt verfügbar, das von der bodyParser
-Middleware gefüllt wird. Dann überprüfen wir nur, ob der Benutzername und das Passwort übereinstimmen.
Und jetzt ist hier die run
-Methode des Controllers, die unseren neuen Helfer verwendet. Wir prüfen, ob der Benutzer berechtigt ist, entweder das Bedienfeld selbst anzuzeigen, ansonsten zeigen wir die Anmeldeseite an:
run: function(req, res, next) { if(this.authorize(req)) { req.session.fastdelivery = true; req.session.save(function(err) { var v = new View(res, 'admin'); v.render({ title: 'Administration', content: 'Welcome to the control panel' }); }); } else { var v = new View(res, 'admin-login'); v.render({ title: 'Please login' }); } }
Inhalt verwalten
Wie ich bereits am Anfang dieses Artikels erwähnt habe, haben wir viele Dinge zu verwalten. Um den Prozess zu vereinfachen, behalten wir alle Daten in einer Sammlung. Jeder Datensatz hat eine title
-, text
-, picture
- und type
genschaft. Die Eigenschaft type
bestimmt den Eigentümer des Datensatzes. Beispielsweise benötigt die Seite "Kontakte" nur einen Datensatz mit dem type: 'contacts'
, während die Seite "Blog" mehr Datensätze benötigt. Wir brauchen also drei neue Seiten zum Hinzufügen, Bearbeiten und Anzeigen von Datensätzen. Bevor wir mit der Erstellung neuer Vorlagen, dem Styling und dem Einbringen neuer Dinge in den Controller beginnen, sollten wir unsere Modellklasse schreiben, die zwischen dem MongoDB-Server und unserer Anwendung steht und natürlich eine sinnvolle API bereitstellt.
// /models/ContentModel.js var Model = require("./Base"), crypto = require("crypto"), model = new Model(); var ContentModel = model.extend({ insert: function(data, callback) { data.ID = crypto.randomBytes(20).toString('hex'); this.collection().insert(data, {}, callback || function(){ }); }, update: function(data, callback) { this.collection().update({ID: data.ID}, data, {}, callback || function(){ }); }, getlist: function(callback, query) { this.collection().find(query || {}).toArray(callback); }, remove: function(ID, callback) { this.collection().findAndModify({ID: ID}, [], {}, {remove: true}, callback); } }); module.exports = ContentModel;
Das Modell erstellt eine eindeutige ID für jeden Datensatz. Wir werden es brauchen, um die Informationen später zu aktualisieren.
Wenn wir einen neuen Datensatz für unsere Kontaktseite hinzufügen möchten, können wir einfach Folgendes verwenden:
var model = new (require("../models/ContentModel")); model.insert({ title: "Contacts", text: "...", type: "contacts" });
Also, wir haben eine nette API, um die Daten in unserer Mongodb-Sammlung zu verwalten. Jetzt sind wir bereit, die Benutzeroberfläche für die Verwendung dieser Funktionalität zu schreiben. Für diesen Teil muss der Admin
-Controller ziemlich geändert werden. Um die Aufgabe zu vereinfachen, habe ich beschlossen, die Liste der hinzugefügten Datensätze und das Formular zum Hinzufügen / Bearbeiten dieser zu kombinieren. Wie Sie auf dem folgenden Screenshot sehen können, ist der linke Teil der Seite für die Liste und der rechte Teil für das Formular reserviert.



Alles auf einer Seite zu haben bedeutet, dass wir uns auf den Teil konzentrieren müssen, der die Seite rendert oder genauer gesagt auf die Daten, die wir an die Vorlage senden. Deshalb habe ich mehrere Hilfsfunktionen erstellt, die wie folgt kombiniert sind:
var self = this; ... var v = new View(res, 'admin'); self.del(req, function() { self.form(req, res, function(formMarkup) { self.list(function(listMarkup) { v.render({ title: 'Administration', content: 'Welcome to the control panel', list: listMarkup, form: formMarkup }); }); }); });
Es sieht ein bisschen hässlich aus, aber es funktioniert so, wie ich es wollte. Der erste Helfer ist eine del
-Methode, die die aktuellen GET-Parameter überprüft und wenn sie action=delete&id=[id of the record]
findet, entfernt sie Daten aus der Sammlung. Die zweite Funktion heißt form
und ist hauptsächlich dafür zuständig, das Formular auf der rechten Seite der Seite anzuzeigen. Es prüft, ob das Formular übermittelt wird und aktualisiert oder erstellt Datensätze in der Datenbank. Am Ende holt die list
methode die Information und erstellt eine HTML-Tabelle, die später an die Vorlage gesendet wird. Die Implementierung dieser drei Helfer finden Sie im Quellcode dieses Tutorials.
Hier habe ich beschlossen, Ihnen die Funktion zu zeigen, die den Datei-Upload übernimmt:
handleFileUpload: function(req) { if(!req.files || !req.files.picture || !req.files.picture.name) { return req.body.currentPicture || ''; } var data = fs.readFileSync(req.files.picture.path); var fileName = req.files.picture.name; var uid = crypto.randomBytes(10).toString('hex'); var dir = __dirname + "/../public/uploads/" + uid; fs.mkdirSync(dir, '0777'); fs.writeFileSync(dir + "/" + fileName, data); return '/uploads/' + uid + "/" + fileName; }
Wenn eine Datei gesendet wird, wird die Node-Skript .files
genschaft des Anforderungsobjekts mit Daten gefüllt. In unserem Fall haben wir das folgende HTML-Element:
<input type="file" name="picture" />
Dies bedeutet, dass wir über req.files.picture
auf die übergebenen Daten zugreifen können. Im obigen Code-Snippet wird req.files.picture.path
verwendet, um den rohen Inhalt der Datei zu erhalten. Später werden dieselben Daten in ein neu erstelltes Verzeichnis geschrieben und am Ende wird eine korrekte URL zurückgegeben. Alle diese Vorgänge sind synchron, aber es empfiehlt sich, die asynchrone Version von readFileSync
, mkdirSync
und writeFileSync
zu verwenden.
Vorderes Ende
Die harte Arbeit ist jetzt abgeschlossen. Der Administrationsbereich funktioniert und wir verfügen über eine
ContentModel
-Klasse, die uns Zugriff auf die in der Datenbank
gespeicherten Informationen gewährt. Jetzt müssen wir die Front-End-Controller schreiben und sie an den gespeicherten Inhalt binden.
Hier ist der Controller für die Homepage - /controllers/Home.js
module.exports = BaseController.extend({ name: "Home", content: null, run: function(req, res, next) { model.setDB(req.db); var self = this; this.getContent(function() { var v = new View(res, 'home'); v.render(self.content); }) }, getContent: function(callback) { var self = this; this.content = {}; model.getlist(function(err, records) { ... storing data to content object model.getlist(function(err, records) { ... storing data to content object callback(); }, { type: 'blog' }); }, { type: 'home' }); } });
Die Startseite benötigt einen Datensatz mit einem home
typ und vier Datensätze mit einem blog
typ. Sobald der Controller fertig ist, müssen wir nur eine Route hinzufügen in app.js
:
app.all('/', attachDB, function(req, res, next) { Home.run(req, res, next); });
Auch hier fügen wir das DB
-Objekt an die request
an. Ziemlich genau der gleiche Workflow wie im Administrationsbereich.
Die anderen Seiten für unser Front-End (Client-Seite) sind fast identisch, da sie alle einen Controller haben, der Daten unter Verwendung der Modellklasse und natürlich einer definierten Route abruft. Es gibt zwei interessante Situationen, die ich ausführlicher erläutern möchte. Der erste bezieht sich auf die Blog-Seite. Es sollte in der Lage sein, alle Artikel zu zeigen, aber auch nur einen zu präsentieren. Also müssen wir zwei Routen registrieren:
app.all('/blog/:id', attachDB, function(req, res, next) { Blog.runArticle(req, res, next); }); app.all('/blog', attachDB, function(req, res, next) { Blog.run(req, res, next); });
Sie verwenden beide den gleichen Controller: Blog
, aber rufen Sie verschiedene run
methoden auf. Achten Sie auf die Zeichenfolge /blog/:id
. Diese Route entspricht URLs wie /blog/4e3455635b4a6f6dccfaa1e50ee71f1cde75222b
und der lange Hash ist in req.params.id
verfügbar. Mit anderen Worten, wir können dynamische Parameter definieren. In unserem Fall ist das die ID des Datensatzes. Sobald wir diese Informationen haben, können wir für jeden Artikel eine einzigartige Seite erstellen.
Der zweite interessante Teil ist, wie ich die Seiten Services, Karriere und Kontakte erstellt habe. Es ist klar, dass sie nur einen Datensatz aus der Datenbank verwenden. Wenn wir für jede Seite einen anderen Controller erstellen müssten, müssten wir denselben Code kopieren und einfügen und nur das type
feld ändern. Es gibt jedoch einen besseren Weg, dies zu erreichen, indem Sie nur einen Controller haben, der den type
in seiner run
-Methode akzeptiert. Also hier sind die Routen:
app.all('/services', attachDB, function(req, res, next) { Page.run('services', req, res, next); }); app.all('/careers', attachDB, function(req, res, next) { Page.run('careers', req, res, next); }); app.all('/contacts', attachDB, function(req, res, next) { Page.run('contacts', req, res, next); });
Und der Controller würde so aussehen:
module.exports = BaseController.extend({ name: "Page", content: null, run: function(type, req, res, next) { model.setDB(req.db); var self = this; this.getContent(type, function() { var v = new View(res, 'inner'); v.render(self.content); }); }, getContent: function(type, callback) { var self = this; this.content = {} model.getlist(function(err, records) { if(records.length > 0) { self.content = records[0]; } callback(); }, { type: type }); } });
Einsatz
Die Bereitstellung einer Express-basierten Website entspricht der Bereitstellung einer anderen Node.js-Anwendung:
- Die Dateien werden auf dem Server platziert.
- Der Knotenprozess sollte gestoppt werden (wenn er ausgeführt wird).
- Ein
npm install
ationsbefehl sollte ausgeführt werden, um die neuen Abhängigkeiten zu installieren (falls vorhanden). - Das Hauptskript sollte dann erneut ausgeführt werden.
Denken Sie daran, dass Node immer noch ziemlich jung ist, also kann nicht alles wie erwartet funktionieren, aber es werden ständig Verbesserungen vorgenommen. Zum Beispiel garantiert forever, dass Ihr Nodejs-Programm kontinuierlich ausgeführt wird. Sie können dies tun, indem Sie den folgenden Befehl ausführen:
forever start yourapp.js
Dies ist es, was ich auch auf meinen Servern verwende. Es ist ein nettes kleines Werkzeug, aber es löst ein großes Problem. Wenn Sie Ihre App nur mit dem Knoten yourapp.js
ausführen, wird der Server nach dem unerwarteten Beenden des Skripts inaktiv. forever
, startet einfach die Anwendung neu.
Now I'm not a system administrator, but I wanted to share my experience integrating node apps with Apache or Nginx, because I think that this is somehow part of the development workflow.
Wie Sie wissen, läuft Apache normalerweise auf Port 80, dh wenn Sie http://localhost
oder http://localhost:80
öffnen, sehen Sie eine Seite, die von Ihrem Apache Server bedient wird und höchstwahrscheinlich Ihr Knotenscript auf a anderer Hafen. Sie müssen also einen virtuellen Host hinzufügen, der die Anforderungen akzeptiert und an den richtigen Port sendet. Angenommen, ich möchte die Website, die wir gerade erstellt haben, auf meinem lokalen Apache-Server unter der Adresse expresscompletewebsite.dev
hosten. Als erstes müssen wir Ihre Domain zur hosts
-Datei hinzufügen.
127.0.0.1 expresscompletewebsite.dev
Danach müssen wir die httpd-vhosts.conf
-Datei im Apache-Konfigurationsverzeichnis bearbeiten und hinzufügen
# expresscompletewebsite.dev <VirtualHost *:80> ServerName expresscompletewebsite.dev ServerAlias www.expresscompletewebsite.dev ProxyRequests off <Proxy *> Order deny,allow Allow from all </Proxy> <Location /> ProxyPass http://localhost:3000/ ProxyPassReverse http://localhost:3000/ </Location> </VirtualHost>
Der Server akzeptiert weiterhin Anforderungen an Port 80, leitet sie jedoch an Port 3000 weiter, wo der Knoten abhört.
Das Nginx-Setup ist viel einfacher und um ehrlich zu sein, ist es eine bessere Wahl für das Hosting von Nodejs-basierten Apps. Sie müssen den Domänennamen in Ihrer hosts
-Datei noch hinzufügen. Erstellen Sie danach einfach eine neue Datei im Verzeichnis /sites-enabled
unter der Nginx-Installation. Der Inhalt der Datei würde ungefähr so aussehen:
server { listen 80; server_name expresscompletewebsite.dev location / { proxy_pass http://127.0.0.1:3000; proxy_set_header Host $http_host; } }
Beachten Sie, dass Sie sowohl Apache als auch Nginx nicht mit dem obigen Host-Setup ausführen können. Das liegt daran, dass beide Port 80 benötigen. Außerdem sollten Sie ein wenig Nachforschungen über eine bessere Serverkonfiguration anstellen, wenn Sie die obigen Code-Snippets in einer Produktionsumgebung verwenden möchten. Wie gesagt, ich bin kein Experte auf diesem Gebiet.
Fazit
Express ist ein großartiger Rahmen, der Ihnen einen guten Ausgangspunkt bietet, um mit dem Erstellen Ihrer Anwendungen zu beginnen. Wie Sie sehen können, ist es eine Frage der Wahl, wie Sie es erweitern und was Sie verwenden werden, um damit zu bauen. Es vereinfacht die langweiligen Aufgaben, indem es ein paar großartige Middlewares benutzt und die spaßigen Teile dem Entwickler überlässt.
Quellcode
Der Quellcode für diese von uns erstellte Beispielwebsite ist auf GitHub verfügbar - https://github.com/tutsplus/build-complete-website-expressjs. Fühlen Sie sich frei, es auszuspannen und damit zu spielen. Hier sind die Schritte zum Ausführen der Site.
- Laden Sie den Quellcode herunter
- Gehe zum
app
-Verzeichnis - Führen Sie
npm install
aus - Führen Sie den mongodb-Daemon aus
- Führen Sie den
node app.js
aus