Node.js 03 Wprowadzenie do modułów

Programiści stworzyli tysiące modułów dla Node.js, które realizują przeróżne zadania. Od bardzo prostych jak left-pad, który uzupełnia ciąg znaków do określonej długości zdefiniowanym przez nas znakiem. Istnieją też moduły jak Express, gdzie mamy do czynienia z pełnoprawnym framework-iem do tworzenia aplikacji webowych. Dziś zajmiemy się modułami w Node.js

Czym są moduły ?

Moduły w Node.js możemy traktować jako pełnoprawne aplikacje (biblioteki), które możemy wykorzystać w naszych projektach. Moduły dzielimy na wbudowane, dostarczane przez twórców Node.js, oraz zewnętrzne tworzone i rozwijane przez niezależnych programistów. Więcej o poszczególnych grupach modułów opowiem Ci nieco później, teraz skupmy się na tym czym owe moduły są i jak z nich korzystamy.

Pierwsze co musisz wiedzieć o modułach to to że, aby ich użyć konieczne jest ich zaimportowanie (jest kilka wyjątków np. moduł console). Import jest banalny, sprowadza się do użycia funkcji require

require('http');

Powyższy kod zaimportuje moduł http, który jest modułem wbudowanym. Import modułów jest operacją na tyle istotną dla działania aplikacji, że odbywa się w sposób synchroniczny. W zawiązku z czym w przypadku niepowodzenia tej operacji aplikacja się nie uruchomi.

Jak pracować z modułami ?

Jak wspominałem wcześniej moduły możemy potraktować jak aplikacje, która może realizować jedno proste zadanie lub dostarczać wiele złożony. Zacznijmy od czegoś prostego, mamy moduł left-pad, realizujący tylko jedno zadanie, uzupełnia przekazany ciąg znaków do określonej długości.

 
// import modułu, który wcześniej zainstalowaliśmy przez NPM
const leftPad = require('left-pad')
 
leftPad('foo', 5)
// rezultat: "  foo" 
 
leftPad('foobar', 6)
// rezultat: "foobar" 
 
leftPad('foo', 5, '0')
// rezultat: "00foo" 
 

Prawda że fajne, nie martwimy się jak to działa pod spodem spełnia jedynie swoje zadanie. Oczywiście dociekliwi mogą popatrzeć jak autor napisał dany moduł, kod znajdziecie w katalogu node_modules, w katalogu projektu jeśli moduł był instalowany lokalnie.

Przy bardziej złożonych modułach ilość dostępnych funkcji rośnie przy czym prostota użytkowania pozostaje bez zmian. Spójrzmy na moduł request, który jest już duuuuużo bardziej rozbudowany:

 
// import modułu
const request = require('request');

// wysłanie żądania typu GET
request
    .get('http://czterytygodnie.pl')
    .on('response', function(response) {
        console.log(response.statusCode) // 200 
    });

// wysłanie żądania typu POST
request
    .post('http://czterytygodnie.pl')
    .form({ key:'value' });

Powyższy kod importuje moduł, a następnie wysyła dwa żądania. Jedno typu GET, gdzie przetwarzamy odpowiedź serwera i wyświetlamy w konsoli kod odpowiedzi. Drugie żądanie typu POST dodatkowo przesyła pole formularza o nazwie „key” i wartości „value”.

Super już wiemy że moduły to takie mniejsze / większe „aplikacje”. Jednak powiem Ci coś jeszcze te „aplikacje” mogą być zbudowane z innych mniejszych aplikacji 🙂 Czyż to nie jest piękne ? Możemy po prostu naszą aplikację poskładać z gotowych klocków i powiedzieć im jak mają współdziałać.

Wyobraźmy sobie teraz taką sytuację, piszemy moduł do wystawiania faktur. Jak wiemy każda faktura musi mieć numer, a klient postawił wymaganie, aby numery były dopełnione zerami do 4 znaków. Czyli numeracja mogła by wyglądać następująco:

FV 0009
FV 0010
FV 0011
FV 0012

I teraz mamy dwie opcje: piszemy dopełnianie zerami sami, lub wykorzystujemy gotowy moduł left-pad. Wybieramy opcję numer dwa, tym samym oszczędzamy czas i pieniądze klienta.

Moduły wbudowane

Wraz z Node.js dostarczane są podstawowe moduły których opis znajdziecie w dokumentacji. Nie jest ich wiele i nie ma potrzeby tego zmieniać, gdyż dzięki społeczności i menadżerowi pakietów NPM mamy dostęp do ogromnej bazy modułów dostarczanych i rozwijanych przez programistów z całego świata. Poniżej krótki opis kilku modułów dostarczanych z Node.js:

http

Moduł pozwala na stworzenie różnego rodzaju serwerów w tym serwera www. Jednak jeśli jesteś przyzwyczajony do serwerów typu Apache, Nginx to tutaj czeka Cię dużo więcej pracy bowiem musisz oprogramować wszystkie aspekty działania serwera.

// import modułu HTTP
const http = require('http');

// tworzymy serwer
http.createServer( function(request, response) {

    // przygotowanie odpowiedzi serwera
    response.writeHead(200, {'Content-Type': 'text/plain'});
    response.end('Hello');

}).listen(3000); // serwer pracuje na porcie 3000

// informacja w konsoli
console.log('Serwer pod adresem http://localhost:3000'); 

Moduł może działać także w trybie klienta, tym samym mamy możliwość wysyłania request-ów jak np. w cURL. Przykładowe żadanie może wyglądać następująco:

// import modułu HTTP
const http = require('http');

// ustawiamy parametry żądania GET
const options = {
  hostname: 'www.czterytygodnie.pl',
  port: 80,
  path: '/',
  method: 'GET',
  headers: {
    'Content-Type': 'application/x-www-form-urlencoded',
    'Content-Length': 0
  }
};

// wysyłamy żądanie 
const req = http.request(options, function(response) {

  // kod odpowiedzi
  console.log(`STATUS: ${response.statusCode}`);

  // ustawiamy kodowanie odpowiedzi
  response.setEncoding('utf8');

  // zdarzenie wywoływane gdy są przesyłane dane z serwera
  response.on('data', function(chunk) {

    console.log(`BODY: ${chunk}`);
  });

  // zdarzenie zostanie wywołane 
  // gdy zostanie zakończone przesyłanie danych
  response.on('end', function() {

    console.log('No more data in response.');
  });
});

// obsługa zdarzenia błędu
req.on('error', function(e) {

  console.error(`problem with request: ${e.message}`);
});

req.end();

Oczywiście możliwości modułu nie kończą się na tych dwóch przykładach. Pełną dokumentację możecie znaleźć tutaj https://nodejs.org/dist/latest-v6.x/docs/api/http.html.

url

Mały moduł służący do parsowania adresów URL. Jego najważniejszą i najczęściej wykorzystywaną metodą jest metoda parse. Zwraca ona obiekt, który został świetnie zobrazowany w dokumentacji co możecie zobaczyć poniżej:

┌─────────────────────────────────────────────────────────────────────────────┐
│                                    href                                     │
├──────────┬┬───────────┬─────────────────┬───────────────────────────┬───────┤
│ protocol ││   auth    │      host       │           path            │ hash  │
│          ││           ├──────────┬──────┼──────────┬────────────────┤       │
│          ││           │ hostname │ port │ pathname │     search     │       │
│          ││           │          │      │          ├─┬──────────────┤       │
│          ││           │          │      │          │ │    query     │       │
"  http:   // user:pass @ host.com : 8080   /p/a/t/h  ?  query=string   #hash "
│          ││           │          │      │          │ │              │       │
└──────────┴┴───────────┴──────────┴──────┴──────────┴─┴──────────────┴───────┘
(all spaces in the "" line should be ignored -- they are purely for formatting)

Więcej o module możecie przeczytać w dokumentacji https://nodejs.org/dist/latest-v6.x/docs/api/url.html

fs

Moduł dostarcza metody przeznaczone do pracy z systemem plików w sposób synchroniczny lub asynchroniczny.

Przykładowa operacja zapisu do pliku mogła by wyglądać następująco:

// import modułu
const fs = require('fs');

// zapis tekstu "Hello Node.js" do pliku "message.txt"
fs.writeFile('message.txt', 'Hello Node.js', function(err) {

  // w przypadku gdy wystąpił błąd rzucamy wyjątek
  if (err) throw err;

  // zapis się powiódł, wyświetlamy komunikat
  console.log('The file has been saved!');
});

Wersja synchroniczna wyglądała by następująco:

// import modułu
const fs = require('fs');

// zapis tekstu "Hello Node.js" do pliku "message.txt"
fs.writeFileSync('message.txt', 'Hello Node.js');

Więcej o module możecie przeczytać w dokumentacji https://nodejs.org/dist/latest-v6.x/docs/api/fs.html

path

Moduł służy do przechowywania i przetwarzania ścieżek plików i katalogów. Działanie tego modułu jest zależne od systemu operacyjnego na którym został uruchomiony Node.js. Pozwólcie że dokładniej to wyjaśnię na przykładzie.

Moduł path udostępnia metodę basename, która zwraca nazwę pliku z przekazanej ścieżki. Co zostało pokazane poniżej:

// import modułu
const path = require('path');

// pobranie nazwy pliku z przekazanej ścieżki
const fileName = path.basename('C:\\tmp\\nazwa_pliku.html');

// wyświetlenie nazwy pliku w konsoli
console.log(fileName);

Po jego uruchomieniu w systemie Windows zostanie wyświetlona nazwa pliku: nazwa_pliku.html, jednak jeśli byśmy przekazali ścieżkę do pliku zapisaną w stylu POSIX (Unix) rezultat byłby nieco inny.

const path = require('path');

// pobranie nazwy pliku z przekazanej ścieżki
const fileName = path.basename('/tmp/nazwa_pliku.html');

// wyświetlenie nazwy pliku w konsoli
console.log(fileName);

Kod nadal uruchamiamy w Windows 😉 Tym razem funkcja basename zwróci: /tmp/nazwa_pliku.html. Jest to właśnie związane ze środowiskiem w jakim został uruchomiony Node.js, jeśli uruchomili byśmy Node.js np. w Ubuntu, Debianie czy innym linuks-ie, sytuacja była by odwrotna. Ścieżki Windows-a były by traktowane jako nazwy pliku.

Czyli „parser” ścieżek dostosowuje się do systemu operacyjnego na którym jest uruchomiony Node.js. Jednak możliwe jest jawne wywołanie „parsera”:

// import modułu
const path = require('path');

path.win32.basename('C:\\tmp\\nazwa_pliku.html');
// zwróci: nazwa_pliku.html

path.posix.basename('/tmp/nazwa_pliku.html');
// zwróci: nazwa_pliku.html

Powyższy kod niezależnie gdzie zostanie uruchomiony, zawsze zwróci prawidłową nazwę pliku. Więcej o module możecie przeczytać w dokumentacji https://nodejs.org/dist/latest-v6.x/docs/api/path.html

console

Moduł konsoli jest odpowiedzialny za wysyłanie danych na standardowe strumienie wyjścia lub do konsoli gdy te nie zostały zdefiniowane. Jako że o samej konsoli zrobiłem wpis w formie wideo to polecam obejrzeć oraz poczytać dokumentację https://nodejs.org/dist/latest-v6.x/docs/api/console.html

process

Jest to jeden z nielicznych modułów nie wymagających importu, jest on bowiem dostępny globalnie. Zadaniem modułu jest dostarczanie informacji o procesie oraz jego kontrola, bowiem każda aplikacja uruchamiana przez Node.js jest osobnym procesem.

Poniżej kilka ciekawszych metod dostarczanych przez moduł:

process.cwd()
Zwraca informację o bieżącym katalogu w którym pracuje aplikacja.

console.log('Katalog: ${process.cwd()}');

process.exit()
Powoduje zakończenie działania aplikacji w sposób synchroniczny z podanym kodem wyjścia. W przypadku nie podania kodu domyślnie zostanie użyty kod 0. Przed zakończeniem działania aplikacji zostaną wywołane wszystkie funkcje przypięte do zdarzenia exit.

process.exit(1);

Powyższy kod spowoduje zakończenie działania aplikacji tak szybko jak to tylko możliwe. Jednak istnieje niebezpieczeństwo zakończenia działania aplikacji przed zakończeniem działania funkcji asynchronicznych. Można tego uniknąć nie posługując się bezpośrednio process.exit, a ustawiając parametr process.exitCode na wartość 1.

process.exitCode = 1;

Więcej o module możecie przeczytać w dokumentacji https://nodejs.org/dist/latest-v6.x/docs/api/process.html

querystring

Moduł który pozwoli Ci w prosty sposób przekształcić obiekt (serializować) na łańcuch zapytań (query string). Czy mówiąc nieco prościej na część składową adresu URL.

// import modułu
const query = require('querystring');

// przykładowy obiekt
var item = {
    name: "Product 1",
    price: 100.00
}

// wyświetlenie łańcucha zapytań
console.log( query.stringify(item) );

W wyniku działania powyższego kodu w konsoli zostanie zwrócony nam ciąg znaków name=Product%201&price=100, gdzie %20 jest odpowiednikiem ” ” (spacji). Operacja odwrócenia tej operacji, czyli zamiany ciągu znaków na obiekt sprowadza się do użycia metody parse jak to zostało pokazane w poniższym przykładzie:

// import modułu
const query = require('querystring');

// przykładowy ciąg znaków do deserializacji
var itemUrl = "name=Product%201&price=100";

// wyświetlenie obiektu po deserializacji
console.log( query.parse(itemUrl) );

Konsola powinna wyświetlić wartość { name: 'Product 1', price: '100' }. Więcej o module i jego możliwościach możecie przeczytać w dokumentacji https://nodejs.org/dist/latest-v6.x/docs/api/querystring.html

Moduły zewnętrzne

Skoro już opowiedziałem Ci co nieco o modułach wbudowanych czas na moduły tworzone i rozwijane przez niezależnych deweloperów. Jeśli będziesz chciał(a) podzielić się swoją pracą z innymi osobami także możesz opublikować swój moduł w repozytorium projektów. Już tłumaczę czym jest owo repozytorium, otóż jest to katalog modułów opublikowanych przez deweloperów wraz z zależnościami pomiędzy nimi. Z repozytorium tego korzysta menadżer pakietów NPM. Jeśli korzystałeś(aś) z linux-a to idea ta nie będzie Ci obca, w systemie OS X także znajdziemy odpowiednik repozytorium i menadżera pakietów jakim jest homebrew.

Instalacja modułów

Moduły możemy instalować na dwa sposoby. Pierwszy najprostszy to menadżer pakietów NPM, który prawdopodobnie został zainstalowany wraz z Node.js. Drugi nieco bardziej skomplikowany to instalacja modułów na piechotę, czyli pobieramy czy to ze strony dewelopera czy też z GitHub-a kod modułu i wrzucamy go do katalogu node_modules w naszym projekcie. Druga metoda może okazać się o tyle upierdliwa, że przy wielu zależnościach pomiędzy modułami i brakiem ich w przygotowanych paczkach będziemy wyszukiwać modułów w określonych wersjach i je instalować w projekcie.

Jako że nie będziemy robili sobie pod górkę, to skupie się na wersji pierwszej czyli NPM. Zacznijmy od podstaw czyli sprawdzamy czy NPM jest zainstalowany, poleceniem:

npm -v

Dostać powinniśmy numer wersji NPM-a. Jeśli dostaliśmy błąd to znaczy że nie mamy menadżera w systemie i powinniśmy go zainstalować. W przypadku Mac OS i Windows instalator Node.js zawiera go w sobie, zaś użytkownicy linux-a instalują poleceniem np.

sudo apt-get install npm

Kiedy mamy już menadżer pakietów sprawdźmy jak owy menadżer działa w praktyce. Zainstalujemy prosty pakiet left-pad w pustym katalogu:

npm install left-pad

Po wykonaniu polecenia z poziomu konsoli, powinien zostać utworzony katalog node_modules (jeśli nie istniał). Kiedy do niego zajrzymy to zobaczymy tam katalog z nazwą instalowanego modułu.

Jednak przy instalacji w pustym katalogu powinien pojawić się komunikat o braku pliku package.json.

package.json jest to specjalny plik w naszym projekcie z meta danymi dotyczącymi aplikacji. Znajdziesz tam nazwę aplikacji, licencję, jej wersję, jakie wykorzystuje moduły i kilka dodatkowych informacji. Możemy taki plik stworzyć sami lub skorzystać z kreatora NPM, uruchamiamy go poleceniem:

npm init

Odpowiadamy na kilka pytań i na końcu weryfikujemy jego poprawność. Jeśli przy uruchamianiu generatora zostanie znaleziony katalog node_modules to znalezione w nim moduły zostaną dodane do listy modułów wymaganych.

Jednak instalując moduły nie są one dopisywane automatycznie do pliku, musimy dodać flagę --save. Tak więc żeby moduł request został dopisany do pliku polecenie instalacji powinno wyglądać następująco:

npm install request --save

Po zakończeniu instalacji modułu zostanie on dodany do listy modułów wymaganych przez aplikację. Tylko spytacie po co to robić? Otóż robi się to po to, aby nie przesyłać tych modułów do systemu kontroli wersji. Po co bowiem śledzić modyfikację w zewnętrznych bibliotekach, skoro instalacja wszystkich zależności sprowadza się do wydania jednego polecenia:

npm install

Wygodne prawda, pobieramy np. GIT-em aplikację i wydajemy polecenie, aby pobrać wszystkie moduły z zależnościami z repozytorium. Jednak jest jeden haczyk 😉 W razie gdyby któryś moduł został usunięty z repozytorium nawet nie bezpośrednio zdefiniowany w naszym pliku package.json. Mogła by się zdarzyć mała tragedia, jak to miało miejsce z pakietem left-pad o czym możecie poczytać tutaj: theregister.co.uk/…/npm_left_pad_chaos. Zalecał bym więc w przypadku produkcji trzymanie kodu wszystkich modułów w repo, tak na wszelki wypadek 😉

Instalacja modułów globalnie

Podczas omawiania instalacji modułów wykorzystując NPM nie wspomniałem o dość istotnej kwestii, mianowicie pakiety instalowane bez specjalnej flagi są instalowane lokalnie. Co to właściwie oznacza ? Otóż tyle że są pobierane i instalowane do katalogu node_modules naszej aplikacji, w większości przypadków jest to najlepsze rozwiązania i nie mamy potrzeby instalacji modułów globalnie w systemie.

Jednak w niektórych przypadkach jest to konieczne np. gdy chcemy, aby jakiś proces pilnował czy nasza aplikacja działa i w razie gdyby z jakiegoś powodu nie działała to ją uruchamiał. Takim modułem jest forever, który powinien zostać zainstalowany globalnie.

npm install forever -g

W powyższym poleceniu pojawiła się dodatkowa flaga -g oznaczająca instalację globalną pakietu.

I to by było na tyle jeśli chodzi o wprowadzenie do modułów. Wyszło tego zdecydowanie więcej niż zakładałem i nie jest to wszystko na temat modułów, jednak zakładam że będziemy jeszcze wracać do tego tematu w kolejnych dniach. Do jutra 🙂

Close