Moduł 1 - Zajęcia 1 - Podstawy Node.js

1.1 Wprowadzenie

Platforma Node.js to środowisko uruchomieniowe (runtime environment) JavaScript po stronie serwera. Oznacza to, że programiści mogą wykorzystać ten sam język programowania zarówno do front-endu, jak i do back-endu dla aplikacji webowych, nad którymi pracują. Nowa wersja Node.js wychodzi zazwyczaj przynajmniej dwa razy w roku.

Zgodnie z ankietą Stack Overflow 2019, Node.js to najpopularniejsze narzędzie w kategorii "frameworki, biblioteki i narzędzia" i korzysta z niego nawet 50% profesjonalnych deweloperów.

1.2 Dziedziny zastosowania technologii

Co sprawia, że Node.js jest tak atrakcyjny dla programistów? Przeanalizujmy po kolei dziedziny w których może zostać on wykorzystany.

Przesyłanie strumieniowe (streaming)

Node.js - dobry wybór jako baza do wykorzystania protokołów przesyłania danych "po kawałku". Można tworzyć serwisy przesyłania strumieniowego do słuchania muzyki lub oglądania filmów. Ma wbudowany moduł przesyłania strumieniowego, pozwalający przekazywać ogromną ilość danych w mniejszych częściach i po kolei. Dzięki temu nie potrzebujesz tymczasowo przechowywać danych w całości lub cashować ich w pamięci aplikacji.

Aplikacje czasu rzeczywistego (real-time)

Możemy tworzyć aplikacje w rodzaju czatu lub wideokonferencji, gdzie ludzie mogą rozmawiać ze sobą w czasie rzeczywistym. Takie aplikacje jak Slack czy Discord zostały napisane z pomocą Node.js. Można również tworzyć narzędzia dla jednoczesnej pracy nad tym samym dokumentem przez kilka osób.

Microservices

Mikroserwisy stały się ostatnimi czasy popularne przy pisaniu aplikacji. Dzięki swojej lekkości i prostocie Node.js to preferowana i łatwo skalowalna technologia do tworzenia i obsługi mikroserwisów.

Aplikacje konsolowe

Dzięki ogromnej ilości dostępnych pakietów w repozytorium NPM można łatwo tworzyć narzędzia (skrypty) działające w konsoli, które mogą na przykład manipulować plikami na serwerze lub jak już wiecie, kompilować pliki scss czy "sklejać" pliki js.

Aplikacje desktopowe

Dzięki frameworkowi Electron możemy budować aplikacje desktopowe na podstawie technologii webowych. Przykładem takich aplikacji są edytory tekstu Atom i Visual Studio Code oraz wspomniany już Slack.

1.3 Podstawowe korzyści

Przyszedł czas porozmawiać o korzyściach płynących z technologii, które niewątpliwie istnieją, skoro jest ona tak szeroko wykorzystywana.

Repozytorium NPM z otwartym kodem źródłowym

Na stronie Node Package Manager znajduje się wiele dostępnych modułów i możesz łatwo znaleźć gotowe rozwiązanie dla swojej aplikacji, zamiast musieć wynajdować koło na nowo.

Skalowalne systemy

Z Node.js łatwo skalować aplikację horyzontalnie, stawiając wiele instancji projektu na różnych serwerach. Dostępne jest również wertykalne skalowanie z wykorzystaniem wbudowanego modułu cluster, który będzie rozgałęziać proces aplikacji na wszystkie dostępne u ciebie rdzenie CPU.

Pasuje do mikroserwisów

Wiele firm wybiera Node.js, kiedy chcą przejść na mikroserwisy. Jest idealny do tego rozwiązania architektonicznego.

Jeden język programowania

Obecnie podstawowym językiem dla front-endu jest JavaScript. Używanie go po stronie back-endu pozwala zatrudnić full-stack dewelopera, a nie dwóch pracowników na różne stanowiska. Oszczędzanie czasu i pieniędzy w biznesie zawsze było i będzie dużym plusem.

Paradygmat asynchronicznego wejścia-wyjścia

Główny problem z operacjami wejścia-wyjścia to oczekiwanie odpowiedzi. Wykorzystanie nie-blokującego API pozwala kontynuować pracę nad kolejnym zadaniem, nie czekając na odpowiedź poprzedniego. Dopiero gdy operacja wejścia-wyjścia zostanie zakończona, otrzymasz powiadomienie przy pomocy callback lub promise. Główna korzyść z takiego rozwiązania polega na zwiększeniu efektywności na rzecz opracowywania większej ilości jednoczesnych połączeń.

Reusable code

Ponieważ podstawowym językiem jest JavaScript, możesz łatwo wymieniać się fragmentami kodu między licznymi komponentami twojego systemu. Co więcej, możesz wykorzystać te same fragmenty kodu czy biblioteki zarówno we front-endzie, jak i back-endzie aplikacji.

Czas wyjścia na rynek

Bardzo często w biznesie pojawia się wspaniały pomysł, który trzeba przetestować na rynku. Przy pomocy Node.js możesz szybko dostarczyć MVP (Minimalna wersja produktu, minimum viable product, produkt wykazujący się minimalnymi, ale wystarczającymi dla pierwszych użytkowników funkcjami), co zmniejszy ilość pieniędzy i czasu potrzebnych dla jego realizacji.

Przetestowane na produkcji

Od momentu utworzenia Node.js w 2009 roku w wielu firmach pojawiła się możliwość sprawdzenia, jak Node.js spisuje się na produkcji. Technologia dobrze się zaprezentowała, dlatego wiele dużych firm zdecydowało się przejść na Node.js.

1.4 Jakie firmy wykorzystują Node.js w swoich aplikacjach?

Lista firm wykorzystujących Node.js w produkcji jest dość obszerna:

  • Netflix,
  • Microsoft,
  • Capital One, ogromna korporacja finansowa wykonuje wiele projektów dzięki szybkiemu developmentowi opartemu o Node.js.
  • Agencje reklamowe, takie jak Fusion Marketing,
  • Walmart w handlu detalicznym,
  • Uber w transporcie,
  • Google,
  • Twitter,
  • GoDaddy

1.5 Instalacja VS

  1. npm init - służy do inicjalizacji nowego projektu Node.js poprzez utworzenie pliku package.json w katalogu projektu. Plik package.json zawiera kluczowe informacje o projekcie, takie jak nazwa, wersja, opis, punkt wejścia (np. index.js), skrypty, zależności i inne metadane. Podczas uruchamiania npm init w wierszu poleceń, użytkownik jest proszony o podanie tych informacji. Można również użyć flagi -y lub --yes, aby zaakceptować domyślne wartości bez interakcji. Dzięki npm init proces tworzenia nowego projektu jest uproszczony, a plik package.json jest generowany automatycznie, co ułatwia zarządzanie zależnościami i konfiguracją projektu.
  2. npm i nazwaBiblioteki -D - (lub równoważne npm install nazwaBiblioteki -D albo npm install nazwaBiblioteki --save-dev) służy do zainstalowania pakietu jako zależności deweloperskiej w projekcie Node.js. Opis:
    • npm install - polecenie instalujące pakiet.
    • nazwaBiblioteki - nazwa pakietu, który chcesz zainstalować.
    • -D lub --save-dev - flaga wskazująca, że pakiet ma być dodany do zależności deweloperskich (devDependencies) w pliku package.json.
    Zależności deweloperskie to pakiety potrzebne wyłącznie podczas tworzenia i testowania aplikacji, ale nie są wymagane w środowisku produkcyjnym. Przykłady takich pakietów to narzędzia do testowania (np. jest), linters (np. eslint) czy transpilery (np. babel). Użycie flagi -D lub --save-dev podczas instalacji pakietu za pomocą npm dodaje go do sekcji devDependencies w pliku package.json, co oznacza, że pakiet jest potrzebny tylko w fazie deweloperskiej projektu. Warto jednak pamiętać, że istnieją inne polecenia, takie jak npm ci, które różnią się od npm install pod względem działania i zastosowania. npm ci jest używane głównie w środowiskach ciągłej integracji i zapewnia szybszą oraz bardziej deterministyczną instalację zależności.
  3. npm i nazwaBiblioteki - służy do instalowania pakietu o nazwie nazwaBiblioteki w projekcie Node.js. Instaluje ono pakiet oraz wszystkie jego zależności, zapisując je w katalogu node_modules i aktualizując plik package.json w sekcji dependencies. Dzięki temu można korzystać z funkcji i modułów dostarczanych przez zainstalowaną bibliotekę w swoim projekcie. Aby zainstalować bibliotekę express, użyj polecenia: npm install express Po wykonaniu tego polecenia, express zostanie dodany do sekcji dependencies w pliku package.json, a jego kod będzie dostępny w katalogu node_modules.

1.6 Komendy

  1. ls - w PowerShell polecenie ls jest aliasem dla cmdletu Get-ChildItem, który służy do wyświetlania zawartości katalogów i uzyskiwania informacji o plikach oraz podkatalogach.
    • ls jest aliasem w PowerShell dla Get-ChildItem, co oznacza, że oba polecenia działają identycznie.
    • Get-ChildItem może być używany z różnymi parametrami, takimi jak -Recurse do rekursywnego przeszukiwania katalogów czy -Force do wyświetlania ukrytych plików.
    • Aby uzyskać szczegółowe informacje o plikach, można użyć: Get-ChildItem | Format-List *
    • Chociaż ls jest powszechnie używany w systemach Unix/Linux, w PowerShell jest on aliasem dla Get-ChildItem. Należy pamiętać, że nie wszystkie opcje dostępne w ls w systemach Unix/Linux będą działać w PowerShell, ponieważ Get-ChildItem ma własny zestaw parametrów i funkcji.
  2. cd - w PowerShell służy do zmiany bieżącego katalogu roboczego. Jest ono aliasem dla cmdletu Set-Location, który umożliwia nawigację pomiędzy katalogami w systemie plików. Przykłady użycia:
    • Przejście do podkatalogu: cd NazwaKatalogu To polecenie przenosi do podkatalogu NazwaKatalogu znajdującego się w bieżącym katalogu.
    • Przejście do katalogu nadrzędnego: cd .. To polecenie przenosi do katalogu nadrzędnego względem bieżącego katalogu.
    • Przejście do określonej ścieżki: cd C:\Users\NazwaUżytkownika\Dokumenty To polecenie ustawia bieżący katalog na C:\Users\NazwaUżytkownika\Dokumenty.
    Chociaż ls jest powszechnie używany w systemach Unix/Linux, w PowerShell jest on aliasem dla Get-ChildItem. Należy pamiętać, że nie wszystkie opcje dostępne w ls w systemach Unix/Linux będą działać w PowerShell, ponieważ Get-ChildItem ma własny zestaw parametrów i funkcji.
  3. Uwagi:
    • W PowerShell można używać zarówno cd, jak i Set-Location do zmiany katalogu. Oba polecenia działają identycznie, ponieważ cd jest aliasem dla Set-Location.
    • Aby wyświetlić bieżący katalog, można użyć polecenia Get-Location lub jego aliasu pwd.
    • W przeciwieństwie do niektórych innych powłok, w PowerShell można bezpośrednio przechodzić między różnymi dyskami, wpisując ich litery, np.: cd D: To polecenie przenosi do katalogu głównego dysku D.

2.1 System modułowy Node.js

2.2 Globalne zmienne

Aby zmienna w Node.js była dostępna globalnie, trzeba zadeklarować ją jako właściwość obiektu Global.

            global.foo = 3;
          

Obiekt Global to odpowiednik obiektu window z przeglądarki. Metoda require, służąca do połączenia modułów nie jest globalna i będzie lokalna dla każdego modułu.

Również lokalnymi dla każdego modułu są:

  • module.export - obiekt odpowiadający za to, co dokładnie będzie eksportować moduł gdy zażądamy go przy wykorzystaniu require;
  • __filename - nazwa pliku wykonywanego skryptu;
  • __dirname - absolutna ścieżka do wykonywanego skryptu.

Wrócimy do nich trochę później i przeanalizujemy je bardziej szczegółowo, gdy przyjrzymy się podłączaniu modułów w Node.js.

W sekcji Global znajdują się między innymi takie klasy, jak:

  • Buffer - klasa wykorzystywana do operacji z danymi binarnymi.
  • process - obiekt w którym znajdują się informacje o danym procesie Node.js

Na przykład właściwość process.argv będzie zawierać tablicę argumentów polecenia podanych przy uruchamianiu skryptu w Node.js. Zerowym elementem będzie ścieżka absolutna dla Node.js, drugim ścieżka pliku który został uruchomiony a następnie dostępne będą podane parametry.

Do pracy z katalogami wykorzystuje się następujące funkcje:

  • process.cwd() zwraca ścieżkę obecnego katalogu roboczego,
  • process.chdir() wykonuje przejście do innego katalogu. Polecenie
  • process.exit() kończy proces ze wskazanym jako argument kodem: 0 - poprawne zakończenie działania, inne statusy np. 1 - oznaczają błąd.

Ważna metoda process.nextTick(fn) zaplanuje wykonanie podanej jako callback funkcji w taki sposób, że zostanie wykonana przed zakończeniem bieżącego "obrotu" Event Loop, (nazwa funkcji może być tutaj nieco myląca, aby przesunąć wykonanie funkcji na następny tick możemy użyć setImmediate)

            setImmediate(function () {
              console.log("setImmediate callback");
            });

            process.nextTick(function () {
              console.log("NextTick callback");
            });

            // NextTick callback
            // setImmediate callback
          

Obiekt process zawiera jeszcze wiele właściwości i metod, z którymi można zapoznać się samodzielnie w dokumentacji do Node.js.

2.3 Moduły

Do podłączenia dodatkowych modułów do twojego projektu w Node.js można zastosować wygodny system zarządzania modułami NPM. Najprościej rzecz ujmując: z jednej strony jest to publiczne repozytorium dla Node.js zawierające stworzone przez społeczność moduły czy biblioteki. Z drugiej strony jest to też narzędzie który instalujemy razem z Node.js i dostępne przez:

Polecenie npm które pozwala tworzyć, usuwać lub aktualizować potrzebne ci moduły, automatycznie uwzględniając przy tym wszystkie zależności wybranego przez ciebie modułu od innych modułów uzupełniających. Instalacja modułu nastąpi po wykonaniu polecenia:

            npm install nazwa-paczki@wersja-paczki flagi
          

Biorąc pod uwagę to, że wszystkie publiczne moduły NPM można łatwo zainstalować przy pomocy npm, dla twojego projektu należy stworzyć plik package.json z listą wszystkich niezbędnych do pracy zależności i później zainstalować na serwerze wszystkie potrzebne moduły dzięki poleceniu:

            npm install
          

Podstawowe flagi przy instalacji to:

  • -S lub --save - moduł instaluje się jako podstawowa zależność. Oznacza to, że moduł jest niezbędny do normalnego funkcjonowania programu niezależnie do tego gdzie jest uruchomiony. W pliku package.json znajdziemy go potem pod kluczem dependancies
  • -D lub --save-dev - oznacza, że moduł zainstaluje się w devDependencies - czyli jest potrzebny tylko podczas developmentu, a nie w środowisku produkcyjnym.
  • -g czyli global - pozwala zainstalować moduł do wykorzystania w dowolnym projekcie na danej maszynie.

Moduły należą do jednej z trzech kategorii:

  1. bazowe (core modules)
  2. plikowe (file modules)
  3. moduły npm (npm modules)

Nazwy bazowych modułów są zarezerwowane i nie powinny być nadpisywane; moduły takie jak fs i os, dostarcza nam środowisko Node.

File Module - gdy tworzysz plik w którym przypisane zostało coś (funkcja, obiekt, itd.) do właściwości module.exports, a później plik ten jest wykorzystywany w innych plikach twojego programu to mówimy o module plikowym.

Moduły npm - to w zasadzie moduły plikowe (często rozbite na wiele plików), które znajdują się w specjalnym folderze o nazwie node_modules.

Gdy wykorzystujesz funkcję require, Node określa typ modułu na podstawie podanej ścieżki/nazwy.

Jeżeli wskażesz moduł który nie jest jednym z core modules, to Node.js będzie szukał w bieżącym katalogu podkatalogu node_modules.

Jeżeli to nie zakończy się sukcesem, to Node.js przejdzie do katalogu-rodzica i znów zacznie szukać katalogu node_modules, i następnie szukał tam modułu.

Proces będzie się powtarzał, dopóki moduł nie zostanie znaleziony lub nie zostanie osiągnięty katalog root.

2.4 Moduły ComonJS

Node.js pracuje z systemem podłączania modułów CommonJS. Każdy moduł CommonJS reprezentuje gotowy do wykorzystania fragment kodu JavaScript, który eksportuje obiekty specjalne który możemy używać w dowolnej ilości miejsc. Dwa główne narzędzia modułów CommonJS to:

  • obiekt module.exports zawierający to, co moduł chce uczynić dostępnym dla innych części systemu
  • funkcja require, która jest wykorzystywana przez jedne moduły do importu obiektu exports z innych.

Stwórzmy plik/moduł module.js

            module.js

            const info = msg => {
              console.log(`Info: ${msg}`);
            };

            const log = msg => {
              console.log(`Log: ${msg}`);
            };

            module.exports = {
              info,
              log,
            };
          

Konstrukcja module - specjalny obiekt, który jest dostępny w Node.js w celu realizacji modułów Common.js. Wszystko to, co będzie przypisane jego właściwości exports, będzie eksportować się z tego modułu.

W innym pliku, main.js, podłączymy ten moduł i wywołajmy eksportowane funkcje:

            const logger = require('./module');

            logger.info('info function');
            logger.log('log function');
          

2.5 Moduły ECMAScript

Zaczynając od wersji: 6.х Node.js wspiera również podłączenie modułów zgodnie ze standardem ECMAScript. Jednak pełne importowanie działa dopiero z wersją 14.x.

Aby wykorzystać moduły ECMAScript musimy skorzystać z jednego z dwóch sposobów. Pierwszy - dodać do plików rozszerzenie .mjs, abyśmy mogli wykorzystać moduły ECMAScript lub drugi - w pliku package.json utworzyć pole "type" z wartością "module". Wykorzystamy drugi sposób i najpierw utworzymy w pustym folderze plik package.json przy pomocy polecenia:

            npm init -y
          

i dodamy w nim pole "type" z wartością "module".

Później utworzymy moduł import.js.

            export const info = msg => {
              console.log(`Info: ${msg}`);
            };

            export const log = msg => {
              console.log(`Log: ${msg}`);
            };
          

W pliku app.js importujemy wskazany moduł i wywołujemy funkcję:

            import { info, log } from './import.js';

            info('info function');
            log('log function');
          

W ten sposób zrealizowaliśmy drugie podejście podłączenia modułów czyli ECMAScript.

3.1 Praca z plikami

Moduł FileSystem (fs) pozwala na pracę z plikami w Node.js. Obecnie najczęściej korzystamy z jego wersji promise, dzięki czemu nie musimy korzystać ze składni callback-ów.

            const fs = require('fs').promises;
          

Najczęściej wykorzystywane funkcje do podstawowych operacji na plikach to:

  • fs.readFile(filename, [options]) - czytanie pliku;
  • fs.writeFile(filename, data, [options]) - zapis pliku;
  • fs.appendFile(filename, data, [options])dodanie do pliku;
  • fs.rename(oldPath, newPath) - zmiana nazwy pliku;
  • fs.unlink(path, callback) - usunięcie pliku.

Przy operacjach z plikami nigdy nie powinniśmy zapominać o obsłudze błędów.

Należy również pamiętać, że parametr data w funkcji readFile, zawiera obiekt będący instancją klasy Buffer, zawierający ciąg przeczytanych bajtów, to znaczy surowe dane.

W przypadku prostych plików tekstowych możemy przekonwertować data metodą toString():

            fs.readFile('readme.txt')
            .then(data => console.log(data.toString()))
            .catch(err => console.log(err.message));
          

Istnieją również metody modułu fs z synchronicznymi wersjami, kończące się na Sync, jak również i wersje przyjmujące callback.

Możemy je importować poprzez:

            const fs = require('fs');
          

Wtedy wszystkie funkcje nie zwracają promise więc nie możemy wykorzystać składni .then. Funkcjom synchronicznym nie jest również potrzebny callback, ponieważ są one blokujące i dlatego też nie są rekomendowane, chyba że wymaga tego obecne zadanie i dobrze rozumiesz, w jakim celu je stosujesz. Zobaczmy przykłady:

            readFileSync

            try {
              const data = fs.readFileSync("readme.txt");
              console.log(data.toString());
            } catch (err) {
              console.log(err.message);
            }
          
            readFile (wersja bez promise)

            const fs = require("fs");

            fs.readFile("readme.txt", function (err, data) {
              if (err) {
                console.log("error", err.message);
              } else {
                console.log(data.toString());
              }
            });
          
            readFile (wersja bez promise)

            const fs = require("fs");

            fs.readFile("readme.txt", function (err, data) {
              if (err) {
                console.log("error", err.message);
              } else {
                console.log(data.toString());
              }
            });
          

Napiszmy skrypt files.js, który będzie odczytał obecny katalog i wyprowadzał do konsoli jego zawartość: nazwę pliku, jego rozmiar i datę wprowadzenia ostatniej zmiany w pliku.

            const fs = require('fs').promises;

            fs.readdir(__dirname)
              .then(files => {
                return Promise.all(
                  files.map(async filename => {
                    const stats = await fs.stat(filename);
                    return {
                      Name: filename,
                      Size: stats.size,
                      Date: stats.mtime,
                    };
                  }),
                );
              })
              .then(result => console.table(result));
          

Przeanalizujmy ten kod bardziej szczegółowo. Na początku podłączamy standardowy moduł fs w wersji z promisami:

            const fs = require('fs').promises;
          

Przy pomocy ścieżki ze zmiennej __dirname odczytujemy wszystkie pliki z obecnego katalogu.

W rezultacie działania funkcji readdir, w zmiennej files otrzymujemy promise który zwróci tablicę nazw plików i katalogów z obecnego katalogu.

Zwracamy więc tablicę promisów gdzie każdy analizuje kolejny element files

Zmienna stats zawierać będzie szczegółowe informacje o każdym kolejnym pliku i katalogu.

Zwracamy obiekt z nazwą pliku oraz

  • stats.mtime - czas ostatniej zmiany pliku
  • stats.size - który określa rozmiar pliku w bajtach.

Rezultat wykonania tego promise, czyli zmienną result, przekazujemy funkcji console.table i dzięki niej wyświetlimy tabelkę z informacjami przy wykonaniu naszego skryptu w konsoli.

Wynik wykonania skryptu: