Wprowadzenie do Express/Node

To tłumaczenie jest niepełne. Pomóż przetłumaczyć ten artykuł z języka angielskiego

Znajdziesz tutaj odpowiedzi na pytania "Czym jest Node?" i "Co to jest Express?", oraz zrozumiesz co czyni framework Express wyjątkowym. W dalszej części zostały przedstawione najistotniejsze możliwości wraz z głównymi elementami potrzebnymi do budowy aplikacji webowych w Express (choć jeszcze nie będziesz miał przygotowanego środowiska do tworzenia i testowania takich aplikacji).
Wymagania: Podstawowe umiejętności w posługiwaniu się komputerem. Rozumienie ogólnych zasad programowania po stronie serwera,  w szczególności mechanizmów interakcji między klientem a serwerem aplikacji webowych.
Cel: Zapoznanie się z frameworkiem Express, jego współpracą ze środowiskiem Node, jego funkcjonalnością i głównymi elementami aplikacji tworzonymi w tym frameworku.

Wprowadzenie do Node

Node (lub bardziej formalnie Node.js) jest wieloplatformowym oprogramowaniem o otwartym kodzie, które pozwala deweloperom na tworzenie wszelkiego rodzaju oprogramowania w języku JavaScript pracującym po stronie serwera. Jest to środowisko uruchomieniowe, które działa poza przeglądarką, współpracujące bezpośrednio z systemem operacyjnym. W ten sposób środowisko Node udostępnia swoim aplikacjom API systemu operacyjnego, w tym dostęp do systemu plików, bibliotek systemowych czy uruchomionych procesów, w tym serwerów HTTP.

Z perspektywy tworzenia oprogramowania po stronie serwera, Node ma szereg zalet:

  • Wysoka wydajność! Node zostało zaprojektowane, aby optymalizować wydajność i skalowalność aplikacji webowych i jest dobrym rozwiązaniem wielu często występujących problemów podczas tworzenia aplikacji tego typu.
  • Kod aplikacji to "stary dobry JavaScript", co oznacza, że spędzamy mniej czasu na "przełączanie kontekstu" podczas pracy z różnymi językami po stronie klienta i serwera.
  • JavaScript jest relatywnie nowym językiem programowania i oferuje sporo ulepszeń w porównaniu do innych, tradycyjnie używanych po stronie serwera języków (np. PHP, Python itd.). Także wiele nowszych i popularnych języków jest kompilowanych/tłumaczonych do JavaScript, więc możesz korzystać także z TypeScript, CoffeScript, ClojureScript, Scala.js, LiveScript i innych.
  • Menadżer pakietów środowiska Node (NPM - Node Package Manager) daje Ci dostęp do setek tysięcy pakietów, z których możesz korzystać. Posiada on najlepszy w swojej klasie mechanizm rozwiązywania zależności i może być wykorzystany do automatyzacji pracy narzędzi budujących aplikację.
  • Node.js jest przenośne. Jest dostępne dla Microsoft Widnows, macOs, Linux, Solaris, FreeBSD, OpenBSD, WebOS, and NonStop OS. Jest także wspierane przez dostawców usług sieciowych, którzy zapewniają odpowiednią infrastrukturę i dokumentację do hostowania aplikacji Node.
  • Ma też bardzo aktywny ekosystem firm trzecich i społeczność programistów, którzy gotowi są służyć pomocą.

Możesz teraz wykorzystać Node.js do stworzenia prostego serwera stron WWW posługując się pakietem HTTP.

Hello Node.js

W poniższym przykładzie utworzymy serwer WWW, który będzie oczekiwał dowolnego rodzaju żądań HTTP skierowanych pod URL http://127.0.0.1:8000/ - gdy żądanie zostanie odebrane, skrypt wyśle odpowiedź z łańcuchem "Hello World". Jeśli masz już zainstalowane Node, możesz wykonać poniższe kroki, aby wykonać przykład:

  1. Otwórz terminal (w Windows otwórz wiersz poleceń - cmd.exe).
  2. Utwórz katalog w miejscu, którym chcesz zapisać program, na przykład test-node, a następnie wejdź do nie niego wykonując polecenie w terminalu:
cd test-node
  1. Korzystając z ulubionego edytora tekstu, utwórz plik o nazwie hello.js i wklej do niego poniższy kod:
// Load HTTP module
const http = require("http");

const hostname = "127.0.0.1";
const port = 8000;

// Create HTTP server 
const server = http.createServer((req, res) => {

   // Set the response HTTP header with HTTP status and Content type
   res.writeHead(200, {'Content-Type': 'text/plain'});
   
   // Send the response body "Hello World"
   res.end('Hello World\n');
});

// Prints a log once the server starts listening
server.listen(port, hostname, () => {
   console.log(`Server running at http://${hostname}:${port}/`);
})
  1. Zapisz plik do katalogu, który utworzyłeś powyżej w punkcie 2.
  2. Wróć do terminalu i wpisz następujące polecenie:
node hello.js

W ostatnim kroku otwórz przeglądarkę WWW i wpisz adres http://localhost:8000 ; powinieneś zobaczyć napis "Hello World" w lewy górnym rogu pustej strony.

Frameworki do budowy aplikacji webowych

Inne typowe zadania związane z tworzeniem aplikacji internetowych nie są bezpośrednio wspierane przez samo Node.js. Jeśli chcesz obsługiwać osobno każdą z metod HTTP (np. GET, POST, DELETE itp.), lub obsługiwać żądania w różnych ścieżkach URL („routes”), lub wysyłać statyczne dokumenty (jako pliki HTML) lub korzystać z szablonów do dynamicznego tworzenia odpowiedzi, to będziesz musiał napisać kod samemu. Albo zamiast wymyślać na nowo koło możesz użyć frameworka webowego do tworzenia aplikacji!

Wprowadzenie do Express

Express to jeden z najpopularniejszych frameworków webowych, który jest także wykorzystywany jako biblioteka w wielu innych popularnych frameworkach Node. Dostarcza następujących mechanizmów:

  • Tworzenie funkcji obsługujących żądania o różnych metodach HTTP i skierowanych do różnych ścieżek w URL (tzw. routing).
  • Integrację z różnymi silnikami do generowania widoków, które są tworzone na podstawie osadzanych danych w szablonach stron.
  • Konfigurowania typowych ustawień aplikacji webowych jak np. portu, lokalizacji szablonów do generowania widoków odpowiedzi.
  • Dodatkowe przetwarzanie żądań w warstwie pośredniej (tzw. "middleware"), które może być umieszczone w dowolnym miejscu łańcucha obsługi żądania.

Ponieważ Express jest dość minimalistyczny, dlatego deweloperzy stworzyli wiele kompatybilnych z nim pakietów pracujących w warstwie pośredniej, które rozwiązują prawie każdy problem pojawiający się w aplikacjach webowych. Są dostępne biblioteki do zarządzania ciasteczkami, do pracy w trybie sesji, do logowania użytkowników, do parsowania: parametrów w URL, danych przesyłanych w żądaniach POST, nagłówków zabezpieczeń i wiele innych. Pełną listę bibliotek rozwijanych przez zespół programistów Express znajdziesz na stronie Express Middleware (która zawiera także popularne pakiety firm trzecich).

Uwaga:  Ta elastyczność Express ma dwa różne oblicza. Jest wiele  pakietów warstwy pośredniej, które rozwiązują prawie każdy problem lub wymaganie, ale ich opracowanie jest czasem nie lada wyzwaniem. Nie ma także "jedynie słusznej" struktury aplikacji, wiele dostępnych w Internecie przykładów nie jest optymalnych, lub prezentują tylko niewielki wycinek tego, co musisz zrobić, aby zbudować aplikację webową.

Skąd się wziął Node i Express?

Pierwsza edycja Node została wydana, tylko dla systemu Linux, w 2009 roku. Menadżer pakietów NPM pojawił się w 2010, a natywne wsparcie dla systemu Windows dodano w 2012 roku. Bieżąca edycja LTS Node ma wersję v12.13.1, gdy najnowsza wersja Node ma numer 13.2.0. Jest to tylko krótki wycinek z bogatej historii; jeśli chcesz się więcej dowiedzieć zajrzyj do Wikipedii.

Express został pierwotnie wydany w listopadzie 2010 i obecnie jest dostępny w wersji 4.17.1. Możesz sprawdzić dziennik zmian, aby uzyskać informacje o zmianach w bieżącej wersji, a GitHub, aby uzyskać bardziej szczegółowe informacje o historii wersji.

Jak popularne są Node i Express?

Popularność danego frameworku jest bardzo ważna, gdyż jest wskaźnikiem, czy będzie rozwijany i nadal będzie dostępna dokumentacja, dodatkowe biblioteki i wsparcie techniczne.

Oczywiście nie ma miarodajnego wskaźnika popularności frameworków po stronie serwera (choć są strony takie jak Hot Frameworks, które próbują oszacować popularność danego framworku na podstawie liczby projektów na GitHub i liczby pytań w serwisie StackOverflow). Lepszym pytaniem jest to, czy Node i Express są wystarczająco popularne, aby nie spotkał ich los niszowych lub zapomnianych platform. Czy mogą liczyć na dalszy rozwój. Czy możesz liczyć na pomoc, gdy będziesz jej potrzebował? Czy istniej możliwość dostania pracy jeśli nauczysz się platformy Express.

Opierając się na liczbie znanych firm korzystających z Express, liczbie osób wnoszących wkład w kod źródłowy oraz liczbie osób zapewniających zarówno bezpłatne, jak i płatne wsparcie, można potwierdzić, że Express jest popularnym frameworkiem!

Czy Express jest restrykcyjny?

Frameworki webowe często określają siebie jako restrykcyjne lub elastyczne.

Frameworki restrykcyjne to takie, które prezentują jeden „właściwy sposób” na wykonanie określonego zadania. Najczęściej wspierają szybki rozwój w określonej dziedzinie (rozwiązywanie problemów określonego typu), ponieważ ten właściwy sposób realizacji zadania jest zwykle dobrze rozpoznany i udokumentowany. Są jednak znaczniej mniej elastyczne w rozwiązywaniu problemów poza obszarem swojej głównej domeny i nie oferują dużych możliwości w kwestii wyboru komponentów czy rozwiązań, których można użyć.

Przeciwieństwem są frameworki elastyczne, które mają znacznie mniejsze ograniczenia dotyczące wyboru połączenia komponentów w celu realizacji aplikacji, a nawet tego, które komponenty należy zastosować. Ułatwiają programistom na korzystanie z najodpowiedniejszych dla nich narzędzi do wykonania określonego zadania, aczkolwiek kosztem jest konieczność samodzielnego szukania takich komponentów.

Express jest elastyczny. Możesz korzystać z dowolnego kompatybilnego oprogramowania w warstwie pośredniej, włączając je w dowolnym miejscu całego łańcucha obsługi żądań. Możesz tworzyć aplikację z kodem w jednym pliku lub podzielić kod na wiele plików, korzystając z dowolnej struktury katalogów. Czasami możesz odnieść wrażenie, że masz zbyt wiele możliwości!

Jak wygląda kod Express?

Tradycyjna aplikacja webowa przetwarzająca dane czeka na żądania HTTP nadchodzące z przeglądarki (lub innego programu klienta). W chwili, gdy żądanie nadejdzie, aplikacja określa akcję, która musi być podjęta. O tym jaką akcję należy wybrać decydują wzorzec zawarty w URL i ewentualne informacje zawarte w żądaniu POST lub GET. W zależności od żądania aplikacja może odczytać lub zapisać informacje w bazie danych, może też wykonywać inne zadania. Gdy aplikacja zakończy akcję wysyła odpowiedź do przeglądarki klienta. Najczęściej tą odpowiedzią jest dynamicznie tworzony dokument HTML zawierający dane dla przeglądarki klienta. Dokument taki tworzony jest na podstawie szablonu HTML, w którym wyróżnione są miejsca na dane.

W kolejnych sekcjach wyjaśnimy typowe elementy kodu, z którymi się spotkasz podczas pracy z Express i Node.

Hello World Express

Na początek zajmiemy się nieśmiertelnym przykładem Hello World (przedyskutujemy każdy fragment kou w tej i następnych sekcjach).

Wskazówka: Jeśli już masz zainstalowany Node i Express (lub zainstalujesz oba według wskazówek zawartych w następnym artykule), zapisz poniższy kod w pliku app.js a następnie uruchom go wpisując w wierszu poleceń:

node ./app.js

var express = require('express');
var app = express();

app.get('/', function(req, res) {
  res.send('Hello World!');
});

app.listen(3000, function() {
  console.log('Example app listening on port 3000!');
});

W pierwszych dwóch wierszach importujemy przez require() moduł o nazwie express a następnie tworzymy Aplikację Express. Zwyczajowo obiekt aplikacji został nazwany app i zawiera metody do ustalania trasy (routing) żądań, konfiguracji warstwy pośredniej, renderowania widoków w HTML, rejestracji silnika szablonów i konfiguracji ustawień aplikacji, które kontrolują jej zachowanie (np. tryb środowiska, czy definicje routingu są wrażliwe na wielkość znaków itd.)

W środkowej części kodu (trzy kolejne wiersze zaczynające  się od app.get) znajduje się definicja trasy. Metoda określa funkcję zwrotną, która zostanie wywołana, gdy pojawi się żądanie GET HTTP skierowane do ścieżki ('/') względem katalogu głównego strony. Argumentami funkcji zwrotnej są obiekty żądania (request) i odpowiedzi (response). W ciele funkcji została wywołana metoda send()  odpowiedzi, która wysyła napis "Hello World!" do klienta.

Ostatni blok odpowiada za uruchomienie serwera, który nasłuchuje na porcie 3000 i drukuje komunikaty logów na konsoli. Gdy serwer już działa możesz otworzyć przeglądarkę i po wpisaniu adresu localhost:3000 powinieneś zobaczyć odpowiedź serwera.

Import i tworzenie modułów

Modułem jest biblioteka lub plik JavaScript, który można zaimportować do innego pliku źródłowego przy użyciu funkcji require()Express jest też modułem, podobnie jak biblioteki funkcji warstwy pośredniej czy baz danych.

W kodzie poniżej widać, jak importujemy moduł przez nazwę, podobnie jak zaimportowaliśmy framework Exress w przykładzie. Najpierw wywołujemy funkcję require() z podaną nazwą modułu ('express'), a potem wykorzystujemy zwrócony przez import obiekt, żeby utworzyć aplikację Express. Mając obiekt aplikacji możemy korzystać z jego właściwości i metod.

var express = require('express');
var app = express();

Oczywiście możesz też tworzyć własne moduły, które moga być importowane w ten sam sposób.

Wskazówka:  Dobrze, żebyś tworzył własne moduły, wtedy Twoja aplikacja będzia złożona z osobnych, łatwo zarządzanych części. Trzymanie całego kodu aplikacji w jednym pliku tworzy trudny do utrzymania i zrozumienia monolit. Korzystanie z modułów pomoże Ci w kontrolowaniu przestrzeni nazw, czyli na zewnątrz modułu będą dostępne tylko te zmienne, które jawnie z niego eksportujesz.

Obiekty, które mają być dostępne na zewnątrz modułu, muszą być przez Ciebie wskazane za pomocą dodanej właściwości obiektu exports. Przykładowy poniżej moduł, zapisany w pliku square.js, exportuje dwie metody: area()perimeter()

exports.area = function(width) { return width * width; };
exports.perimeter = function(width) { return 4 * width; };

Możemy zaimportować moduł funkcją require() i wywołać wyeksportowane metody:

var square = require('./square'); // Here we require() the name of the file without the (optional) .js file extension
console.log('The area of a square with a width of 4 is ' + square.area(4));

Wskazówka:  Możesz też wskazać moduł podając ścieżkę absolutną do jego pliku (lub nazwy, tak jak to już robiliśmy).

Jeśli chcesz wyeksportować cały obiekt jednym przypisaniem (zamiast eksportować osobno każdą jego metodę lub pole) to przypisz ten obiekt do module.exports (możesz również tak zrobić, aby obiekt główny eksportu był konstruktorem lub inną funkcją):

module.exports = {
  area: function(width) {
    return width * width;
  },
       
  perimeter: function(width) {
    return 4 * width;
  }
};

Wskazówka:  Możesz traktować  exports jak skrót do module.exports w obrębie danego modułu. Faktycznie, exports  jest właśnie zmienną zainicjalizowana  wartością module.exports zanim modułe zostanie ewaluowany. Ta wartość jest referencją do obiektu (w tym przypadku pustego). Oznacza to, że exports przechowuje referencję do tego samego obiektu, którego odwołuje się module.exports.  To oznacza także, że przypisując do obiektu exports inną wartość przestaje on mieć związek z module.exports.

Więcej informacji o modułach znajdziesz w dokumentacji API Node: Modules.

Asynchroniczne API

Kod JavaScript częściej wykorzystuje asynchroniczne wykonywanie operacji, których czas może być znaczny. Synchroniczne wykonywanie kodu zakłada, że następna operacja nie może się zacząć, dopóki poprzednia się nie zakończy. Przykładem może być wywołanie synchroniczne dwóch funkcji wysyłających komunikaty do konsoli, które powinny się wyświetlić w kolejności: "First, Second":

console.log('First');
console.log('Second');

Przeciwieństwem jest kod wykonywany asynchronicznie, w którym kolejna operacja jest uruchamiana, i zanim się zakończy, wywołana jest następna. Gdy operacja się zakończy, mechanizm API wywołuje dodatkowe działanie. Poniższy przykład wyświetli na konsoli komunikaty "Second, First", ponieważ najpierw wywoływana jest funkcja setTimeout(), która po uruchomieniu odliczania czasu, natychmiast wraca i jest wywoływana następna funkcja.

setTimeout(function() {
   console.log('First');
   }, 3000);
console.log('Second');

Korzystanie z nieblokującego asynchronicznego API jest znacznie ważniejsze w Node niż w przeglądarce, ponieważ Node jest jednowątkowym, sterowanym zdarzeniowo, środowiskiem uruchomieniowym. "Jednowątkowe" oznacza, że wszystkie żądania skierowane do serwera są wykonywane w tym samym wątku (czyli nie są uruchamiane w oddzielnych procesach, które mogłyby się wykonywać równocześnie). Taki model jest bardzo wydajny pod względem szybkość wykonywania i wykorzystania zasobów serwera, ale powoduje on, że jeśli zostanie wywołana synchronicznie funkcja, której wykonanie trwa długo, to zablokuje ona nie tylko bieżące żądanie, ale także wszystkie pozostałe obsługiwane w tym czasie przez aplikację.

Jest kilka metod do sygnalizowania, że asynchroniczna operacja się zakończyła. Najczęściej wykorzystywana jest rejestracja funkcji zwrotnej podczas wywoływania asynchronicznego. Funkcja zwrotna zostanie wywołana, gdy asynchroniczna operacja się zakończy. Z takiego podejścia skorzystano w przykładzie powyżej.

Uwaga: Korzystanie z funkcji zwrotnych może prowadzić do bałaganu. Jeśli zaczniesz tworzyć sekwencję kolejnych wywołań operacji asynchronicznych, aby wykonały się we właściwym porządku, to w rezultacie powstaje wiele poziomów zagnieżdżonych wywołań. Jest to znany problem o nazwie "callback hell" (piekło funkcji callback).  Skutki problemu można złagodzić stosując dobre praktyki (zobacz http://callbackhell.com/), wykorzystując odpowiednie biblioteki jak np. async, lub możliwości standardu ES6 jak np. Promises.

Uwaga:  Funkcje zwrotne w Node i Express stosują wspólną konwencję, według której pierwszym argumentem funkcji zwrotnej jest wartość błędu. Drugim argumentem jest obiekt zawierający wszystkie dane zwrócone po prawidłowym zakończeniu funkcji wywołującej. Blog The Node.js Way - Understanding Error-First Callbacks (fredkschott.com) zawiera dobre wyjaśnienie, dlaczego ta konwencja jest tak użyteczna.

Tworzenie procedur obsługi tras

W naszej przykładowej aplikacji Hello World zdefiniowaliśmy funkcję obsługującą żądania GET protokołu HTTP skierowane do katalogu głównego strony '/').

app.get('/', function(req, res) {
  res.send('Hello World!');
});

Funkcja zwrotna podczas wywołania otrzymuje obiekty żądania i odpowiedzi jako argumenty. W naszym przypadku funkcja po prostu wywołuje send() na obiekcie odpowiedzi ,żeby wysłać napis "Hello World!". Nie jest to jedyna metoda formułowania odpowiedzi na żądanie. Istnieje wiele metod kończących cykl procesu przetwarzania żądania w odpowiedzi np. możesz wywołać metodę json(), żeby wysłać odpowiedź w formacie JSON lub sendFile(), aby wysyłać plik.

JavaScript tip:  W funkcji zwrotnej obiekty żądania i odpowiedzi mogą mieć dowolne nazwy, ważne abyś zapamiętał, że podczas wywołania tej funkcji zawsze pierwszym argumentem jest obiekt żądania, a drugim obiekt odpowiedzi. Jednak jest głęboki sens w tym, aby oba argumenty nazwać zgodnie z ich przeznaczeniem.

Obiekt aplikacji Express posiada odpowiednie metody do definiowania funkcji obsługujących pozostałe rodzaje żądań HTTP :

checkout()copy()delete()get()head()lock()merge()mkactivity(), mkcol(), move(), m-search(), notify(), options(), patch(), post(), purge(), put(), report(), search(), subscribe(), trace(), unlock(), unsubscribe().

Jest  też jedna specjalna metoda app.all(), która jest wywoływana dla każdego żądania HTTP. Wykorzystuje się ją do instalowania funkcji warstwy pośredniej obsługujących wybraną trasę dla dowolnych żądań. Poniższy przykład (pochodzący z dokumentacji Express) ilustruje przypadek funkcji obsługującej wszystkie żądania (o dowolnej metodzie HTTP) skierowane do ścieżki  /secret (funkcja pochodzi z modułu http).

app.all('/secret', function(req, res, next) {
  console.log('Accessing the secret section ...');
  next(); // pass control to the next handler
});

Metody definiujące trasy są wywoływane na podstawie dopasowania do określonych wzorców w URL jak i wydobywają pewne części z URL i przekazują je jako parametry do funkcji obsługi żądania (jako atrybuty żądania).

Często przydatne jest grupowanie procedur obsługi tras dla określonej części witryny i uzyskiwanie do nich dostępu za pomocą wspólnego przedrostka ścieżki (np. strona z Wiki może mieć wszystkie trasy związane z wiki w jednym pliku i mieć do nich dostęp w postaci ścieżki z prefiksem /wiki/). Za realizacje grupowania odpowiedzialny jest obiekt express.Router.  Wracając do przykładu Wiki: wszystkie trasy możemy umieścić w module wiki.js i wyeksportować je do obiektu Router , jak w przykładzie poniżej:

// wiki.js - Wiki route module

var express = require('express');
var router = express.Router();

// Home page route
router.get('/', function(req, res) {
  res.send('Wiki home page');
});

// About page route
router.get('/about', function(req, res) {
  res.send('About this wiki');
});

module.exports = router;

Uwaga: Dodawanie tras do obiektu Router jest jak dodawanie tras do obiektu app (jak pokazaliśmy to już wcześniej).

Dołączamy utworzony router (nasz plik wiki.js) do głównego pliku aplikacji przez require()  i włączamy go do warstwy pośredniej aplikacji wywołując metodę use(). To spowoduje, że nasza aplikacja będzie teraz obsługiwać dwie ściezki: /wiki/ i /wiki/about/.

var wiki = require('./wiki.js');
// ...
app.use('/wiki', wiki);

W dalszych częściach cyklu pokażemy więcej możliwości konfigurowania ścieżek, w szczególności korzystanie z obiektu  Router w rozdziale Routery i kontrolery.

Warstwa pośrednia

Warstwa pośrednia (middleware) jest bardzo istotną częścią aplikacji Express. Wykonuje ona wiele operacji począwszy od obsługi plików statycznych po kompresje odpowiedzi HTTP. Jeśli funkcje obsługi żądań są ostatnim etapem, w którym wysyłamy odpowiedź do klienta, to funkcje middleware po wykonaniu operacji na żądaniu lub odpowiedzi wywołują następną funkcje w łańcuchu, którą może być kolejna funkcja warstwy pośredniej lub końcowa funkcja obsługi żądania. O kolejnością wywołań funkcji middleware decyduje programista.

Uwaga: Warstwa pośrednia może wykonywać dowolne operacje, wykonywać dowolny kod, dokonywać zmian w żądaniu lub odpowiedzi a także może zakończyć cały cykl przetwarzania żądania w odpowiedź. Jeśli funkcja middleware nie kończy przetwarzania to musi wywołać metodę next(), która przekazuje sterowanie do następnej funkcji warstwy (w przeciwnym przypadku żądanie zostanie zawieszone). 

Większość aplikacji korzysta z funkcji warstwy pośredniej firm trzecich, bo upraszcza to proces projektowania aplikacji webowych, w których korzystamy z ciasteczek, sesji, autentykacji użytkowników, danych w formacie JSON, logowania itd. Lista dostępnych pakietów znajduje się na stronie Express middleware (łącznie z pakietami firm trzecich). Więcej pakietów jest dostępnych poprzez menadżer pakietów NPM.

Pakiet middleware'u, który chcesz wykorzystać w swojej aplikacji, musisz najpierw zainstalować przy pomocy NPM. Załóżmy, że chcesz dodać logger żądań do warstwy pośredniej znajdujący się w pakiecie morgan HTTP:

$ npm install morgan

Teraz powinieneś go dodać do stosu warstwy pośredniej wywołując metodę use():

var express = require('express');
var logger = require('morgan');
var app = express();
app.use(logger('dev'));
...

Uwaga: Funkcje middleware'u i obsługi żadań wywoływane są w kolejności ich deklaracji. Dla wybranych pakietów ich miejsce w łańcuchu wywołań jest bardzo ważne (np. jeśli warstwa sesji zależy od warstwy obsługi ciasteczek, wtedy funkcja obsługi ciasteczek powinna być dodana jako pierwsza). W praktyce prawie zawsze funkcje warstwy pośredniej są dodawane przed definicją funkcji obsługi tras, gdyż w przeciwnym razie obsługa żądań nie będzie miała dostępu do funkcji warstwy pośredniej.

Oczywiście możesz tworzyć własne funkcje warstwy pośredniej, i prawdopodobnie będziesz zmuszony do ich definiowania (jak choćby funkcje obsługi błędów). Jedyną różnicą między funkcjami middleware'u, a funkcjami obsługi żądań jest dodatkowy, trzeci argument funkcji warstwy pośredniej. Argument ten, zazwyczaj o nazwie next, jest funkcją, której wywołanie uruchamia następną funkcje warstwy pośredniej. Każda funkcja middleware'u musi wywołać ten argument, gdyż tego oczekuje kontrakt przetwarzania żądania i odpowiedzi w warstwie pośredniej i to je odróżnia od funkcji, które tego nie robią lub nie posiadają takiego argumentu, na których kończy się cykl życia żądania.

Wywołując metodę app.use() masz dwie możliwości włączania funkcji middleware'u do łańcucha przetwarzania żądania, określając czy funkcja ta obsługuje wszystkie żądania czy tylko o określonej metodzie protokołu HTTP (GET, POST itd.). Możesz też określić dla jakiś ścieżek funkcja ma działać, choć podanie ścieżki przy wowołaniu app.use() jest opcjonalne.

W przykładzie poniżej możesz zobaczyć jak należy dodawać funkcje middleware'u bez i ze ścieżką.

var express = require('express');
var app = express();

// An example middleware function
var a_middleware_function = function(req, res, next) {
  // ... perform some operations
  next(); // Call next() so Express will call the next middleware function in the chain.
}

// Function added with use() for all routes and verbs
app.use(a_middleware_function);

// Function added with use() for a specific route
app.use('/someroute', a_middleware_function);

// A middleware function added for a specific HTTP verb and route
app.get('/', a_middleware_function);

app.listen(3000);

Wskazówka JavaScript: W przykładzie powyżej najpierw zadeklarowaliśmy funkcję middleware'u a następnie dodaliśmy ją jako funkcję zwrotną. W poprzednich przykładach funkcję zwrotną definiowaliśmy w miejscu definiowania funkcji obsługi żądania. W JavaScript oba podejścia są poprawne. 

Dokumentacja Express zawiera o wiele więcej informacji na temat wykorzystania i definiowania warstwy pośredniej w aplikacjach webowych.

Obsługa plików statycznych

Aby Twoja aplikacja mogła obsługiwać pliki umieszczone na serwerze, jak pliki graficzne, CSS czy JavaScript, musi posiadać w warstwie pośredniej odpowiednią funkcję. Na szczęście jest ona częścią Express (jest to jedyna dostarczana z pakietem Express funkcja middlewaru). Jeśli na przykład, chcesz aby aplikacja umożliwiała klientom pobieranie plików z katalogu 'public' (pliki CSS, JavaScript, grafika), który znajduje się w katalogu aplikacji, umieść poniższy kod:

app.use(express.static('public'));

Każdy plik z katalogu `public` jest teraz dostępny pod adresem URL z dołączoną na końcu nazwą tego pliku. Na przykład:

http://localhost:3000/images/dog.jpg
http://localhost:3000/css/style.css
http://localhost:3000/js/app.js
http://localhost:3000/about.html

Możesz wielokrotnie dodać funkcję static(), aby dodać więcej katalogów z zasobami statycznymi. Jeśli plik nie zostanie znaleziony przez jedną funkcję middleware'u to żądanie tego pliku zostanie przekazane do następnej funkcji tej warstwy (kolejność wywołań funkcji jest zogdna z kolejnościa ich deklaracji).

app.use(express.static('public'));
app.use(express.static('media'));

Istnieje też możliwość tworzenia wirtualnych przedrostów w URL do zasobów statycznych, tak aby nie udostępniać ich pod tym samym adresem bazowym URL. W rezultacie zasoby będą dostępne jakby były w odrębnym katalogu. Posłużmy się kolejnym przykładem, w którym podamy ścieżkę montowania katalogu `public` pod przedrostkiem "/media"

app.use('/media', express.static('public'));

Od tego momentu możesz dostać się do plików z katalogu public podając URL zakończony prefiksem  /media z umieszczoną za nim nazwą pliku:

http://localhost:3000/media/images/dog.jpg
http://localhost:3000/media/video/cat.mp4
http://localhost:3000/media/cry.mp3

Więcej dowiesz się zaglądając do rozdziału Udostępnianie plików statycznych w Express.

Obsługa błędów

Błędy mogą być obsługiwane przez jedną lub więcej specjalnych funkcji middleware'u, które muszą posiadać cztery argumenty (w odróżnieniu od pozostałych, trójargumentowych funkcji tej warstwy) (err, req, res, next). Popatrzmy na przykład poniżej:

app.use(function(err, req, res, next) {
  console.error(err.stack);
  res.status(500).send('Something broke!');
});

Funkcje obsługi błędów mogą zwracać odpowiedzi o dowolnej zawartości, ale muszą zostać dodane metodą app.use()  po wszystkich pozostałych funkcjach tej warstwy, aby zostały wywołane na końcu procesu przetwarzania żądania.

Express posiada wbudowany mechanizm obsługi wszelkich błędów mogących wystąpić w aplikacji. Ta domyślna obsługa znajduje się w warstwie pośredniej jako ostania funkcja tej warstwy. Jeśli wywołaniem next() przekażesz błąd do następnej funkcji, w której nic z tym błędem nie zrobisz, to zostanie on obsłużony przez domyślną funkcję, która wyśle do klienta opis błędu wraz z opisem śladu stosu.

Uwaga:  Śledzenie stosu nie jest włączone do środowiska produkcyjnego. Aby je włączyć w trybie produkcyjnym musisz ustawić zmienną środowiskową NODE_ENV o wartości 'production'

Uwaga: HTTP404  i pozostałe "błędy" kojarzone z kodem statusu nie są traktowane jako błędy. Jeśli chcesz je kontrolować to musisz dołączyć odpowiednią funkcję middleware'u. Więcej informacji znajdziesz w FAQ.

Więcje o obsłudze błędów możesz dowiedzieć się w Error handling (Dokumentacja Express).

Korzystanie z baz danych

Aplikacja Express może korzystać z dowolnej bazy danych wspieranej przez środowisko Node (sam Express nie definiuje żadnych specjalnych zachowań ani wymagań odnośnie współpracy z bazami danych). Możliwa jest współpraca z wieloma silnikami BD, włączając takie jak PostgreSQL, MySQL, Redis, SQLite, MongoDB itd.

Pierwszym krokiem jest zainstalowanie sterownika przy pomocy menadżera pakietów NPM. Na przykład zainstalowanie sterownika do popularnej bazy NoSQL MongoDB sprowadza się do wydania polecenia:

$ npm install mongodb

Silnik bazodanowy może być zainstalowany lokalnie lub na zdalnym serwerze. W kodzie aplikacji wymagany jest zainstalowany sterownik, przy pomocy którego łączymy się z bazą i wykonujemy typowe operacje jak tworzenie, odczyt, edycja i usunięcie (CRUD). W przykładzie poniżej (dokumentacja Express) możesz zobaczyć jak znaleźć i odczytać wszystkie ssaki z bazy zawierającej informacje o zwierzętach.

//this works with older versions of  mongodb version ~ 2.2.33
var MongoClient = require('mongodb').MongoClient;

MongoClient.connect('mongodb://localhost:27017/animals', function(err, db) {
  if (err) throw err;

  db.collection('mammals').find().toArray(function (err, result) {
    if (err) throw err;

    console.log(result);
  });
});


//for mongodb version 3.0 and up
let MongoClient = require('mongodb').MongoClient;
MongoClient.connect('mongodb://localhost:27017/animals', function(err, client){
   if(err) throw err;
   let db = client.db('animals');
   db.collection('mammals').find().toArray(function(err, result){
     if(err) throw err;
     console.log(result);
     client.close();
   });
});

Innym, popularnym modelem współpracy aplikacji z bazą danych jest ORM (Object Relational Mapping - mapowanie obiektowo relacyjne). Korzystanie z ORM powoduje, że dane utrwalane w bazie definiujemy jako obiekty lub modele, które są automatycznie mapowane na format bazodanowy. Zaletą takiego podejścia, jest to, że możesz myśleć w kategoriach obiektów JavaScript a nie formatu danych bazy, które w dodatku podlegają sprawdzeniu poprawności danych. Bardziej szczegółowo bazy danych omówimy w późniejszym artykule.

Więcej informacji znajdziesz w Database integration (Dokumentacja Express).

Prezentacja danych (widoki)

Silniki szablonów (określane w Express jako "view engines") pozwalają Ci na łatwiejsze tworzenie dokumentów wysyłanych jako odpowiedź żądania. Taki dokument, a jest to najczęściej plik HTML lub inny typ dokumentu, zawiera specjalne znaczniki, w miejscu których silnik szablonów wstawia, przekazane z funkcji obsługi żądania, dane. Express wspiera wiele różnych silników widoków, więc przy wyborze najbardziej odpowiedniego można posłużyć się zestawieniem Comparing JavaScript Templating Engines: Jade, Mustache, Dust and More.

Jeśli chcesz budować swoją aplikację w architekturze MVC musisz w niej wskazać położenie plików szablonów. W sekcji konfiguracyjnej aplikacji wskaż atrybutami 'views' lub 'view engine' katalog szablonów, tak jak poniżej:

var express = require('express');
var app = express();

// Set directory to contain the templates ('views')
app.set('views', path.join(__dirname, 'views'));

// Set view engine to use, in this case 'some_template_engine_name'
app.set('view engine', 'some_template_engine_name');

Wygląd szablonu zależy od zastosowanego silnika generującego widoki, ale niezależnie od niego przekazanie danych do szablonu wygląda podobnie. Zakładając, że plik szablonu ma nazwę "index.<template_extension>", a w nim znajdują się znaczniki na umieszczenie zmiennych o nazwach `title` i `message`, to w funkcji obsługi żądania powinieneś wywołać Response.render(), żeby klient otrzymał wygenerowany z szablonu dokument HTML:

app.get('/', function(req, res) {
  res.render('index', { title: 'About dogs', message: 'Dogs rock!' });
});

Więcej informacji o szablonach znajdziesz w Using template engines with Express (dokumentacja Express).

Struktura plików

Express nie wymaga sztywnej struktury plików, składających się na aplikację. Kontrolery, widoki, pliki statyczne i pozostałe elementy aplikacji mogą być rozmieszczone w dowolnej liczbie plików rozmieszczonych w dowolnej strukturze katalogów. Choć możliwe jest zawarcie całej aplikacji w jednym pliku, to znacznie bardziej rozsądnym rozwiązaniem jest podzielenie aplikacji na pliki o określonych funkcjach. (np. zarządzanie kontem, blogi, forum dyskusyjne) i zgodnie z architekturą aplikacji (np. model, widok i kontroler jeśli jest to aplikacja w architekturze MVC)

W dalszych rozdziałach będziemy korzystać Express Application Generator, który tworzy modularny szkielet aplikacji, którą możemy łatwo rozbudowywać.

Podsumowanie

Gratulacje, zrobiłeś pierwszy krok w swojej podróży z Express/Node! Mam nadzieję, że dobrze poznałeś najważniejsze zalety środowiska Express/Node i zrozumiałeś ogólne działanie najważniejszych elementów aplikacji (funkcje obsługi żądań, warstwa pośrednia, obsługa błędów, szablony widoków. Powinieneś też rozumieć, że Express jako elastyczny framework, nie narzuca Ci sposobu łączenia elementy aplikacji, ale Tobie pozostawia w tej kwestii wybór.

W założeniu twórców Express jest lekkim frameworkiem. dlatego wiele jego zalet i możliwości pochodzi z bibliotek  i funkcji firm trzecich. Będziemy im się przyglądać bardzie szczegółowo w dalszych częściach cyklu. W następnym artykule będziemy poznawać konfigurację środowiska uruchomieniowego Node.

Zobacz także

W tym module