Moduł 1 - Zajęcia 3 - Framework Express

1.1 Wprowadzenie do Express

Express - to minimalistyczny i elastyczny web framework dla aplikacji Node.js, dostarczający obszerny zestaw narzędzi potrzebnych dla typowej aplikacji backend. Pozwala wygodnie tworzyć między innymi API oparte o protokół HTTP i metodologię REST.

Przystąpmy od razy do praktyki. Utwórz katalog dla swojej aplikacji i przejdź do niego

            $ mkdir myapp
            $ cd myapp
          

Przy pomocy polecenia:

            $ npm init -y
          

utwórz plik package.json dla swojej aplikacji. Teraz zainstaluj paczkę Express w katalogu myapp i zapisz go na liście zależności poprzez polecenie:

            $ npm install express
          

UWAGA: w poniższych przykładach korzystamy z wersji express 4.18.2 - możesz sprawdzić ją w pliku package.json

W katalogu myapp utwórz plik o nazwie app.js i dodaj w niej następujący kod:

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

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

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

Aplikacja uruchamia serwer i nasłuchuje połączeń na porcie 3000. Uruchom ją korzystając z polecenia:

            $ node app.js
          

Aplikacja daje w przeglądarce odpowiedź "Hello World!"" dla zapytania GET adresowanego do root URL (/) (w przypadku uruchomienia aplikacji lokalnie, pełną ścieżką będzie więc [localhost:3000/](http://localhost:3000) lub 127.0.0.1:3000/.

O ścieżce i funkcji która zostanie wywołana dla niej będziemy mówić w skrócie jako o route

URL-e które udostępnia dany serwer w swoim API, nazywamy też często endpoint-ami, są to właśnie swojego rodzaju "punkty styku" pomiędzy frontendem a backendem.

Dla wszystkich pozostałych ścieżek URL odpowiedzią będzie standardowy status 404 - Not Found.

Dla opracowywania zapytań, w routes w Express dodano szereg wbudowanych funkcji. Routing bądź trasowanie określa, jak twoja aplikacja odpowiada na zapytanie klienta dla konkretnego adresu - URL. Każdy route może mieć jedną lub więcej funkcji, które wykonują się przy odpytaniu danej ścieżki. Określenie route w Express ma następującą strukturę:

            app.METHOD(PATH, HANDLER)
          

Gdzie:

  • app to instancja aplikacji Express.
  • METHOD -— metoda zapytania HTTP (GET, POST, PUT, PATCH, DELETE).
  • PATH —- ścieżka na serwerze, w naszym przypadku mamy tylko jedną i jest nią root strony '/'.
  • HANDLER —- funkcja, wykonywana po odpytaniu naszego endpointa.

Przyjrzyjmy się krótko temu, do czego wykorzystuje się każdą z metod HTTP:

  • GET żąda danych o zasobie. Zapytania z wykorzystaniem tej metody mają tylko zwracać dane.
  • POST wykorzystuje się aby utworzyć nowy zasób przy użyciu danych wysłanych w zapytaniu
  • PUT służy do utworzenia lub (częściej) modyfikacji całościowej danego zasobu
  • DELETE usuwa wskazany zasób.
  • PATCH wykorzystuje się do częściowej aktualizacji zasobu. Wrócimy jeszcze do czasowników, gdy będziemy analizować tworzenie pełnego REST API.

W naszym przypadku funkcja HANDLER przyjmuje również dwa parametry, obiekt zapytania req i obiekt odpowiedzi res (istnieje trzeci parametr next do którego wrócimy w dalszej części kursu).

            (req, res) => {
              res.send('Hello World!');
            };
          

Do uruchomienia serwera wywołuje się metodę app.listen(), w której przekazuje się numer portu. Aplikacja zwraca odpowiedź "Hello World!" na zapytania adresowane do root URL (/) lub trasy. Dla wszystkich pozostałych ścieżek (endpointów) które nie są zdefiniowane w naszej aplikacji, na przykład http://localhost:3000/contact, odpowiedzią będzie:

            Cannot GET /contact
            --- 404 Not Found ---
          

Dodamy program obsłużenia route /contact:

            app.get('/contact', (req, res) => {
            res.send('<h1>Contact page</h1>');
          });
          

I teraz URL http://localhost:3000/contact będzie zwracał nam dokument z nagłówkiem Contact page.

Symbol ? w ścieżce definiuje, że poprzedni znak może wystąpić 1 raz lub być całkiem nieobecny. Zdefiniowana poniżej ścieżka obsługuje zarówno /cotact jak i /contact

            app.get('/con?tact', (req, res) => {
              res.send('<h1>Contact page</h1>');
            });
          

Symbol + definiuje, że poprzedzający go znak może wystąpić jeden raz lub więcej. Ta ścieżka obsługuje: /contact, /conntact, /connntact i tak dalej.

            app.get('/con+tact', (req, res) => {
              res.send('<h1>Contact page</h1>');
            });
          

Symbol * definiuje, że na miejscu tego symbolu może znajdować się dowolna ilość innych znaków. Ta ścieżka obsługuje /contact, /conxtact, /con123tact i tak dalej.

            app.get('/con*tact', (req, res) => {
              res.send('<h1>Contact page</h1>');
            });
          

Warto zauważyć, że choć jest taka możliwość, lepiej wybierać określone ścieżki bez korzystania z symboli jeśli nie ma takiej potrzeby.

2.1 Oprogramowanie pośredniczące

Przejdźmy teraz do tematu middleware lub oprogramowania pośredniczącego. W praktyce oprogramowanie pośredniczące to po prostu funkcja przyjmująca trzy argumenty: obiekt zapytania (req), obiekt odpowiedzi (res) i funkcję next. Oprogramowanie pośredniczące wykonuje się kaskadowo lub łańcuchowo. Wyobraź sobie rurę wodociągową, którą płynie woda. Woda pompowana jest przez jeden koniec rury, następnie przepływa przez ciśnieniomierz i zawory, nasze oprogramowanie pośredniczące, zanim wpada do miejsca przeznaczenia - naszej szklanki. Podobnie jak w tej analogii, kolejność middlewares ma znaczenie. Na końcu łańcucha middleware znajdzie się funkcja obsługująca ścieżkę (kończąca się na przykład metodą res.send())

Stwórzmy własne oprogramowanie pośredniczące w naszym pliku app.js przed wywołaniem dowolnej ścieżki.

            app.use((req, res, next) => {
              console.log('Nasze oprogramowanie pośredniczące');
              next();
            });
          

Ta funkcja na ten moment nie robi nic istotnego, ale każde zapytanie przechodzi przez nią, a na konsoli zawsze będzie wyskakiwać nasza wiadomość.

Oprogramowanie pośredniczące (middleware) to funkcje mające dostęp do obiektu zapytania (req), obiektu odpowiedzi (res) i funkcji przetwarzania pośredniego w cyklu "zapytanie-odpowiedź" aplikacji. Funkcja kontynuowania wykonywania z reguły definiowana jest jako next.

Co robi więc middleware?

  • wykonuje kod wspólny dla wielu ścieżek;
  • wnosi zmiany do obiektów zapytań i odpowiedzi;
  • może zakończyć cykl "zapytanie-odpowiedź" i przerwać opracowywanie zapytania;
  • może wywołać następną funkcję przetwarzania z kolejki, poprzez wykonanie funkcji next().

Jeżeli bieżąca funkcja przetwarzania pośredniego nie kończy cyklu "zapytanie-odpowiedź", powinna ona wywołać next() aby przejść do następnej funkcji. W przeciwny razie zapytanie zawiesi się.

3.1 Przekazanie danych na serwer

3.2 Przekazanie parametru do URL

Pierwszy sposób - przekazanie przez parametr. Ścieżki mogą zawierać parametry - nazwane segmenty adresu URL. Nazwa parametru powinna zawierać symbole z przedziału [A-Za-z0-9_]. W określeniu ścieżki, przed nazwą parametru stawia się znak dwukropka. Dodamy następujący handler dla ścieżki:

            app.get('/contact/:id', (req, res) => {
              res.send(`<h1>Contact</h1> Prametr: ${req.params.id}`);
            });
          

Jeżeli teraz zwrócimy się po ścieżce /contact/123 to req.params.id będzie zawierał wartość 123. Ten sposób przekazywania parametrów na serwer wykorzystywany jest bardzo często. Na przykład aktualizacja danych użytkownika może wyglądać następująco:

            app.patch('/user/:userId', (req, res) => {
              const { userId } = req.params;
                // wykonujemy wymagane działania
            });
          

Zobaczymy więcej przykładów gdy będziemy analizować REST API.

3.3 Wykorzystanie parametrów zapytania GET

Drugi sposób - parametry zapytania GET lub też query params. W adresie URL, po którym następuje zwrócenie się do serwera, stawia się znak zapytania ?, za którym następuje lista par klucz=wartość rozdzielonych symbolami &. Na przykład:

            http://localhost:3000/contacts?skip=0&limit=10
          

To najprostszy sposób między innymi na obsłużenie paginacji wyników naszego zapytania. Rezultat takiego zapytania znajduje się w obiekcie req.query. W naszym konkretnym przykładzie:

            {
              skip: "0",
              limit: "10"
            }
          

Jeżeli w zapytaniu GET parametry query nie są podane, na przykład mamy ścieżkę /search bez znaku zapytania i dalszych danych, to req.query domyślnie otrzyma pusty obiekt: {}.

Framework Express zawiera wbudowane narzędzie służące do odczytywania parametrów query, ponieważ jest to bardzo często spotykana praktyka.

3.4 Wysyłanie danych przy pomocy formularza

Przy wysyłaniu danych na serwer zazwyczaj wykorzystuje się metody HTTP: POST, PUT i PATCH. Formularze HTML mogą korzystać z metody POST, zobaczmy więc jak odczytać z nich dane.

Żądanie HTTP składa się między innymi z listy nagłówków (headers) i ciała wiadomości (request body). Zapytanie POST od formularza standardowo zawiera nagłówek Content-Type: application/x-www-form-urlencoded. Zazwyczaj w celu otrzymania wysłanych danych należy podłączyć odpowiedni parser jako middleware, jest on już zawarty w frameworku. Do utworzenia parsera danych od formularzy stosuje się funkcję urlencoded().:

            app.use(express.urlencoded({ extended: false }));
          

Do tej funkcji przekazujemy obiekt definiujący parametry parsowania. Wartość extended: false wskazuje, że rezultat parsingu będzie reprezentować listę par klucz-wartość, a każda wartość może być przedstawiona jako string lub tablica. Gdy parametr ten jest równy true, parser wykorzystuje inną bibliotekę do analizy formatu parametrów.

Przyjmijmy informację od formularza uwierzytelnienia:

            <form action="/login" method="POST">
              <label for="email">Email</label>
              <input type="text" name="email" id="email" />
              <label for="password">Hasło</label>
              <input type="password" name="password" id="password" />
              <button type="submit">Zaloguj</button>
            </form>
          

Po wciśnięciu przycisku w formularzu, przeglądarka wyśle na URL <url naszej aplikacji>/login dane formularza. Będą to dwie zmienne: email i password. Odpowiedzialne są za to wartości atrybutów name w znacznikach typu input. Te dane możemy odczytać po stronie serwera w następujący sposób:

            app.post('/login', (req, res, next) => {
              const { email, password } = req.body;
                // Wykonujemy niezbędne operacje
            });
          

W wyniku tego serwer powinien otrzymać dane w obiekcie req.body, w następującej postaci:

            {
              email: 'Wartość wprowadzona w polu input o name=email',
              password: 'Wartość wprowadzona w polu input o name=password'
            }
          

3.5 Przekazanie JSON

Przy tworzeniu aplikacji webowych na Node.js, często trzeba pracować z danymi w formacie JSON. To podstawowy sposób przekazywania danych dla Web-API. Istnieje również format XML, jednak coraz bardziej się on starzeje z powodu swojej rozwlekłości i wychodzi z użytku. Parser JSON w naszej aplikacji podłączamy w następujący sposób:

            app.use(express.json());
          

Dane w postaci JSON mogą pochodzić między innymi

  • z kodu JavaScript po stronie przeglądarki,
  • z zapytania z innego serwera
  • zapytania curl dla systemu linux
  • Przy użyciu klienta służącego do testowania zapytań HTTP, takiego jak Postman lub Insomnia (więcej o nich dowiemy się na module dotyczącym REST API)

Po tym, jak parser JSON zostanie podłączony, nasze handlers mogą interpretować wartość req.body jako obiekt JavaScript zamiast wartości string.

            app.post('/login', (req, res, next) => {
              const { email, password } = req.body;
                // Wykonujemy niezbędne operacje
            });
          

Dany przykład wskazuje, że wysłany został obiekt JSON z właściwościami email i password. Co najważniejsze, w zapytaniu nagłówek Content-Type powinien zawierać application/json, a ty powinieneś wysłać właśnie wartość typu JSON.

Przeanalizowaliśmy wszystkie podstawowe sposoby przesyłania danych na serwer, które przydadzą się nam później.

4.1 Routing w aplikacji

4.2 Metody Route

Przy pomocy klasy express.Router można utworzyć modułowe, programy obsługi ścieżek (handlers). Instancja Router reprezentuje kompleksowy system pośredniczących programów obsługi i trasowania; z tego powodu często nazywany jest "mini-aplikacją".

            const express = require("express");
            const router = express.Router();

            // określamy bazową ścieżkę
            router.get("/", (req, res) => {
              res.send("To główny router");
            });

            // określamy ścieżkę about
            router.get("/about", (req, res) => {
              res.send("About");
            });

            module.exports = router;
          

Później podłączamy moduł my-router.js w aplikacji:

            const myRouter = require('./my-router');
            ...
            app.use('/my-router', myRouter);
          

Dana aplikacja może teraz opracowywać zapytania adresowane do zasobów /my-router i /my-router/about.

Express wspiera dużą ilość metod trasowania, odpowiadających metodom HTTP, ale z większością nie będziemy nawet mieć do czynienia. Podstawowymi metodami dla nas będą:

  • get
  • post
  • put
  • delete
  • patch

Istnieje także szczególna metoda app.all(), nie będąca odpowiednikiem konkretnej metody HTTP. Ta metoda wykorzystywana jest do ładowania funkcji pośredniczącego opracowywania w ścieżce dla wszystkich metod zapytań. Bywa przydatna, gdy musimy reagować na dowolne zwrócenie się do serwera.

W podanym niżej przykładzie program opracowywania będzie uruchomiony dla zapytań do dla ścieżki /anything, niezależnie od tego, czy wykorzystywany jest GET, POST, PUT, DELETE lub jakakolwiek inna metoda zapytania HTTP, wspierana w module http.

            app.all('/anything', (req, res, next) => {
              console.log('Anything method.');
              next();// przechodzimy do dalszej obsługi zapytania
            });
          

4.3 Metody odpowiedzi

Część metod znajdujących się w obiekcie odpowiedzi (res), wymienione zostały w tablicy poniżej, mogą przekazywać odpowiedź do klienta i zakończyć cykl "zapytanie-odpowiedź". Jeżeli żadna z tych metod nie zostanie wywołana w którejkolwiek funkcji obsługi trasy, zapytanie klienta zawiesi się.

Metoda Opis
res.download() Zaproszenie do ładowania pliku
res.end() Zakończenie procesu odpowiedzi
res.json() Wysłanie odpowiedzi JSON
res.jsonp() Wysłanie odpowiedzi JSON ze wsparciem JSONP
res.redirect() Przekierowanie odpowiedzi
res.render() Wyprowadzenie szablonu widoku
res.send() Wysłanie odpowiedzi różnych typów
res.sendFile() Wysłanie pliku w postaci strumienia obiektów

4.4 Łańcuchy metod

Metoda app.route() pozwala tworzyć programy opracowywania tras, łańcuchy dla konkretnej ścieżki trasy. O ile ścieżka jest taka sama, to dla różnych metod HTTP, wygodne jest tworzenie tras modułowych, aby minimalizować redundancję i ilość błędów. Niżej pokazano przykład połączonych w łańcuch programów opracowywania tras, określonych przy pomocy funkcji app.route().

            app
            .route("/blog")
            .get((req, res) => {
              res.send("Get a list of blog");
            })
            .post((req, res) => {
              res.send("Add a record to blog");
            })
            .put((req, res) => {
              res.send("Update blog");
            });